Skip to content

9.3.1 — How to Use a Design Principle

A pull request lands. It adds a fourth payment method to the delivery app, and it touches nine files. A reviewer writes:

"This violates the Open/Closed Principle."

That comment is true and almost useless. The author now knows a rule was broken and still does not know what to do, why it matters here, or what it will cost to fix. Half the time they will reply "OK but it works", and the reviewer will have no answer that is not an appeal to authority.

Here is the same review comment, written by somebody who understands what the principle is for:

"Adding the fifth method will touch these same nine files again. Two of them are switch statements the compiler will not warn you about if you miss one, and last quarter we missed one. If we put the four methods behind one interface now, the fifth is one new file."

No principle was named, and the point landed. That difference is what this chapter is about.

1. What a principle actually is

A design principle is a name for a mistake that many people made, and a short description of the shape that avoids it.

That is the whole thing. Somebody spread one business rule across eleven files, felt the pain for two years, and wrote down what they wished they had done. Somebody else built a flexible plugin system for a feature that was cancelled, and wrote down that lesson too. The principles in this chapter are compressed experience, and the compression is why they sound like commandments when they are really more like the advice on the side of a ladder.

That immediately tells you how to use one. A principle is not a rule you obey. It is a prediction you check. Every principle predicts a specific future pain. So the question is never "does this code follow the principle?" It is:

Is the pain this principle predicts actually going to happen here?

If yes, apply it. If no, applying it costs you something and buys you nothing.

2. All of them serve one goal

Every principle in this chapter is machinery for the definition from 9.1: good code is code where the next change is cheap and safe. They attack it from different angles, and it is worth seeing the map before the details.

PrincipleThe pain it prevents
DRYone rule copied, then updated in one copy only
KISScode nobody can safely change because nobody understands it
YAGNIflexibility built for a future that never arrived
Single Responsibilityone file that two teams both need to edit
Open/Closedevery new case forces edits to working code
Liskov Substitutiona subclass that breaks callers written for its parent
Interface Segregationimplementing methods you do not need, badly
Dependency Inversionyour business rules welded to one vendor

Read that column on its own and you can see the shape of the whole chapter. Every row is a story someone lived through.

Notice also that the eight split into two groups, and knowing which group you are in stops most confusion.

DRY, KISS and YAGNI answer "should I write this code at all, and how much of it?" They are about restraint. They fire before you write anything.

The five SOLID principles answer "given that this code must exist, what shape should it have?" They are about arrangement. They fire once you know what you are building.

3. They contradict each other, and that is the point

This is the part most treatments leave out, and it is the part that makes principles usable by an adult instead of a zealot.

DRY pushes you to merge similar code. YAGNI and KISS push you to leave it alone. Two functions that look alike: DRY says extract, YAGNI says wait until you know they are really the same thing.

Open/Closed pushes you to add plug points. YAGNI says a plug point nobody uses is dead weight. Both are right, and which one wins depends entirely on whether a second case is actually coming.

Single Responsibility pushes you to split. KISS says four files where one would do is not simple. Splitting has a real cost: you now open four files to follow one flow.

So the principles do not form a checklist that can be satisfied all at once. They are forces pulling in different directions, and design is choosing where to stand between them. Anybody who tells you one of them always wins has stopped thinking.

The tiebreaker, every time, is the question from 9.1: which choice makes the next change cheaper and safer? That question has an answer for your specific codebase, this week, which is more than any principle can offer on its own.

4. Every principle costs something

Applying a principle is never free, and being able to state the cost is what separates a design argument from a religious one.

Take the most common move in this whole chapter: putting something behind an interface so it can vary. Here is what you gain and what you pay, both concretely.

typescript
// Before: direct and obvious. One file, one hop to read.
class OrderService {
  async place(order: Order) {
    await this.stripe.charges.create({ amount: order.total.cents });   // (1)
  }
}

// After: flexible. Two files, two hops to read.
interface PaymentGateway {                                            // (2)
  charge(amount: Money): Promise<ChargeResult>;
}

class OrderService {
  constructor(private readonly payments: PaymentGateway) {}           // (3)
  async place(order: Order) {
    await this.payments.charge(order.total);                          // (4)
  }
}

Line (1) names a specific payment company directly. To find out what happens when an order is placed, you read this one line and you are done.

Line (2) introduces a role, and line (3) takes whoever is playing it. Line (4) now says what happens without saying who does it.

What you gained: the payment company can be swapped without touching this file, and this class can be exercised without the network.

What you paid, and it is real: somebody reading place now has to go somewhere else to find out what actually happens. Add nine more interfaces like this and understanding one request means opening eleven files. That is a genuine cost, and teams that apply this move everywhere by reflex end up with codebases where nothing is hard and nothing is findable.

The honest rule: apply a principle when the pain it prevents is real, and be able to name what you paid. "We put payments behind an interface because we are switching providers next quarter and because it lets us run the order flow without the network" is a design decision. "We put payments behind an interface because Dependency Inversion" is a ritual.

5. When to apply, in one procedure

Run this in a review or on your own code.

Step 1: name the pain, not the rule. What exactly goes wrong if this stays as it is? If you cannot describe a concrete bad outcome — a bug, a slow change, a file two teams fight over — there may be no problem.

Step 2: ask whether it is coming. Not "could it happen" but "is it happening, or scheduled". A switch on three payment methods is fine if the business has had three for six years. It is a problem if there is a fourth in the roadmap.

Step 3: price the fix. How many files, how much indirection, how much harder to read afterwards.

Step 4: compare. If the fix costs less than the pain, do it. If not, write down the decision and move on, because "we chose not to abstract this yet" is a real answer and a useful one for the next reader.

Step 5: leave a trail. If you decided against, a one-line comment saying why saves the next person from re-deriving it. If you decided for, the reason belongs in the pull request description so it can be revisited when circumstances change.

6. The two failure modes

Under-applying looks like: the same rule appears in five files, a 900-line function, a class every team edits, a switch that must be found in nine places. Symptoms: changes take long, changes break unrelated things, and everyone is scared of one particular file.

Over-applying looks like: an interface with one implementation that will never have a second, a factory that builds one thing, four layers that each pass the call straight through, configuration for a value that has never changed. Symptoms: reading one behaviour means opening six files, new joiners take months, and simple changes require touching every layer.

Both are expensive. Over-applying is the more fashionable mistake, because it looks like craftsmanship and is usually done by people who have just learned the principles, so nobody wants to challenge it in review. If you take one habit from this chapter, take this one: ask what the second implementation is going to be, out loud, before adding the interface. If nobody can name it, wait.

7. How the rest of the chapter runs

Each of the next nine pages takes one principle and gives it the same treatment: the story of the pain it came from, what it actually says once the slogan is stripped off, line-by-line code showing the violation and the fix, when it is wrong to apply, and how it comes up in interviews.

The order is deliberate. The three restraint principles come first — DRY (9.3.2), KISS (9.3.3), YAGNI (9.3.4) — because they decide whether code gets written at all. Then the five shaping principles, in the SOLID order everyone quotes: Single Responsibility (9.3.5), Open/Closed (9.3.6), Liskov Substitution (9.3.7), Interface Segregation (9.3.8), Dependency Inversion (9.3.9). The chapter closes with the smaller rules that experienced engineers actually use day to day and that never made it into an acronym (9.3.10).

Recall

  • A design principle is a name for a mistake many people made, plus the shape that avoids it. It is a prediction to check, not a rule to obey. Ask: is the pain it predicts actually going to happen here?
  • All of them serve one goal from 9.1: the next change should be cheap and safe.
  • They split into two groups. DRY, KISS and YAGNI decide whether code gets written at all. The five SOLID principles decide the shape of code that must exist.
  • They contradict each other on purpose. DRY says merge, YAGNI says wait. Open/Closed says add a plug point, YAGNI says not yet. They are forces to balance, not boxes to tick. The tiebreaker is always which choice makes the next change cheaper.
  • Every principle costs something, usually indirection: one more file to open to follow one flow. Be able to name what you paid, or you are performing a ritual.
  • Two failure modes. Under-applying: one rule in five files, a file every team edits, changes that break unrelated things. Over-applying: an interface with one implementation, layers that pass calls straight through. Over-applying is the more fashionable mistake because it looks like craftsmanship.
  • In review, name the pain, not the rule. "The fifth payment method will touch these nine files again" beats "this violates Open/Closed".

Self-test: What is a design principle, in one sentence, without using the word "rule"? Give two principles that pull against each other and the question that settles them. Name the cost of putting something behind an interface. Which failure mode is more common among engineers who have just learned SOLID, and why does nobody challenge it?