Design Patterns (GoF and modern) Module 1 – Why patterns, and when not to use them
What a design pattern is (and is not)
A design pattern is a named answer to a recurring design problem, with its trade-offs. How patterns differ from idioms, algorithms and architecture styles.
What you will learn
- Define a design pattern by its four parts, name, problem, solution and consequences
- Distinguish patterns from idioms, algorithms, libraries and architecture styles
- Trace the idea from Alexander's building patterns to the 1994 Gang of Four catalogue
- Navigate this track, from the 23 classic patterns to modern, resilience, library and anti-pattern lessons
What you’ll learn
What you will learn
- Define a design pattern by its four parts, name, problem, solution and consequences
- Distinguish patterns from idioms, algorithms, libraries and architecture styles
- Trace the idea from Alexander's building patterns to the 1994 Gang of Four catalogue
- Navigate this track, from the 23 classic patterns to modern, resilience, library and anti-pattern lessons
On this page
A design pattern is a named, reusable answer to a design problem that keeps coming back, written down together with the situations it fits and the price it charges. Strategy, Observer and Adapter are patterns. Each one names a problem (an algorithm that has to vary, objects that must hear about a change, an interface that does not fit), gives the shape of a solution, and states the consequences of using it. A pattern is a description, not a piece of code: you implement it again in every design, so two correct implementations of Strategy can look quite different.
What a pattern is not matters just as much. It is not an idiom of one language, such as Python’s with block; not an
algorithm, which computes a result; not a library that you call; and not an architecture style, which shapes a whole
system. This lesson takes one problem from an if/elif chain to a pattern, sorts reusable knowledge into those kinds,
follows the idea from buildings to software, and fills in the pattern card that every later lesson of this track uses.
The four parts of a pattern
The catalogue most people mean by “design patterns” is a book that Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides published with Addison-Wesley in 1994, Design Patterns: Elements of Reusable Object-Oriented Software. Its authors are known as the Gang of Four, so its 23 patterns are often called the GoF patterns. Its first chapter says that a pattern has four essential elements:
- Name. A word or two that lets people discuss a design without drawing it. Saying “Observer” in a meeting replaces a paragraph of explanation, as long as everyone means the same thing by it.
- Problem. When the pattern applies: the situation, and the forces that pull against each other in it. A typical pair of forces is “this behaviour will change often” against “the code that uses it should not have to change”.
- Solution. The elements of the design, their responsibilities and how they work together. It is a shape that you adapt, not code that you copy, which is why the same pattern looks different in Java and in Python.
- Consequences. What the pattern buys and what it costs: flexibility along one axis, paid for with extra types, indirection, memory or speed, and new ways for the code to go wrong.
The four parts of a pattern, filled in for the greeting registry
Text description of the diagram
Four boxes stacked from top to bottom, joined by arrows.
The first box is the name: Strategy, with a registry.
An arrow labelled "names" leads to the problem: behaviour is picked by a key, the user's language, and new keys keep arriving.
An arrow labelled "answered by" leads to the solution: a map from each key to a function, where every function takes the same input, the user's name.
An arrow labelled "costs and buys" leads to the consequences. Gained: a new language edits no existing function. Lost: a misspelt key falls back to the default silently instead of failing.
The consequences are the part people skip, and the part that decides whether a pattern belongs in your code. A pattern applied where its problem does not exist brings all of its costs and none of its benefits, so every pattern lesson in this track ends with the situations in which you should leave the pattern out.
One problem, two designs
An app greets each user in their own language. The first version picks the greeting with an if/elif chain, which is short and clear. The real test of a design is the next change, though, and here it is a request for Tamil.
Python · greet/greet_chain.py
"""Greetings chosen by an if/elif chain: a new language means editing greet() itself."""
def greet(locale, name):
if locale == "en":
return f"Hello, {name}"
elif locale == "hi":
return f"नमस्ते, {name}"
elif locale == "fr":
return f"Bonjour, {name}"
elif locale == "es":
return f"Hola, {name}"
elif locale == "ja":
return f"こんにちは、{name}"
else:
return f"Hello, {name}"
# "ta" (Tamil) is not in the chain, so it falls through to English.
for locale in ["en", "hi", "fr", "es", "ja", "ta"]:
print(locale, "->", greet(locale, "Asha")) Output
en -> Hello, Asha hi -> नमस्ते, Asha fr -> Bonjour, Asha es -> Hola, Asha ja -> こんにちは、Asha ta -> Hello, Asha
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 greet_chain.py
JavaScript · greet/greet_chain.mjs
// Greetings chosen by an if/else chain: a new language means editing greet() itself.
function greet(locale, name) {
if (locale === 'en') return `Hello, ${name}`;
else if (locale === 'hi') return `नमस्ते, ${name}`;
else if (locale === 'fr') return `Bonjour, ${name}`;
else if (locale === 'es') return `Hola, ${name}`;
else if (locale === 'ja') return `こんにちは、${name}`;
else return `Hello, ${name}`;
}
// "ta" (Tamil) is not in the chain, so it falls through to English.
for (const locale of ['en', 'hi', 'fr', 'es', 'ja', 'ta']) {
console.log(locale, '->', greet(locale, 'Asha'));
} Output
en -> Hello, Asha hi -> नमस्ते, Asha fr -> Bonjour, Asha es -> Hola, Asha ja -> こんにちは、Asha ta -> Hello, Asha
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node greet_chain.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
In this design a new language means editing greet() itself. The program asks for Tamil (ta) and gets English,
because the chain does not know the locale and nothing outside the function can teach it. The second design keeps the
greetings in a registry: a dictionary (a Map in JavaScript) from each locale to a function that turns a name into a
greeting. greet() looks the locale up instead of branching on it.
Python · greet/greet_registry.py
"""The same greetings as a registry: a new language joins by registering a function."""
GREETINGS = {}
def register(locale, make_greeting):
GREETINGS[locale] = make_greeting
def greet(locale, name):
make_greeting = GREETINGS.get(locale, GREETINGS["en"])
return make_greeting(name)
register("en", lambda name: f"Hello, {name}")
register("hi", lambda name: f"नमस्ते, {name}")
register("fr", lambda name: f"Bonjour, {name}")
register("es", lambda name: f"Hola, {name}")
register("ja", lambda name: f"こんにちは、{name}")
# Tamil arrives from code outside greet(), which stays exactly as it is.
register("ta", lambda name: f"வணக்கம், {name}")
for locale in ["en", "hi", "fr", "es", "ja", "ta"]:
print(locale, "->", greet(locale, "Asha"))
print("registered:", ", ".join(GREETINGS)) Output
en -> Hello, Asha hi -> नमस्ते, Asha fr -> Bonjour, Asha es -> Hola, Asha ja -> こんにちは、Asha ta -> வணக்கம், Asha registered: en, hi, fr, es, ja, ta
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 greet_registry.py
JavaScript · greet/greet_registry.mjs
// The same greetings as a registry: a new language joins by registering a function.
const greetings = new Map();
function register(locale, makeGreeting) {
greetings.set(locale, makeGreeting);
}
function greet(locale, name) {
const makeGreeting = greetings.get(locale) ?? greetings.get('en');
return makeGreeting(name);
}
register('en', (name) => `Hello, ${name}`);
register('hi', (name) => `नमस्ते, ${name}`);
register('fr', (name) => `Bonjour, ${name}`);
register('es', (name) => `Hola, ${name}`);
register('ja', (name) => `こんにちは、${name}`);
// Tamil arrives from code outside greet(), which stays exactly as it is.
register('ta', (name) => `வணக்கம், ${name}`);
for (const locale of ['en', 'hi', 'fr', 'es', 'ja', 'ta']) {
console.log(locale, '->', greet(locale, 'Asha'));
}
console.log('registered:', [...greetings.keys()].join(', ')); Output
en -> Hello, Asha hi -> नमस्ते, Asha fr -> Bonjour, Asha es -> Hola, Asha ja -> こんにちは、Asha ta -> வணக்கம், Asha registered: en, hi, fr, es, ja, ta
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node greet_registry.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Both programs print the same five greetings. The difference is where Tamil came from: one call to register(),
written after greet() and without changing it. That line could live in another file, maintained by the people who
look after the Tamil text, which is how plugin systems let others add behaviour to code they cannot edit.
Described as a pattern, the registry has all four parts:
- Name: Strategy. Each greeting function is one strategy, a member of a family of interchangeable functions with the same signature, and the registry picks one by its key.
- Problem: behaviour is chosen by a key, the set of keys keeps growing, and the code that chooses should not have to change every time a key arrives.
- Solution: a map from each key to a function with one shared signature (a name in, a greeting out), and a single lookup with a default in place of a chain of comparisons.
- Consequences: a new language edits no existing function, and each greeting can be tested on its own. In
exchange, the list of languages is no longer visible in one place, and a misspelt key such as
"jp"quietly falls back to English, where the chain would have shown the missing branch to anyone reading it. If two parts of a program register the same key, the last one wins without a warning.
None of this makes the chain wrong. For five languages that never change, the chain is shorter and just as easy to read; the registry starts to pay when languages arrive regularly or come from code you do not own. Choosing between the two by those forces is the skill this whole track teaches.
What a pattern is not
Patterns sit beside other kinds of reusable knowledge, and mixing them up leads to questions with no good answer, such as “which pattern is binary search?”.
| Criterion | Scope | Judged by |
|---|---|---|
| Design pattern | A few classes or functions | How well its consequences fit the problem's forces |
| Idiom | A construct of one language | Whether the code reads as normal to that language's users |
| Algorithm | One computation | Correctness, time and memory |
| Architecture style | A whole system | Scaling, deployment and how teams own the parts |
Idioms. An idiom is the usual way one language spells an intent. Python’s with open(path) as f: closes the file
however the block ends, through the language’s context-manager protocol, and Go’s defer f.Close(), written straight
after the file is opened, does the same job with a statement that the Go specification defines. The intent, releasing
a resource on every path out of the code, is shared; the spelling belongs to each language, and a reader who knows the
idiom recognises it at a glance.
Algorithms. An algorithm is a procedure that computes a result from an input, with a cost you can state and a correctness you can prove. Binary search finds an item among n sorted items in about log₂ n comparisons. A pattern has no single correct output and no cost of its own; it is judged by whether its consequences suit the situation.
Libraries and frameworks. A library is code you call, and a framework is code that calls yours. Both often contain
patterns: Node.js’s EventEmitter, the browser’s addEventListener and Java’s PropertyChangeSupport are all forms
of Observer. Using such a library does not mean you designed with the pattern, and applying the pattern needs no
library at all, as the twenty-line Observer later in this lesson shows.
Architecture styles. Layered systems, microservices and event-driven systems are decisions about a whole system: how it is split, how its parts communicate, how they are deployed and which team owns what. Patterns work inside one of those parts. The software architecture track covers the styles; this track stays at the level of classes, functions and modules.
Two misreadings of the word are worth unlearning early. A pattern is not a template to paste, because the code in a catalogue shows one implementation among many. And a design does not get better with the number of patterns in it: a pattern is worth its cost only where its problem actually exists.
Where patterns came from
The idea started with buildings. The architect Christopher Alexander and his co-authors published A Pattern Language (Oxford University Press, 1977), with more than 250 patterns for towns, buildings and construction. Each pattern describes a problem that recurs in the places people live and work, and the core of its solution, written so that the people who would live in a place could design it themselves. The patterns run from the scale of a town down to the details of a room, and each one points to the larger patterns it helps complete and the smaller ones that complete it, which is what makes the collection a language rather than a list.
Kent Beck and Ward Cunningham tried the idea on software. Their paper “Using Pattern Languages for Object-Oriented Programs” (Tektronix technical report CR-87-43), written for a workshop at the OOPSLA-87 conference, gave a team of application specialists five patterns for designing Smalltalk windows, among them Window Per Task, Few Panes Per Window and Short Menus. The team did not know Smalltalk’s user-interface machinery in detail, yet it wrote sensible interface specifications after a day of practice.
In 1993 a retreat in Colorado, sponsored by Kent Beck and Grady Booch, brought people together to apply Alexander’s ideas to object-oriented programming. It became the Hillside Group, which runs the PLoP (Pattern Languages of Programs) conferences, where authors improve their patterns in writing groups. In 1994 Design Patterns collected 23 object-oriented patterns in three groups: 5 creational patterns about creating objects, 7 structural patterns about composing them, and 11 behavioural patterns about how objects share work. Each entry follows the same template, with sections such as intent, motivation, applicability, structure, participants, collaborations, consequences, implementation, sample code, known uses and related patterns.
A pattern card: Observer, field by field
This track describes every pattern with one card, a shorter form of that template. Here it is, filled in for Observer.
| Field | Observer |
|---|---|
| Intent | When one object changes, every object that asked to hear about it is told, and the changed object does not need to know who they are. |
| Problem and forces | A price feed, a chart and an alert must stay in step. The feed should not import the chart or the alert, and new listeners keep arriving. |
| Solution | The subject keeps a list of subscribers behind a small interface, here any function that takes a price, and calls each one on every change. |
| Consequences | Subjects and subscribers change independently, and listeners can join while the program runs. In exchange, callers start to rely on the order of the calls by accident, one slow subscriber delays the rest, a forgotten subscription keeps its object alive, and a subscriber that changes the list during a call can make another one miss the change. |
| Known uses | Node.js EventEmitter, the DOM’s addEventListener, Java’s PropertyChangeSupport. |
| Related patterns | Mediator, where one object coordinates many; publish-subscribe through a broker, the same idea between systems. |
| When not to use it | One listener that never changes: call it directly, and the code stays easier to follow. |
The last consequence is easy to read past, so here it is happening. A one-shot subscriber removes itself while the feed is calling its subscribers:
"""Observer in a few lines, and one consequence that its pattern card warns about."""
class PriceFeed:
"""The subject: it keeps a list of subscribers and calls each one when the price changes."""
def __init__(self):
self._subscribers = []
def subscribe(self, callback):
self._subscribers.append(callback)
def unsubscribe(self, callback):
self._subscribers.remove(callback)
def publish_live_list(self, price):
# Walks the very list that a subscriber may change while it is being called.
for callback in self._subscribers:
callback(price)
def publish_snapshot(self, price):
# Walks a copy taken before the first call, so changes wait for the next price.
for callback in list(self._subscribers):
callback(price)
def run(publish):
feed = PriceFeed()
heard = []
def once(price):
heard.append(f"once:{price}")
feed.unsubscribe(once) # a one-shot subscriber removes itself
def chart(price):
heard.append(f"chart:{price}")
def alert(price):
heard.append(f"alert:{price}")
for callback in (once, chart, alert):
feed.subscribe(callback)
getattr(feed, publish)(101)
getattr(feed, publish)(102)
print(f"{publish:<18} {heard}")
run("publish_live_list")
run("publish_snapshot") Output
publish_live_list ['once:101', 'alert:101', 'chart:102', 'alert:102'] publish_snapshot ['once:101', 'chart:101', 'alert:101', 'chart:102', 'alert:102']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 observer_card.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
With the live list, chart never hears the price 101. Removing once shifted every later subscriber one place to the
left while the loop was walking the list by position, so the loop stepped over chart. Walking a copy taken before
the first call delivers 101 to all three subscribers, and once is still gone for 102. Node.js documents the same
choice for EventEmitter: every listener attached when an event is emitted is called, even if another listener
removes it during that emit. Work out what this shorter version prints before you run it:
"""What does this print? The first subscriber removes itself while the list is being walked."""
heard = []
subscribers = []
def first(price):
heard.append("first")
subscribers.remove(first)
def second(price):
heard.append("second")
def third(price):
heard.append("third")
subscribers.extend([first, second, third])
for callback in subscribers:
callback(100)
print(heard) Output
['first', 'third']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 predict_feed.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Patterns as a shared vocabulary
Much of a pattern’s value in a team is its name. In a design discussion or a low-level design interview, “a registry
of strategies” tells the listener what the code looks like and why, in four words. The useful sentence names the
variation and the consequence together: “Strategy, so a new pricing rule needs no edit to ParkingLot.” It is worth
more than reciting the class diagram, because it shows that you know what the pattern protects and what it costs.
How this track is organised
After this foundations module, four modules cover the 23 GoF patterns, Interpreter included: creational, structural, and behavioural in two parts, algorithms and requests first, then state and communication. Then come modern patterns for application code (dependency injection, Null Object and option types, value objects, repositories), patterns for resources and runtime (pools, pipelines, plugins, RAII), resilience patterns as code (retries, circuit breakers, timeouts), the patterns inside the standard libraries of six languages, anti-patterns and refactoring, and practice for design discussions and interviews. Every pattern lesson has the card above, code that runs with its recorded output, and a section on when not to use the pattern. The track page lists every lesson in order.
Practise: from a chain to a registry
Refactor the chain into a registry in Python. The sample tests check the five greetings and the fallback to English,
register Tamil from the test file, and read the source of greet() to make sure that its branches on the locale are
gone.
Exercise · Easy · Python
Turn a greeting if/elif chain into a registry
greet.py picks a greeting with an if/elif chain, so every new language means editing greet(). Refactor it into a registry, the design of this lesson:
1. register(locale, make_greeting) stores a function that takes a name and returns the greeting. 2. greet(locale, name) looks the locale up and calls the function it finds. A locale that nobody registered gets the English greeting. 3. Register the five greetings of the starter code, with exactly the same text.
Keep the two names, register and greet, because the tests import them. The tests check the five greetings and the English fallback, register Tamil ("ta", "வணக்கம், <name>") from the test file without touching your code, and read the source of greet(): it must not branch on the locale any more, so no if, conditional expression or match may be left in it.
Starter code · greet.py
def register(locale, make_greeting):
"""Store make_greeting, a function from a name to a greeting, under locale."""
raise NotImplementedError("write the registry")
def greet(locale, name):
"""The greeting for name in locale's language; English for a locale nobody registered."""
if locale == "en":
return f"Hello, {name}"
elif locale == "hi":
return f"नमस्ते, {name}"
elif locale == "fr":
return f"Bonjour, {name}"
elif locale == "es":
return f"Hola, {name}"
elif locale == "ja":
return f"こんにちは、{name}"
else:
return f"Hello, {name}" The sample tests · test_greet.py
import ast
import inspect
import greet as module
from greet import greet, register
def test_five_languages():
"""the five greetings of the starter code, with the same text"""
assert greet("en", "Asha") == "Hello, Asha"
assert greet("hi", "Asha") == "नमस्ते, Asha"
assert greet("fr", "Asha") == "Bonjour, Asha"
assert greet("es", "Asha") == "Hola, Asha"
assert greet("ja", "Asha") == "こんにちは、Asha"
def test_unknown_locale_falls_back_to_english():
"""a locale that nobody registered gets the English greeting"""
assert greet("xx", "Ravi") == "Hello, Ravi"
def test_register_from_outside():
"""a new language joins from this file, without an edit to greet.py"""
register("ta", lambda name: f"வணக்கம், {name}")
assert greet("ta", "Asha") == "வணக்கம், Asha"
assert greet("en", "Asha") == "Hello, Asha"
def test_greet_no_longer_branches_on_the_locale():
"""greet() looks the locale up: no if, conditional expression or match is left in it"""
tree = ast.parse(inspect.getsource(module.greet))
branches = [type(n).__name__ for n in ast.walk(tree) if isinstance(n, (ast.If, ast.IfExp, ast.Match))]
assert branches == [], f"greet() still branches: {branches}" A hint
Start with an empty dict at module level, GREETINGS = {}, and make register put the function into it under its locale. Then greet needs one lookup with a default: dict.get(key, default) returns the default when the key is missing, and the default here is the English function, GREETINGS["en"]. Register the five languages with lambda name: f"…{name}" after both functions are defined.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
14 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What are the four essential parts of a design pattern?
A name, a problem, a solution and its consequences. The name is the shared word, such as Observer, that lets a team discuss a design without drawing it. The problem says when the pattern applies: the situation and the forces in it, for example behaviour that keeps changing while the code that uses it should not. The solution describes the elements and how they collaborate, as a shape to adapt rather than code to paste, so it looks different in each language. The consequences say what the pattern buys and what it costs, such as flexibility paid for with extra types and indirection. When you describe a pattern you have used, cover all four and give the consequences the most weight: they show that you know when the pattern is the wrong choice.
Key takeaways
- A design pattern is a named answer to a recurring design problem, described by its name, problem, solution and consequences; you implement it again in each design.
- The consequences decide whether a pattern belongs in your code: it buys flexibility along one axis and pays for it with extra structure.
- Idioms belong to one language, algorithms compute results, libraries are code you call, and architecture styles shape whole systems; patterns work at the level of a few classes or functions.
- Patterns came from Alexander’s building patterns, reached software through Beck and Cunningham’s 1987 paper, and were catalogued as 23 object-oriented patterns in 1994.
- A pattern’s name is most useful together with its variation and consequence: “Strategy, so a new rule needs no edit to the caller.”
References
- Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley (InformIT))
- Using Pattern Languages for Object-Oriented Programs (Technical Report CR-87-43) (Kent Beck and Ward Cunningham)
- A Pattern Language: Towns, Buildings, Construction (Oxford University Press (record at Open Library))
- History of the Hillside Group (The Hillside Group)
- The with statement (The Python Language Reference) (Python Software Foundation)
- Function definitions and decorators (The Python Language Reference) (Python Software Foundation)
- The Go Programming Language Specification: Defer statements (The Go Authors)
- Events: emitter.removeListener (Node.js v24 documentation) (OpenJS Foundation)
- DOM Standard: addEventListener() (WHATWG)
- PropertyChangeSupport (Java SE 25 API) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress