Skip to content

The Principles Behind the Patterns

Six ideas that generate most of the catalogue — encapsulate what varies, composition over inheritance, and the rest — plus the rule of three.

Beginner19 minUpdated 8 August 2026

Memorising twenty-three patterns is hard. Understanding six principles and deriving the patterns from them is not, and it also tells you what to do when your problem is not in the catalogue.

The principles here are deliberately not SOLID — low-level design already covers those, and they answer a different question. SOLID tells you whether a class is well-formed. These tell you where a pattern comes from.

Conceptual diagramEncapsulate what variesStrategyStateAbstract FactoryProgramme to an interfaceAdapterBridgeComposition over inheritanceDecoratorLoose couplingObserverMediatorHollywood principleTemplate MethodDependency injection
Diagram, as text
  • Encapsulate what varies leads to Strategy
  • Encapsulate what varies leads to State
  • Encapsulate what varies leads to Abstract Factory
  • Programme to an interface leads to Adapter
  • Programme to an interface leads to Bridge
  • Composition over inheritance leads to Decorator
  • Composition over inheritance leads to Bridge
  • Loose coupling leads to Observer
  • Loose coupling leads to Mediator
  • Hollywood principle leads to Template Method
  • Hollywood principle leads to Dependency injection

Encapsulate what varies

Find the part of your system that changes for a different reason or on a different schedule from everything around it, and put a boundary around exactly that part.

This is the single most productive question in design, because it converts a vague feeling ("this file keeps changing") into a concrete move ("this file changes for three unrelated reasons; separate them").

// One function, three reasons to change: new discount rules, new tax rules,
// new currencies. A change to any one risks the other two.
function priceOrder(order, country) {
  let total = order.items.reduce((sum, i) => sum + i.price, 0);
  if (order.coupon === 'SPRING') total *= 0.9;
  if (country === 'IN') total *= 1.18;
  if (country === 'GB') total *= 1.2;
  return total;
}

The varying parts are the discount rule and the tax rule. Naming each one and giving it a boundary is what produces Strategy; naming a family of them that must be chosen together is what produces Abstract Factory.

Programme to an interface, not an implementation

Depend on what a collaborator does, not on which concrete thing does it. "Interface" here means the set of operations you rely on, not necessarily a language keyword — a duck-typed object or a function signature counts.

The practical test is not "is there an interface declaration?" but "can I substitute a different implementation without editing the caller?" If the answer is no, you are coupled to the implementation regardless of what the type declaration says.

This is what makes testing possible at all: a fake is just another implementation, and it can only be substituted where the caller was written against the operations rather than the class.

Favour composition over inheritance

Inheritance is the strongest coupling in object-oriented programming: the subclass depends on the superclass's structure, its protected members, and the order in which it calls its own methods. Change any of those and every subclass can break.

Composition is a weaker relationship. The object holds a collaborator and calls its published operations, nothing more.

The concrete failure inheritance produces is combinatorial subclassing. Suppose a report can be compressed or not, encrypted or not, and cached or not:

// Inheritance: one class per combination, and eight is only the start
class Report {}
class CompressedReport extends Report {}
class EncryptedReport extends Report {}
class CompressedEncryptedReport extends CompressedReport {}
// ...and CachedCompressedEncryptedReport, and so on
// Composition: three independent wrappers, any combination, no new classes
const report = cached(encrypted(compressed(baseReport)));

Three orthogonal concerns give eight subclasses and three wrappers. Four concerns give sixteen subclasses and four wrappers. That is Decorator, and it exists because inheritance multiplies where composition adds.

Keep interacting objects loosely coupled

Two objects are loosely coupled when each can change without forcing a change in the other. The measure is not "do they know about each other" but "how much do they know".

An object that publishes an event knows only that an event happened. An object that calls four services by name knows those four services exist, what they are called, what they need, and in what order. The first can gain a fifth consumer without editing; the second cannot.

Loose coupling is what Observer and Mediator are for — and it is not free. Coupling you can see in a function signature is easier to reason about than coupling you have to search for. Trade explicit coupling for invisible coupling deliberately, not by default.

Do not call us, we will call you

The Hollywood principle: a lower-level component should not reach up and drive the framework. The framework calls into it at the points it defines.

This is the shape of every plug-in system, every lifecycle hook, and every test framework you have used. It is also what separates two patterns that look similar:

  • Template Method fixes the sequence in a base class and calls down into steps you override. The framework owns the order.
  • Strategy hands the whole behaviour over. The caller owns the order and simply asks the strategy to do its job.

The same principle read one more time gives you dependency injection: a class does not fetch its collaborators, it is handed them.

Ask for what you need, not for what has it

Also known as the Law of Demeter, and best stated as a smell: order.customer.address.country.taxRate couples the caller to four types, any of which can be reshaped and break it.

// Knows the whole object graph
const rate = order.customer.address.country.taxRate;

// Knows one operation
const rate = order.taxRate();

The second is not merely shorter. It moves the knowledge of how the rate is found into the object that owns the data, so a change to the graph touches one place. Long chains are the clearest early signal that behaviour has drifted away from the data it operates on — the anaemic-model problem the anti-patterns topic returns to.

And the one that stops you

The rule of three. Do not abstract at the first occurrence. At the second, note the duplication and leave it. At the third, you can finally see what actually varies between the cases — and only then can you name the abstraction correctly.

The reason is not laziness. Two examples cannot distinguish "these differ by a currency" from "these differ by a formatting rule that happens to include a currency". The third example decides it. Abstracting at two is a coin flip, and a wrong abstraction is more expensive than the duplication it replaced, because every future case must now be bent to fit it.

Check your understanding

Common mistakes

Treating "composition over inheritance" as "never inherit". Inheritance is right when the subtype is genuinely substitutable and the variation is one axis. The rule targets inheritance used for code reuse.

Declaring an interface and calling it decoupling. If the caller narrows the type, checks which implementation it got, or depends on behaviour only one implementation has, nothing has been decoupled.

Encapsulating what does not vary. A boundary around a stable part is pure cost. Boundaries belong where change happens, and the evidence is your own change history.

Applying the Law of Demeter as a syntax rule. Adding a delegating method for every chain link produces a class full of pass-throughs. The point is moving behaviour to the data, not shortening lines.

Abstracting at two occurrences because the third feels inevitable. It usually is, and it usually differs from what you assumed.

Reaching for loose coupling everywhere. An event bus between two collaborators that could simply call each other makes the flow unreadable for no gain.

Practice

A pricing function has grown to 200 lines and mixes discount rules, regional tax rules and currency formatting. Using 'encapsulate what varies', describe how you would find the boundaries — and explain how you would know afterwards whether you drew them correctly.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The evidence is in the change history, not in how the code looks.

Show a strong answer

Find the boundaries from the reasons to change, not from the shape of the code.

  • Read the last twenty commits that touched the file. Group them by why they happened: a new promotion, a tax rate change, a new market, a formatting fix. Those groups are the axes. This is real evidence, and it routinely disagrees with the boundaries you would guess by reading.
  • Check whether the groups are independent. Tax rules changing when a promotion is added means they are entangled, and the boundary between them is not where you think it is.
  • Name each axis as a noun the business would recognise — discount rule, tax policy, money formatting. If a boundary cannot be named without using the word "helper" or "manager", it is not a real axis.

Draw the boundaries: a discount rule and a tax policy each become an interchangeable thing selected at runtime (Strategy). Currency formatting is not a variation at all — it is a separate responsibility that belongs with money, so it moves rather than becoming a strategy.

How you know afterwards — and this is the part people skip:

  • The next change touches one boundary. Adding a market should edit tax policy only. If it also edits discounts, the split was wrong.
  • Each part is testable without the others. If testing a discount requires constructing a country, they are still coupled.
  • The number of boundaries is small. Three or four named axes is a design; eleven is the 200-line function with extra files.

What would tell you not to split at all: if the change history shows the file is edited twice a year and always for the same reason, the length is not a design problem, and splitting it buys nothing.

Explain the difference between Template Method and Strategy purely in terms of the Hollywood principle, then give one scenario where you would deliberately choose the more tightly coupled of the two.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask who owns the order of the steps.

Show a strong answer

The difference is who calls whom.

  • Template Method inverts control. The base class owns the sequence and calls down into the steps a subclass overrides. The subclass never decides the order; it fills in blanks. That is "do not call us, we will call you".
  • Strategy does not invert anything. The caller holds a strategy object and calls it. The strategy owns its whole behaviour, including any internal ordering, and knows nothing about the caller.

The coupling follows from that. Template Method binds the variant to the base class at compile time through inheritance, and the subclass depends on the base class's protected surface and its call order — the strongest coupling available. Strategy binds at runtime through composition, and the two sides share only the operation's signature.

When I would still choose Template Method:

An import pipeline where every format must run validate → parse → normalise → persist in that order, because skipping normalisation silently corrupts data. The order is a correctness invariant, not a preference.

  • With Template Method the invariant lives in one place and cannot be violated by a new format, because a subclass has no opportunity to reorder anything.
  • With Strategy each format would receive the whole job, and the fourth implementer eventually parses before validating. Nothing structural stops them.

The trade I am accepting: a format variant can no longer be swapped at runtime, cannot be reused outside the hierarchy, and is harder to test in isolation. That is worth it when the sequence is the thing being protected — and not worth it when the sequence is merely conventional.

The middle option worth naming: keep the fixed sequence in a plain function and have it call injected steps. The order stays in one place, the steps stay composable, and no inheritance is involved.

Key takeaways

  • Encapsulate what varies is the most productive question in design, and the evidence for what varies is your change history.
  • Programming to an interface means substitutability, not the presence of an interface keyword.
  • Composition adds where inheritance multiplies — orthogonal concerns are the signal, and Decorator is the result.
  • Loose coupling is a trade, not a virtue: invisible coupling is harder to reason about than coupling you can see in a signature.
  • The Hollywood principle separates Template Method (framework owns the order) from Strategy (caller owns it), and gives you dependency injection.
  • The rule of three exists because two examples cannot distinguish incidental from meaningful difference. A wrong abstraction costs more than the duplication it replaced.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994) — the introductory chapter states the "encapsulate what varies" and composition principles.
  • Karl Lieberherr and Ian Holland, "Assuring Good Style for Object-Oriented Programs" (IEEE Software, 1989) — the original statement of the Law of Demeter.
  • Barbara Liskov and Jeannette Wing, "A Behavioral Notion of Subtyping" (ACM TOPLAS, 1994) — what substitutability actually requires.
  • Martin Fowler, Refactoring: Improving the Design of Existing Code, second edition (Addison-Wesley, 2018).
  • Sandi Metz, Practical Object-Oriented Design in Ruby (Addison-Wesley, 2012) — the case for waiting before abstracting.
  • Andrew Hunt and David Thomas, The Pragmatic Programmer (Addison-Wesley, 1999) — orthogonality and the cost of coupling.

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonReading Class and Sequence Diagrams
Lessons in this track