The real-world analogy: prefab assembly and adapter plugs
Imagine assembling a physical office space:
- Prefab Assembly (Factory & Builder): Instead of mixing cement and forging glass on-site, you order pre-fabricated walls (Factory Method) from a supplier who shields you from the fabrication details. If you're buying a custom luxury desk, you use a modular catalog builder to select standard drawers, tabletops, and cord inserts step-by-step (Builder).
- Outlet Adapter (Adapter): You travel to the UK and find a 3-prong square outlet. Your laptop charger has a 2-prong round plug. You don't rebuild the wall or re-wire your charger. You insert a UK plug adapter—the plug stays the same, the outlet stays the same, the adapter bridges the gap.
- Gift Wrap (Decorator): You buy a book. To make it a premium gift, you wrap it in foil paper. Then you add a ribbon. Then you affix a greeting card. Each wrapping conforms to the standard shape of a box (the interface), but adds a new layer of appearance or function dynamically.
- Matryoshka Nesting Doll (Composite): You open a large wooden nesting doll. Inside are smaller dolls, and inside those are even smaller dolls. You can paint the exterior of the doll without caring if you're holding a leaf-level solid wood doll or a hollow shell containing other dolls.
Creational Patterns
Factory Method — "don't make callers know concrete class names"
Pain: code littered with new PdfExporter(), new CsvExporter() behind if (format == ...) chains — every new format edits every call site.
Python
class ExporterFactory:
_registry = {"pdf": PdfExporter, "csv": CsvExporter}
@classmethod
def create(cls, fmt: str) -> Exporter:
if fmt not in cls._registry:
raise ValueError(f"unknown format: {fmt}")
return cls._registry[fmt]() # caller never names a class
exporter = ExporterFactory.create(request.format)
exporter.export(report) # polymorphism does the rest
Java
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public interface Exporter { void export(String data); }
public class PdfExporter implements Exporter { public void export(String d) {} }
public class CsvExporter implements Exporter { public void export(String d) {} }
public class ExporterFactory {
private static final Map<String, Supplier<Exporter>> registry = new HashMap<>();
static {
registry.put("pdf", PdfExporter::new);
registry.put("csv", CsvExporter::new);
}
public static Exporter create(String format) {
Supplier<Exporter> supplier = registry.get(format);
if (supplier == null) throw new IllegalArgumentException("Unknown format: " + format);
return supplier.get();
}
}
C++
#include <string>
#include <memory>
#include <unordered_map>
#include <stdexcept>
class Exporter {
public:
virtual ~Exporter() = default;
virtual void exportData(const std::string& data) = 0;
};
class PdfExporter : public Exporter {
public:
void exportData(const std::string& d) override {}
};
class CsvExporter : public Exporter {
public:
void exportData(const std::string& d) override {}
};
class ExporterFactory {
public:
static std::unique_ptr<Exporter> create(const std::string& format) {
if (format == "pdf") return std::make_unique<PdfExporter>();
if (format == "csv") return std::make_unique<CsvExporter>();
throw std::invalid_argument("Unknown format: " + format);
}
};
Builder — "constructors with nine arguments are unreadable"
Pain: Pizza(true, false, null, 12, null, true) — what do those mean? Which are optional?
Python
In Python, native keyword arguments (Pizza(cheese=True, size=12)) make Builder mostly redundant. Pointing this out is a major interview bonus.
class Pizza:
def __init__(self, size: int, cheese: bool = False, pepperoni: bool = False):
self.size = size
self.cheese = cheese
self.pepperoni = pepperoni
pizza = Pizza(size=12, cheese=True) # named parameters
Java
HttpRequest req = HttpRequest.newBuilder() // standard Java SDK example
.uri(URI.create("https://api.example.com/orders"))
.timeout(Duration.ofSeconds(5))
.header("Authorization", token)
.POST(BodyPublishers.ofString(json))
.build(); // validate once, emit immutable object
C++
#include <string>
class Pizza {
public:
int size = 0;
bool cheese = false;
bool pepperoni = false;
};
class PizzaBuilder {
private:
Pizza pizza;
public:
PizzaBuilder& setSize(int s) { pizza.size = s; return *this; }
PizzaBuilder& addCheese() { pizza.cheese = true; return *this; }
PizzaBuilder& addPepperoni() { pizza.pepperoni = true; return *this; }
Pizza build() { return pizza; }
};
Structural Patterns
Adapter — "right behavior, wrong interface"
Pain: your code expects PaymentGateway.charge(amount); the new provider's SDK exposes submit_transaction(cents, currency, ref). You can't change their SDK, and you shouldn't change your 40 call sites.
Python
class StripeAdapter(PaymentGateway): # wraps theirs, speaks yours
def __init__(self, sdk: StripeSDK):
self._sdk = sdk
def charge(self, amount: Money) -> Receipt:
result = self._sdk.submit_transaction(
cents=amount.to_cents(), currency=amount.currency, ref=new_ref()
)
return Receipt.from_stripe(result) # translate the answer too
Java
public interface PaymentGateway { void charge(double amount); }
public class StripeSDK {
public void submitTransaction(int cents) {}
}
public class StripeAdapter implements PaymentGateway {
private final StripeSDK sdk;
public StripeAdapter(StripeSDK sdk) { this.sdk = sdk; }
@Override
public void charge(double amount) {
sdk.submitTransaction((int)(amount * 100)); // convert dollars to cents
}
}
C++
class PaymentGateway {
public:
virtual ~PaymentGateway() = default;
virtual void charge(double amount) = 0;
};
class StripeSDK {
public:
void submitTransaction(int cents) {}
};
class StripeAdapter : public PaymentGateway {
private:
StripeSDK sdk;
public:
StripeAdapter(StripeSDK s) : sdk(s) {}
void charge(double amount) override {
sdk.submitTransaction(static_cast<int>(amount * 100));
}
};
Decorator — "add behavior without touching the class"
Pain: you need retries on the payment gateway. And logging. And metrics. Subclassing gives RetryingLoggingMeteredGateway — a combinatorial explosion.
Python
class RetryingGateway(PaymentGateway): # wraps ANY gateway
def __init__(self, inner: PaymentGateway, attempts=3):
self._inner, self._attempts = inner, attempts
def charge(self, amount):
for i in range(self._attempts):
try:
return self._inner.charge(amount) # delegate
except TransientError:
backoff(i)
raise
gateway = MeteredGateway(RetryingGateway(StripeAdapter(sdk))) # stack at runtime
Java
public interface PaymentGateway { void charge(double amount); }
public class LoggingGateway implements PaymentGateway {
private final PaymentGateway inner;
public LoggingGateway(PaymentGateway inner) { this.inner = inner; }
@Override
public void charge(double amount) {
System.out.println("Starting transaction of " + amount);
inner.charge(amount);
System.out.println("Finished transaction.");
}
}
C++
#include <iostream>
#include <memory>
class PaymentGateway {
public:
virtual ~PaymentGateway() = default;
virtual void charge(double amount) = 0;
};
class LoggingGateway : public PaymentGateway {
private:
std::shared_ptr<PaymentGateway> inner;
public:
LoggingGateway(std::shared_ptr<PaymentGateway> in) : inner(in) {}
void charge(double amount) override {
std::cout << "Starting charge of " << amount << "\n";
inner->charge(amount);
std::cout << "Finished charge.\n";
}
};
Same interface in, same interface out — so decorators stack in any order, chosen at runtime.
Composite — "treat one and many uniformly"
Pain: a folder contains files and folders; your size-calculator shouldn't care which it's holding.
Python
class Node(ABC):
@abstractmethod
def size(self) -> int: ...
class File(Node):
def size(self): return self._bytes
class Folder(Node):
def __init__(self): self.children: list[Node] = []
def size(self): return sum(c.size() for c in self.children) # recursion!
Java
import java.util.ArrayList;
import java.util.List;
public interface Node { int size(); }
public class FileNode implements Node {
private final int bytes;
public FileNode(int bytes) { this.bytes = bytes; }
@Override
public int size() { return bytes; }
}
public class FolderNode implements Node {
private final List<Node> children = new ArrayList<>();
public void add(Node node) { children.add(node); }
@Override
public int size() {
return children.stream().mapToInt(Node::size).sum();
}
}
C++
#include <vector>
#include <memory>
#include <numeric>
class Node {
public:
virtual ~Node() = default;
virtual int size() const = 0;
};
class FileNode : public Node {
private:
int bytes;
public:
FileNode(int b) : bytes(b) {}
int size() const override { return bytes; }
};
class FolderNode : public Node {
private:
std::vector<std::shared_ptr<Node>> children;
public:
void add(std::shared_ptr<Node> child) { children.push_back(child); }
int size() const override {
int total = 0;
for (const auto& child : children) {
total += child->size();
}
return total;
}
};
Behavioral Patterns
Command — "turn an action into an object"
Pain: undo/redo, job queues, macros — anything where actions must be stored, not just executed immediately.
Python
class Command(ABC):
@abstractmethod
def execute(self): ...
@abstractmethod
def undo(self): ...
class AddItemCommand(Command):
def __init__(self, cart, item): self.cart, self.item = cart, item
def execute(self): self.cart.add(self.item)
def undo(self): self.cart.remove(self.item)
history.push(cmd); cmd.execute() # undo = history.pop().undo()
Java
public interface Command {
void execute();
void undo();
}
public class Cart {
public void add(String item) {}
public void remove(String item) {}
}
public class AddItemCommand implements Command {
private final Cart cart;
private final String item;
public AddItemCommand(Cart cart, String item) {
this.cart = cart;
this.item = item;
}
@Override
public void execute() { cart.add(item); }
@Override
public void undo() { cart.remove(item); }
}
C++
#include <string>
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
class Cart {
public:
void add(const std::string& item) {}
void remove(const std::string& item) {}
};
class AddItemCommand : public Command {
private:
Cart& cart;
std::string item;
public:
AddItemCommand(Cart& c, const std::string& i) : cart(c), item(i) {}
void execute() override { cart.add(item); }
void undo() override { cart.remove(item); }
};
Chain of Responsibility — "let a pipeline decide who handles it"
Pain: a request must pass auth, then rate limiting, then validation, then the handler — without one god-function knowing all steps.
Python
class Handler(ABC):
def __init__(self): self._next = None
def set_next(self, h): self._next = h; return h
def handle(self, req):
if self._next: return self._next.handle(req)
class AuthHandler(Handler):
def handle(self, req):
if not verify(req.token): raise Unauthorized()
return super().handle(req) # pass it on
Java
public abstract class Handler {
protected Handler next;
public Handler setNext(Handler next) {
this.next = next;
return next;
}
public void handle(String request) {
if (next != null) next.handle(request);
}
}
public class AuthHandler extends Handler {
@Override
public void handle(String request) {
if (request.contains("valid-token")) {
super.handle(request);
} else {
throw new RuntimeException("Unauthorized");
}
}
}
C++
#include <memory>
#include <string>
#include <stdexcept>
class Handler {
protected:
std::shared_ptr<Handler> next;
public:
virtual ~Handler() = default;
std::shared_ptr<Handler> setNext(std::shared_ptr<Handler> n) {
next = n;
return next;
}
virtual void handle(const std::string& request) {
if (next) next->handle(request);
}
};
class AuthHandler : public Handler {
public:
void handle(const std::string& request) override {
if (request.find("valid-token") != std::string::npos) {
Handler::handle(request);
} else {
throw std::runtime_error("Unauthorized");
}
}
};
Interactive Quiz
1.
2.
3.
The honest meta-lesson
Patterns are vocabulary, not virtue. The failure mode interviews screen for isn't ignorance — it's pattern fever: a Builder for a two-field object, an AbstractFactoryFactory. Every pattern adds indirection, and indirection must pay rent (the simplicity rule). Strong answers name the pain first, the pattern second, and the simpler alternative they rejected third. ("Keyword args would do, but this is Java, so: Builder.")
Practice — level up
You learn a pattern by building its shape, not memorizing its name. Each drill below is one of the second-tier patterns in disguise — spot it as you solve.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — Adapter
Expose one interface (stack) backed by another (queue) — Adapter in three methods.
Core — Composite & Command
Trees of objects; reversible operations.Treat leaves and branches uniformly — Composite plus an Iterator.
- Design Browser HistoryMedium
Reversible forward/back operations over state — the Command / Memento shape.
Stretch — recursive structure behind a clean API
A recursive Composite (trie) hidden behind add / search — structure the caller never sees.