Two engineers debug a payment service. One says, "Just wrap the old gateway in an Adapter and inject it." The other instantly nods — no whiteboard needed. That single word carried a whole design. That is the power of patterns: compressed, shared experience.
Why do we even need patterns?
Software problems rhyme. "I need exactly one of this thing." "I have two interfaces that don't fit together." "I want to add behavior without touching the original class." Skilled engineers have solved these so many times that the solutions became folklore — passed around, refined, and eventually written down. A design pattern is that folklore, formalized: a description of a problem that recurs, together with the core of a solution you can apply countless times without ever doing it the same way twice.
The term comes from architect Christopher Alexander, who described patterns in buildings and towns. Four software engineers — later nicknamed the "Gang of Four" — borrowed the idea and catalogued 23 patterns for object-oriented software. We'll meet them in the next lesson.
Anatomy of a pattern
Every well-described pattern has four parts. Keep these in mind every time you learn a new one:
- Name — the shared word that lets you say "use an Adapter here" and be understood.
- Problem / Context — when does this pattern apply? What forces are in tension?
- Solution — the arrangement of classes and objects that resolves those forces.
- Consequences — the trade-offs. Every pattern costs something (usually indirection).
Patterns rest on one idea: program to an interface
Almost every GoF pattern leans on the same principle — depend on what something does (an interface), not how it does it (a concrete class). That single move is what lets you swap implementations without rewriting callers. Here is the idea in its smallest form:
// Depend on the ABSTRACTION, not the concrete type.
interface PaymentGateway {
void charge(int cents);
}
class StripeGateway implements PaymentGateway {
public void charge(int cents) { /* call Stripe API */ }
}
class CheckoutService {
private final PaymentGateway gateway; // <-- interface, not StripeGateway
CheckoutService(PaymentGateway gateway) { // injected from outside
this.gateway = gateway;
}
void pay(int cents) {
gateway.charge(cents); // works with ANY gateway implementation
}
}
CheckoutService never names StripeGateway. Swap in PaypalGateway or a FakeGateway for tests and nothing in CheckoutService changes. Hold onto this — when you meet Strategy, Adapter, Factory, and the rest, you will see this same move again and again.
What design patterns are not
- Not snippets to copy-paste. A pattern is a shape; you implement it fresh for your context every time.
- Not algorithms. Quicksort is an algorithm (steps to compute a result). A pattern is about structure and responsibility, not computation.
- Not a goal. "I used 5 patterns" is not an achievement. The goal is maintainable code; patterns are a means.
- Not a silver bullet. Forcing a pattern where none is needed adds indirection and hurts readability — we call that "pattern fever."
The three families, at a glance
The 23 GoF patterns split into three buckets by what kind of problem they address:
- Creational — how objects get created. e.g. Singleton, Factory, Builder.
- Structural — how objects are composed into bigger structures. e.g. Adapter, Decorator, Facade.
- Behavioral — how objects communicate and share responsibility. e.g. Strategy, Observer, Command.
When you hit a design problem, first ask "is this about creating, composing, or communicating?" — that narrows 23 patterns down to a handful instantly.
A design pattern is a named, reusable solution to a recurring design problem, described by its name, context, solution, and consequences. Patterns are not code to copy, not algorithms, and not goals in themselves — they are shared vocabulary and proven structure, almost all built on "program to an interface." They group into three families: creational, structural, and behavioral.