skip to content

How do you decide when applying SOLID and GoF patterns adds value versus when it becomes over-engineering in a Java codebase?

level: principalimportance: should knowfreq 40%

answer

  1. Patterns/SOLID buy change-tolerance; they cost indirection
  2. Speculative generality = single-impl interface / one-product factory
  3. Wrong abstraction is costlier than duplication
  4. Rule of three; refactor TOWARD patterns reactively
  5. Match structure to the actual change profile, not predictions

basics

~20 s

Patterns and SOLID earn their cost only when there's real, recurring change. If a problem is simple and stable, a plain class or method beats an interface plus a pattern. Add abstraction when a second real case appears, not on speculation.

solid answer

~50 s

SOLID and GoF patterns are tools for managing *change and coupling*, and they carry a cost: more types, indirection, and cognitive load. The principal-level judgement is matching that cost to the actual rate and shape of change. Apply them when you have genuine variation (multiple implementations behind an abstraction), volatile dependencies you must isolate for testing (DIP), or extension points the product roadmap really demands (OCP). Avoid them when the code is simple and stable: a single-implementation interface, a `FactoryFactory`, or a pattern with one concrete variant is speculative generality that violates YAGNI and KISS — it makes the code harder to read with no payoff. The heuristic is the 'rule of three' and refactor-to-pattern: write the simple direct code first, and introduce the abstraction reactively when a second or third real case proves the seam is needed. Patterns are a destination you refactor toward, not scaffolding you erect up front. Always weigh team familiarity, debuggability, and the cost of the wrong abstraction (often worse than duplication).

go deeper

for a junior

Knows patterns/SOLID exist and that not every problem needs one, but may default to applying them by the book.

for a middle

Recognizes obvious over-engineering (single-impl interface, needless factory) and prefers the simpler solution when change isn't required.

for a senior

Deliberately defers abstraction until a real second case appears, uses the rule of three, and justifies each abstraction by a concrete change need.

for a principal

Calibrates structure to the system's change history and roadmap, balances conflicting principles, weighs team/maintenance cost and the wrong-abstraction risk, and sets the guidance others follow.

## Framing the question **SOLID** (five OO design principles) and the **GoF patterns** (23 catalogued class-collaboration shapes) all exist to make software *cheaper to change*. They achieve that by adding **indirection** — interfaces, extra classes, injection seams — that decouple parts so each can vary independently. But indirection is never free: every abstraction adds a layer a reader must trace, a type to name, and a place a bug can hide. **Over-engineering** is paying that cost when there's no change to manage. The principal-level skill is *calibrating abstraction to the actual change profile of the system*. ## Why abstraction has a cost - **Cognitive load / indirection:** to understand `service.charge()` you must find which of N `PaymentMethod` implementations runs, where it's wired, and follow the injection. With one concrete type, that indirection buys nothing but obscures the call. - **Speculative generality (a known code smell):** an interface with a single implementation, an abstract class with one subclass, a `Factory` that builds one product, configuration hooks nobody uses. These predict a future that often never arrives or arrives differently — and you maintain the scaffolding meanwhile. This directly violates **YAGNI** and **KISS**. - **The wrong abstraction is worse than duplication.** Once you extract a shared abstraction, code couples to it; when the two cases diverge, you bolt on flags and parameters to the abstraction until it's a tangled mess. Sandi Metz's rule: 'duplication is far cheaper than the wrong abstraction.' Removing a premature abstraction is harder than removing duplication. ## When the cost is justified Apply SOLID/patterns when there is **real, demonstrated variation or volatility**: - **Genuine multiplicity:** you actually have (or imminently will have) several implementations of a behaviour → an interface + polymorphism (OCP, Strategy) pays off. - **Volatile or external dependencies:** a class talks to a database, network, or clock you must isolate for testing or swapability → DIP + injection earns its keep (you can inject a fake). - **A real, sanctioned extension point:** the product genuinely requires third-party or pluggable behaviour → designed-in OCP/Bridge is warranted. - **A recurring, well-understood problem the pattern names:** using a Builder for an object with many optional params, or Decorator for stackable cross-cutting concerns, communicates intent and reuses proven structure. ## Heuristics for the call 1. **Rule of three.** Don't abstract on the first occurrence, or even the second; the third real case reveals the *true* axis of variation, so the abstraction you extract fits reality instead of a guess. 2. **Refactor *toward* patterns, not *to* them up front.** Write the simplest direct code; when change pressure appears, refactor to introduce the seam. Patterns are a target state, not initial scaffolding. (Joshua Kerievsky, *Refactoring to Patterns*.) 3. **Optimize for deletion/change, not prediction.** Simple, even slightly duplicated code is easy to change when requirements clarify; a wrong abstraction is expensive to unwind. 4. **Cost the team.** An obscure pattern the team doesn't know, or layers that make stack traces unreadable, has a real ongoing cost. Maintainability includes *who maintains it*. 5. **Make the change easy, then make the easy change** (Kent Beck) — refactor to the abstraction *as part of* the change that needs it, so the seam is validated by a concrete requirement. ## Balancing the principles against each other The deepest version of this judgement is recognizing that the principles **conflict** and design is choosing the balance: OCP wants extension points, YAGNI says not yet; DRY wants one abstraction, KISS and the 'wrong abstraction' risk say keep them separate until the shared knowledge is proven. A principal engineer doesn't apply rules mechanically — they read the system's change history and roadmap, and add exactly the structure that the *actual* variation demands, deferring the rest. The goal isn't 'maximally SOLID code'; it's *the simplest design that absorbs the changes you can credibly foresee*. ## Signals you've over- or under-engineered - **Over:** interfaces/abstract classes with a single implementation; factories that make one thing; deep inheritance for slight variation; configuration nobody sets; a pattern name in a class but only one concrete participant. - **Under:** the same change must be made in many places (missing DRY/abstraction); a giant `switch` edited for every new variant (missing OCP); a class you can't unit-test because it `new`s its own database (missing DIP).

  • A teammate adds an interface with exactly one implementation 'for testability and future-proofing'. How do you respond?
    Testability is a legitimate reason if the implementation is a volatile/external dependency (DB, network) you must fake — that's DIP earning its keep. But a single-implementation interface over pure in-memory logic is speculative: you can test the class directly, and 'future-proofing' is YAGNI. I'd keep the interface where it isolates a real boundary and drop it where it's just one concrete behaviour wrapped in ceremony.
  • Why is 'the wrong abstraction is worse than duplication' true?
    Once code couples to a shared abstraction, divergence is handled by adding parameters and conditionals to the abstraction, accreting complexity until it serves no case well. Unwinding it means untangling every caller. Duplication, by contrast, is locally removable once the real shared knowledge becomes clear — so it's safer to wait and extract the right abstraction than to extract the wrong one early.

saying these in an interview costs you the question

  • Treating 'more SOLID / more patterns' as automatically better design.
  • Adding single-implementation interfaces or one-product factories and calling it future-proofing (speculative generality).
  • Claiming abstraction is free — every layer adds cognitive and debugging cost.
  • Extracting an abstraction on the first sign of similarity instead of waiting for the real axis of variation.
  • Ignoring team familiarity, debuggability, and the cost of unwinding a wrong abstraction when choosing structure.

context