Two engineers who have never met sit down to review each other's code. One says, "I made the logger a Singleton, the connections come from a Factory, and the UI widgets are Observers of the data model." The other nods — they understand the whole architecture from three words, before reading a single line. That shared shorthand is the real power of design patterns: they are the vocabulary that professional software engineers use to talk about the shape of a solution.
A design pattern is a named, reusable solution to a problem that keeps coming up when you design software. It is not a finished class you copy and paste, and it is not a library you install. It is a template — a proven arrangement of objects and responsibilities — that you adapt to your own situation. Think of it like a recipe or a chess opening: the idea is general, but you cook the actual meal (write the actual code) yourself.
The idea was made famous in 1994 by four authors — Gamma, Helm, Johnson and Vlissides, forever known as the "Gang of Four" — whose book catalogued 23 patterns they had seen again and again in real object-oriented systems. They did not invent these solutions; they noticed and named them, the way a botanist names species that were always growing in the wild. Naming them turned private cleverness into a language everyone could share.
The Gang of Four grouped their patterns into three families, according to what kind of problem each one addresses:
We will now meet four canonical patterns — two creational (Singleton, Factory) and two behavioural (Observer, Strategy). For each, ask the same three questions: what problem does it solve?, what shape is the solution?, and what does it look like in code?
The problem. Some things should exist only once in a whole program: a single configuration object, one logging service, one connection pool. If different parts of the code each made their own copy, they would disagree with one another and waste resources.
The solution shape. Make the class responsible for guaranteeing its own
uniqueness. The constructor is hidden (private), and a single access point — usually
getInstance() — creates the one object the first time it is asked for, then hands back
that same object forever after.
Whenever anyone calls Logger.getInstance(), they receive the one and only
Logger. The pattern enforces "there can be only one".
The problem. Your code needs an object, but you do not want it to hard-wire
which concrete class to create. Maybe the choice depends on a setting, a file type, or the
user's platform. Scattering new WindowsButton() / new MacButton()
decisions throughout the code makes it brittle — add a new platform and you must hunt down every
spot.
The solution shape. Put the "which class do I build?" decision in one place — a factory method or function — that returns objects through a common interface. The caller asks the factory for "a button" and works with the interface; only the factory knows the concrete type.
Adding a Linux button later means editing one function, not the whole codebase. The caller
never changes, because it only ever knew about the Button interface.
The problem. When one object changes, several others need to react — but you do not want the changing object to know the exact list of who cares. A spreadsheet cell changes and a chart, a total, and a formula all need updating. Hard-coding those dependencies makes the cell impossible to reuse.
The solution shape. The changing object is a subject. Interested objects subscribe to it as observers. When the subject changes, it walks its list of subscribers and notifies each one — without knowing or caring what they do with the news. The subject depends only on a small observer interface, not on the concrete observers.
Below is the whole pattern working end to end. A WeatherStation (the subject) holds a
temperature; a phone display and a logger both subscribe. When the temperature changes, both are
notified automatically — the station never mentions either of them by name. Press Run
and read the output.
Notice the station has no idea what a PhoneDisplay or a Logger is. Add a
third observer and the station code does not change at all. This decoupling is exactly why Observer
underlies event systems, UI frameworks, and the "publish/subscribe" pattern you meet everywhere.
The problem. You have several ways to do the same job — sort by price or by rating,
pay by card or by voucher, compress with zip or gzip — and you want to choose which at
runtime, without a sprawling if/else that has to be edited every time a new option
appears.
The solution shape. Define a common interface for the interchangeable behaviours — each concrete strategy is one implementation. A context object holds a strategy and delegates the work to it. Swap the strategy object and the context behaves differently, with no change to the context's own code.
Strategy turns a chain of if branches into a set of small, testable classes you can
plug in and out. Observer and Strategy are both behavioural patterns, but note the difference:
Observer is about notification (one-to-many), while Strategy is about
substitutable behaviour (swap one algorithm for another).
In a language with first-class functions — TypeScript included — you often can collapse a
simple Strategy into "just pass a function": checkout.complete(50, cardPayFn). That is
not cheating; it is the same idea in lighter clothing. The full object-based Strategy earns its
keep when a strategy needs its own state or several related methods (not just one), or
when you want to name and register a family of them. Patterns describe intent, and the
intent — "make the algorithm interchangeable" — is identical whether you use a class or a lambda.
Suppose you are building a game with three difficulty levels, and the enemy AI should behave
differently at each. A first instinct is one giant method full of
if (difficulty === "easy") … else if …. But new difficulties, and testing each one in
isolation, become a nightmare. This is a textbook fit for Strategy.
AIStrategy interface with a method chooseMove(state).EasyAI (moves randomly), NormalAI
(blocks obvious threats), HardAI (searches ahead) — each implementing that interface.
Enemy (the context) a current AIStrategy and have it call
strategy.chooseMove(state) on its turn.
Adding a "Nightmare" mode later means writing one new class and one new line in the factory — the enemy, the menu, and every other strategy stay untouched. That is the whole promise of patterns: the system bends to change instead of breaking under it. And notice how patterns combine — a real design usually weaves several together.
The classic beginner mistake is treating patterns as goals rather than tools — learning the catalogue and then reaching for a pattern everywhere, wrapping a two-line problem in five interfaces and three factories. That is called over-engineering, and it makes code harder to read, not easier. A pattern only pays off when it solves a real, present problem; if the simple version is clear and correct, keep it.
Singleton deserves a special warning. It looks harmless — "just one shared object" — but it is essentially global mutable state dressed up in a class. It creates hidden dependencies (any method can quietly reach in and grab it), makes code hard to test (you cannot easily swap in a fake), and can cause trouble with multiple threads. Many experienced engineers now regard Singleton as an anti-pattern and prefer to pass the shared object in explicitly (this is "dependency injection"). Know the pattern — but reach for it rarely, and with your eyes open.