Hermes Wiki

SOLID, DRY, KISS, and YAGNI

Concept

These are judgment-call heuristics for software design, not enforceable rules — a compiler can't reject code for violating DRY. They exist because "good design" is otherwise too vague to apply consistently, but each one is also easy to over-apply into its own failure mode, which is the part that gets left out when they're taught as unconditional virtues.

SOLID (Robert C. Martin) is five principles for class/module-level design:

  • Single Responsibility — a class should have one reason to change.
  • Open/Closed — open for extension, closed for modification (add new behavior via new code, not by editing existing working code).
  • Liskov Substitution — a subtype must be usable anywhere its supertype is expected, without breaking caller assumptions.
  • Interface Segregation — don't force a class to implement methods it doesn't use just because they're bundled into one fat interface.
  • Dependency Inversion — depend on abstractions, not concrete implementations, so high-level policy doesn't depend on low-level detail.

DRY (Don't Repeat Yourself, from The Pragmatic Programmer) says every piece of knowledge should have a single, unambiguous representation in a system — not "never write similar-looking code twice," but "never let the same fact need to change in two places when it changes."

KISS (Keep It Simple, Stupid) says prefer the simplest design that solves the actual problem, resisting cleverness that isn't earning its complexity cost.

YAGNI (You Aren't Gonna Need It, from Extreme Programming) says don't build capability for a requirement you don't have yet, on the theory that you'll usually guess wrong about what the future actually needs, and the unused generality just adds cost until (if ever) it's needed.

Tradeoffs

Principle applied Benefit at moderate application Cost at over-application
SOLID Modules stay independently changeable and testable; new behavior added without touching stable code Over-applying SRP/OCP produces excessive indirection — a simple feature scattered across five tiny classes and three interfaces, harder to trace than a single class would have been
DRY A fact that changes (a tax rate, a validation rule) changes in exactly one place Merging code that merely looks similar but represents different facts creates a false shared abstraction — see Common pitfall below
KISS Less code to read, test, and reason about; fewer places for bugs to hide Under-applying can mean skipping necessary structure too (a "simple" god-function that's actually just under-decomposed) — KISS isn't license to skip real design work
YAGNI No cost sunk into speculative generality that never gets used Applied too rigidly, it can justify skipping structure a near-certain, not just hypothetical, near-term need would clearly require — YAGNI targets speculative future needs, not the current one poorly disguised as future

The principles pull in slightly different directions on purpose: DRY pushes toward abstraction (merge the duplication), YAGNI and KISS push toward restraint (don't build the abstraction until it's earned). A design that only ever applies DRY without YAGNI/KISS as a counterweight tends toward premature, over-general abstractions; a design that only applies YAGNI/KISS without DRY tends toward copy-pasted logic that drifts out of sync. Using them together, as a check on each other, is the actual skill — not maximizing any one of them in isolation.

When to use / when not to

  • Apply DRY only after seeing genuine duplication show up at least twice for the same underlying reason (the "rule of three" heuristic) — extracting an abstraction after a single occurrence is usually premature, since you don't yet know whether the second occurrence will actually share the first's reasons for existing.
  • Apply YAGNI and KISS by default, every time a design is tempted to add a parameter, config option, or abstraction layer for a need that isn't real yet — the bar is "do I have a concrete requirement today," not "can I imagine a future one."
  • Apply SOLID's five principles as a checklist during code review of a change, not as an upfront blueprint for every class from day one — asking "does this violate SRP now that we're adding this?" is more useful than designing five interfaces before the first requirement exists.
  • Don't apply any of these as an absolute — a small script or prototype doesn't need SOLID's full discipline, and a one-off duplication of three lines doesn't need a shared helper function if the two call sites have no reason to stay in sync.

Common pitfall

Merging code because it looks the same, not because it means the same thing. Two validation functions that happen to both check "is this string non-empty and under 50 characters" look identical today, but if one validates a username and the other validates a product SKU, they represent two independent facts that happen to coincide right now — a future change to username rules (say, allowing longer names) has nothing to do with SKU rules, but a shared "abstraction" now couples them. Applying DRY to this case doesn't remove duplication, it creates a false shared dependency between two things that were never actually the same fact — exactly the failure mode Sandi Metz's "The Wrong Abstraction" documents: a shared abstraction acquires ad hoc parameters and conditionals as its callers' needs diverge, until it's harder to read and change than the original duplication would have been. Metz's fix is not "avoid abstraction" but "when an abstraction has become the wrong one, the fastest way forward is to inline it back into its callers and let each caller's real, now-diverged need be visible again" — duplication is a cheaper mistake than the wrong abstraction, because duplication is at least honest about there being two separate things.

Engineering Lens

In a design review, invoking "SOLID" or "DRY" by name proves nothing on its own — the useful move is being able to say which specific future change a given structure makes cheaper, and at what cost paid today. "This interface exists so a new payment provider can be added without touching the checkout flow" is a real answer; "this follows the Open/Closed Principle" is not, until it's backed by a concrete future change the structure actually protects against. The same discipline applies to catching over-application: if a reviewer can't name the actual duplicated fact that DRY is protecting, or the actual near-term requirement YAGNI would be skipping, the principle is being invoked as ritual rather than reasoned about — and that's the moment to ask whether the simpler, more duplicated, more concrete version might genuinely be the better design right now.

  • Composition vs Inheritance — "favor composition over inheritance" is itself a KISS/YAGNI-flavored judgment call about which structure costs less over time
  • Facade Pattern — an example of Interface Segregation applied deliberately: hiding a large, complex interface behind a smaller one tailored to what callers actually need

Sources

Hermes Wiki