Design Patterns, Explained Simply · Foundations: Thinking in Patterns
What a Design Pattern Is (and Isn't)
Patterns are named, reusable answers to design problems that keep coming back. Learn what one contains, the three families, and why the name matters as much as the code.
The problem
A checkout function has grown an if/else for every payment method: card, PayPal, bank transfer, a wallet. Each new method means editing the same function, retesting all the others, and resolving merge conflicts with whoever else touched it this week.
In review, a senior engineer writes: "Make each payment method a Strategy." If you know the pattern, that one word explains the whole change. If you don't, it is jargon.
Real-world analogy
Think of cooking techniques. "Braise it" is not a recipe for one dish. It is a proven technique you apply to beef, chickpeas or leeks, and every cook who hears the word knows the steps. A design pattern is a technique like that: a proven way to arrange code that you adapt to your problem, with a name everyone shares.
The idea
The book that named the classic patterns, Design Patterns by Gamma, Helm, Johnson and Vlissides (the "Gang of Four", 1994), describes each one with four parts:
- Name: a word that lets a team talk about the design ("use an Adapter here").
- Problem: the situation where it helps.
- Solution: the roles (classes, interfaces, objects) and how they work together. It is a shape, not code to paste.
- Consequences: what you gain and what it costs.
The 23 classic patterns come in three families:
| Family | Question it answers | Examples |
|---|---|---|
| Creational | How do I create objects flexibly? | Factory Method, Builder, Singleton |
| Structural | How do I fit objects together? | Adapter, Decorator, Facade |
| Behavioral | How do objects share work and talk? | Strategy, Observer, Command |
What a pattern is not:
- Not a library. You don't install "Observer"; you arrange your own code that way.
- Not a goal. Code with no patterns can be excellent. Patterns are tools for specific problems.
- Not language-bound. Python, TypeScript, Java and PHP express the same pattern with different syntax, which is why every example in this course has four tabs.
The code
The checkout problem, before and after. The "after" version is the Strategy pattern: each payment method is a separate object with the same pay method, and checkout just looks one up. Pick your language tab; the output is identical in all four.
# Before: every new payment method means editing this function
def pay_v1(method, cents):
if method == "card":
return f"card charged {cents}"
elif method == "paypal":
return f"paypal charged {cents}"
raise ValueError(f"unknown method {method}")
# After: each method is its own object with the same shape
class Card:
def pay(self, cents):
return f"card charged {cents}"
class PayPal:
def pay(self, cents):
return f"paypal charged {cents}"
class Wallet: # a new method is a new class; checkout() never changes
def pay(self, cents):
return f"wallet charged {cents}"
METHODS = {"card": Card(), "paypal": PayPal(), "wallet": Wallet()}
def checkout(method, cents):
return METHODS[method].pay(cents)
print(pay_v1("card", 1200))
for name in ["card", "paypal", "wallet"]:
print(checkout(name, 1200))
Output
card charged 1200 card charged 1200 paypal charged 1200 wallet charged 1200
// Before: every new payment method means editing this function
function payV1(method: string, cents: number): string {
if (method === "card") return `card charged ${cents}`;
if (method === "paypal") return `paypal charged ${cents}`;
throw new Error(`unknown method ${method}`);
}
// After: each method is its own object with the same shape
interface PaymentMethod {
pay(cents: number): string;
}
class Card implements PaymentMethod {
pay(cents: number) { return `card charged ${cents}`; }
}
class PayPal implements PaymentMethod {
pay(cents: number) { return `paypal charged ${cents}`; }
}
class Wallet implements PaymentMethod { // a new method is a new class; checkout() never changes
pay(cents: number) { return `wallet charged ${cents}`; }
}
const methods: Record<string, PaymentMethod> = { card: new Card(), paypal: new PayPal(), wallet: new Wallet() };
function checkout(method: string, cents: number): string {
return methods[method].pay(cents);
}
console.log(payV1("card", 1200));
for (const name of ["card", "paypal", "wallet"]) {
console.log(checkout(name, 1200));
}
Output
card charged 1200 card charged 1200 paypal charged 1200 wallet charged 1200
import java.util.List;
import java.util.Map;
public class Main {
// Before: every new payment method means editing this method
static String payV1(String method, int cents) {
if (method.equals("card")) return "card charged " + cents;
if (method.equals("paypal")) return "paypal charged " + cents;
throw new IllegalArgumentException("unknown method " + method);
}
// After: each method is its own object with the same shape
static final Map<String, PaymentMethod> METHODS =
Map.of("card", new Card(), "paypal", new PayPal(), "wallet", new Wallet());
static String checkout(String method, int cents) {
return METHODS.get(method).pay(cents);
}
public static void main(String[] args) {
System.out.println(payV1("card", 1200));
for (String name : List.of("card", "paypal", "wallet")) {
System.out.println(checkout(name, 1200));
}
}
}
interface PaymentMethod {
String pay(int cents);
}
class Card implements PaymentMethod {
public String pay(int cents) { return "card charged " + cents; }
}
class PayPal implements PaymentMethod {
public String pay(int cents) { return "paypal charged " + cents; }
}
class Wallet implements PaymentMethod { // a new method is a new class; checkout() never changes
public String pay(int cents) { return "wallet charged " + cents; }
}
Output
card charged 1200 card charged 1200 paypal charged 1200 wallet charged 1200
Run this on your computer — see “Get Started” for setup.
<?php
// Before: every new payment method means editing this function
function payV1(string $method, int $cents): string {
if ($method === "card") return "card charged $cents";
if ($method === "paypal") return "paypal charged $cents";
throw new InvalidArgumentException("unknown method $method");
}
// After: each method is its own object with the same shape
interface PaymentMethod {
public function pay(int $cents): string;
}
class Card implements PaymentMethod {
public function pay(int $cents): string { return "card charged $cents"; }
}
class PayPal implements PaymentMethod {
public function pay(int $cents): string { return "paypal charged $cents"; }
}
class Wallet implements PaymentMethod { // a new method is a new class; checkout() never changes
public function pay(int $cents): string { return "wallet charged $cents"; }
}
function checkout(array $methods, string $method, int $cents): string {
return $methods[$method]->pay($cents);
}
$methods = ["card" => new Card(), "paypal" => new PayPal(), "wallet" => new Wallet()];
echo payV1("card", 1200), "\n";
foreach (["card", "paypal", "wallet"] as $name) {
echo checkout($methods, $name, 1200), "\n";
}
Output
card charged 1200 card charged 1200 paypal charged 1200 wallet charged 1200
Adding Wallet did not touch checkout. That is the whole point: the part that changes often (payment methods) is separated from the part that stays the same (checkout).
Common mistakes
- Pattern hunting. Starting from "where can I use a Visitor?" instead of "what problem do I have?" produces complicated code for simple problems.
- Copying the textbook diagram exactly. Patterns are shapes. In Python or TypeScript, a Strategy can be a plain function; you don't need an interface and three classes.
- Treating names as decoration. Naming the pattern in a class (
ShippingStrategy) or a comment helps the next reader. Naming it wrongly (calling a Facade an Adapter) misleads them.
Key points
- A design pattern is a named, proven solution shape for a recurring design problem: name, problem, solution, consequences.
- The classic 23 come in three families: creational, structural and behavioral.
- Patterns are not libraries or goals. Use one when you recognise its problem.
- The shared name is half the value: one word can explain a whole design in review.
- Further reading: What's a design pattern? and the Gang of Four book.
Practice questions
What are the four essential parts of a design pattern, according to the Gang of Four?
Answer
Name, problem, solution and consequences. The name gives the team a shared word, the problem says when to apply it, the solution describes the roles and how they collaborate (a shape, not finished code), and the consequences list the trade-offs.
Name the three families of classic design patterns and give one example of each.
Answer
Creational (how objects are created: Factory Method, Builder, Singleton), structural (how objects are composed: Adapter, Decorator, Facade) and behavioral (how objects divide work and communicate: Strategy, Observer, Command).
Is it a good idea to add as many design patterns to a codebase as possible? Why or why not?
Answer
No. Each pattern adds indirection (more classes or interfaces), which costs readability. Use a pattern when you actually have the problem it solves, for example code that keeps changing in one place. Simple code without patterns is often the best design.
Why does this course show the same example in four languages?
Answer
Because a pattern is a design idea, not syntax. The roles stay the same while the language features change: Python and TypeScript can often use plain functions where Java uses an interface and a class. Seeing the four side by side shows what is the pattern and what is just syntax.
Open in the app to mark lessons complete, take notes and track your progress.