This is the first post in a series on design patterns. Before we spend the next few weeks pulling apart MVC, hexagonal architecture, CQRS, the Observer, and the rest of the catalog, it's worth getting one thing straight: most of what gets called "design patterns" in practice is cargo cult. People memorize the 23 names from the Gang of Four book, sprinkle a FactoryFactory into a codebase that needed a function, and call it engineering.
So here is the thesis, stated up front: a design pattern is not a thing you add to your code. It's a name for a shape your code already wants to take, given the constraints you're under. The pattern is the label, not the artifact. And a large fraction of the famous patterns exist only because the language you're writing in is missing a feature. Change the language, and the pattern evaporates. The skill worth building is not "knowing the patterns." It's knowing when a pattern is paying rent, and when you've imported a solution to a problem you don't have.
Where the idea actually came from
The word "pattern" in software comes from a building architect, not a programmer. Christopher Alexander wrote A Pattern Language in 1977, describing recurring solutions to problems in towns and buildings. "Light on Two Sides of Every Room." "Six-Foot Balcony." Each pattern named a problem, the forces pulling on it, and a configuration that resolved those forces. The key word is forces. A pattern was never a blueprint you copied. It was a way of talking about a tension and how it tends to resolve well.
In 1994, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, the people the industry calls the Gang of Four, published Design Patterns: Elements of Reusable Object-Oriented Software and borrowed Alexander's framing for code. They catalogued 23 patterns, each with a name, an intent, a structure, and a discussion of tradeoffs. That structure section matters and almost everyone skips it. The book was a vocabulary, a way for two engineers to say "let's put a Strategy here" and both understand the shape being proposed without drawing it on a whiteboard.
That is the part worth keeping. A pattern is a compression of communication. When you say "this is a Repository," a teammate who knows the word instantly understands that you're separating domain logic from data access behind a collection-like interface, and they can argue with that decision in one sentence instead of ten. Patterns are most valuable as shared language, and only incidentally valuable as code.
The catalog is half a workaround for missing features
Here is the uncomfortable part, and it's been known for almost thirty years. In 1996 Peter Norvig gave a talk called Design Patterns in Dynamic Languages where he made a simple observation: of the 23 Gang of Four patterns, 16 become "invisible or simpler" in a sufficiently expressive language. They aren't deep truths about software design. They are scaffolding you build to work around what the language won't let you say directly.
The cleanest example is Strategy. In a language without first-class functions, swapping an algorithm at runtime means wrapping each algorithm in an object that implements a shared interface. In Java circa 2004 that looked like this:
interface DiscountStrategy {
BigDecimal apply(BigDecimal price);
}
class BlackFridayDiscount implements DiscountStrategy {
public BigDecimal apply(BigDecimal price) {
return price.multiply(new BigDecimal("0.7"));
}
}
class Checkout {
private final DiscountStrategy strategy;
Checkout(DiscountStrategy strategy) { this.strategy = strategy; }
BigDecimal total(BigDecimal price) { return strategy.apply(price); }
}
That is the Strategy pattern, textbook. Now here is the same thing in Python:
def black_friday(price):
return price * 0.7
def checkout(price, strategy):
return strategy(price)
checkout(100, black_friday)
There is no pattern here. There is a function passed as an argument. The "pattern" was never about algorithms being swappable; it was about the fact that Java in 2004 had no way to pass behavior around except by wrapping it in a class. Java got lambdas in 2014 and the elaborate version became a relic. Same story for Command (a function plus its arguments, which is a closure), Iterator (built into every modern language's for loop), and Template Method (pass the varying step as a function). The pattern was a symptom of a language gap, not a design insight.
This is the single most useful lens for reading any pattern catalog. Before you reach for a pattern, ask: is this solving a real structural problem, or is it ceremony I'm performing because my language can't express the simple version? If your language has first-class functions, "I need a Strategy" usually means "I need to pass a function." Recognizing that saves you three files and an interface nobody else will ever implement.
The patterns that survive are about boundaries, not features
If 16 of the 23 dissolve under a better language, what about the rest? The ones that survive tend to share a property: they are about boundaries between parts of a system you don't control, not about local code structure. These are the patterns worth keeping in your head, because no language feature makes them go away.
Adapter is the clearest survivor. When you integrate a third-party SDK whose interface doesn't match what your code expects, you wrap it. That wrapper is not ceremony; it's a contract boundary that lets you swap the vendor later and contains the blast radius of their API changes to one file. Here's the shape, and notice it earns its keep:
# Your domain speaks this language.
class PaymentGateway(Protocol):
def charge(self, cents: int, token: str) -> str: ...
# Stripe's SDK speaks a different one. The adapter translates.
class StripeAdapter:
def __init__(self, client): self.client = client
def charge(self, cents: int, token: str) -> str:
intent = self.client.PaymentIntent.create(
amount=cents, currency="usd", payment_method=token, confirm=True
)
return intent.id
The day you migrate from Stripe to Adyen, you write one new adapter and change one line of wiring. Every call site, every test that mocks PaymentGateway, stays untouched. That is a pattern paying rent. It survives because the problem it solves, "my code and their code disagree about shape," is not a missing language feature. It's a permanent fact of building on top of other people's software.
The other durable ones are similar. Facade hides a messy subsystem behind a small surface so the rest of your code doesn't couple to its internals. Observer (and its grown-up cousin, pub/sub) decouples the thing that changes from the things that care about the change, which is the backbone of every event-driven system. Repository puts a seam between your domain and your database. None of these are workarounds for syntax. They're all about drawing lines so that change on one side doesn't leak to the other. That's design. The rest is mostly typing.
The cost nobody puts on the slide
Patterns are not free, and the cost is almost never discussed when someone proposes one. Every pattern adds indirection, and indirection is a tax you pay on every future read of the code. A Strategy interface with exactly one implementation is not flexible; it's a misdirection that forces the next reader to chase an abstraction to discover there was never a choice. An AbstractFactory that produces one concrete product is a maze with one room.
I have seen a production codebase where a single HTTP endpoint went through a Controller, a Service, a Facade, a Strategy selected by a Factory, and finally a Repository, to read one row from one table. Six indirections for a SELECT. Every one of those layers was a "best practice" added by someone who had read the book and not the chapter on when not to. The team's velocity on that service was a third of their velocity elsewhere, and nobody could say why, because each layer looked reasonable in isolation. The cost of a pattern is paid in the aggregate, by the person debugging at 2am who has to hold all six layers in their head to understand one query.
The honest rule is the same one Alexander used: a pattern resolves competing forces. If there are no competing forces, there is no pattern to apply, only overhead to add. Before you introduce one, name the forces. "I expect three more payment providers next quarter" is a force. "The book says to" is not.
Yes, but: doesn't this just lead to spaghetti?
The strongest objection to all of this is real, so let me state it fairly. If you tell a mid-level engineer "ignore patterns, just write the simple thing," some fraction of them will write a 4,000-line God class with thirty boolean flags and a switch statement that controls the universe. Patterns exist partly as guardrails against exactly that. The discipline of "extract a Strategy when you see a growing conditional" prevents the conditional from metastasizing. There is genuine value in having a named, recognized escape hatch that a team agrees to reach for at a known threshold.
That's true, and it's why this series is going to spend time on each pattern rather than dismissing the lot. The resolution is not "patterns bad." It's that patterns are a response to pressure that already exists, not a prophylactic you apply preemptively. Write the simple version first. The if/else is fine at two branches. When it hits four or five and you find yourself copying the structure into a second place, that is the moment the Strategy starts paying for itself, and now you have a real force to point at. Patterns applied at the first sign of pressure are engineering. Patterns applied at the first sight of a keyboard are decoration. The skill is reading the pressure, and that only comes from having felt the pain of both extremes.
What to do on Monday
When you next reach for a pattern, do two things. First, say out loud what forces it resolves, in concrete terms tied to your actual roadmap or your actual pain, not to a book. If you can't name a force, you don't have a pattern, you have ceremony, and you should write the function. Second, check whether your language already has the feature the pattern is faking. If you're about to build a Strategy and your language has first-class functions, you probably just need to pass a function. If you're about to build an Iterator, your for loop already is one.
And keep the one durable insight from the whole catalog: the patterns worth their cost are the ones that draw boundaries between code you control and code you don't, or between parts of a system that should be able to change independently. Adapter, Facade, Observer, Repository earn their indirection because the seams they create are real. The rest are mostly your language apologizing for what it can't say. For the next few weeks we'll go pattern by pattern and sort which is which, starting with MVC, the pattern every framework reinvents and almost none of them implement the way the name implies.

