Design Patterns — a tool, not an end
A design pattern is a named solution to a recurring problem of code structure. It is not a library and not an algorithm. An algorithm solves a computational task; a pattern describes relationships between classes. A pattern's value is twofold: it offers a proven structure and a shared vocabulary. Saying "Observer here" is faster than drawing a diagram.
In C++ the choice of pattern is sharper than in Java or Python because you have two mechanisms for polymorphism. The same idea — "pluggable behavior" — can be expressed through virtual functions (runtime choice, cost is indirect call and no inlining) or templates (compile-time choice, cost is code bloat and loss of ABI flexibility). So in C++ it is not enough to know a pattern's structure — you must know its cost.
The main trap is applying a pattern as a goal. An extra layer of abstraction worsens readability and performance. Start with the simplest working code and introduce a pattern only when the problem it solves actually appears. The interviewer is checking exactly this judgment — not the ability to list 23 GoF patterns.
Pattern categories
The Gang of Four catalog divides 23 patterns into three categories by the problem they solve:
- Creational — manage object creation, separating the client from concrete types: Singleton, Factory Method, Abstract Factory, Builder, Prototype.
- Structural — assemble objects and classes into larger structures: Adapter, Decorator, Facade, Flyweight, Proxy, and from C++ idioms — PIMPL.
- Behavioral — describe interaction and responsibility distribution among objects: Strategy, Observer, State, Command, Chain of Responsibility, Mediator, Visitor, Iterator, Template Method.
A category hints at intent. Proxy and Decorator are structurally similar (both wrap an object) but solve different problems: Proxy controls access to the same interface, Decorator adds new behavior. Remember the problems, not the names.
Design principles
Patterns are specific solutions; principles are general rules for evaluating them. Five SOLID principles:
- S — Single Responsibility. A class has one reason to change. But if two responsibilities always change together — they belong to one class; do not split for the sake of splitting.
- O — Open/Closed. A module is open for extension, closed for modification. The principle works at the interface boundary — refactoring the class internals does not violate it.
- L — Liskov Substitution. A derived-class object substitutes for a base-class object without breaking the contract. The classic counterexample is
Square : Rectangle: overriddensetWidthbreaks theRectanglecontract. - I — Interface Segregation. A client should not depend on methods it does not use. Fat interfaces are split into narrow role-based ones.
- D — Dependency Inversion. Modules depend on abstractions, not concrete types. But "abstraction" is a point of real variability — not every dependency needs one.
// DIP: high-level code depends on abstraction, not a concrete logger
struct ILogger {
virtual void write(std::string_view msg) = 0;
virtual ~ILogger() = default;
};
class OrderService {
ILogger& log_; // dependency inverted
public:
explicit OrderService(ILogger& log) : log_(log) {}
void place() { log_.write("order placed"); }
};
Besides SOLID — three heuristics of simplicity: DRY (Don't Repeat Yourself — eliminate duplication of the same knowledge), KISS (Keep It Simple — remove accidental complexity, not necessary) and YAGNI (You Aren't Gonna Need It — do not build for "the future").
The trap of over-application
Principles can be overextended. The most common senior-level mistake is reading DIP as "every dependency must be an interface." An interface appears with a single implementation: unnecessary virtual call, extra file, code navigation through empty abstraction. OCP did not require that — it talks about module boundaries, not abstraction where a second implementation does not exist and is not foreseen. If an interface has one inheritor and a fake for testing adds no value — inline the interface back into the class. DRY applied aggressively is harmful too: accidental similarity of two fragments is not a reason to merge them — you would couple unrelated concepts.
Composition versus inheritance
Inheritance expresses is-a and tightly couples the derived class to the base: memory layout, vtable, protected interface. Composition expresses has-a: an object holds another object and delegates work to it.
// Inheritance for code reuse — an antipattern
class Stack : public std::vector<int> { /* ... */ }; // Stack "is a" vector? No.
// Composition + delegation — Stack HAS a storage
class Stack {
std::vector<int> data_;
public:
void push(int x) { data_.push_back(x); }
void pop() { data_.pop_back(); }
int top() const { return data_.back(); }
};
"Prefer composition to inheritance" means prefer — not always. Code reuse is not a valid reason for inheritance; composition exists for that. But polymorphic hierarchies where runtime polymorphism through a base pointer is needed require inheritance by necessity. A red flag is private inheritance "to access internals" — almost always that is composition, written wrong.
Creational patterns
Singleton
Guarantees a single instance and provides a global access point. Canonical in modern C++ — Meyer's static: a function-local static variable.
class Config {
public:
static Config& instance() {
static Config inst; // initialized once, thread-safe as of C++11
return inst;
}
Config(const Config&) = delete; // prevent copying —
Config& operator=(const Config&) = delete; // otherwise a second instance appears
private:
Config() = default;
};
Mechanics: as of C++11, initialization of a function-local static is thread-safe — the compiler wraps it in "magic static" (flag + barrier), repeated initialization is prevented. Return a reference, not a pointer: a client might accidentally delete or null it. Always = delete the copy operations.
Singleton's drawbacks make it a suspicious pattern. It is global state: dependencies are hidden (not visible in the constructor signature), code is harder to test and parallelize, object destruction order becomes a problem — a singleton's destructor accessing an already-destroyed other singleton is undefined behavior. If you just need one instance per application run — pass it explicitly through the constructor (this is dependency injection), not hide it in a singleton.
Factory Method and Abstract Factory
Both separate the client from concrete created types, but differently:
- Factory Method — a virtual method that a subclass overrides to decide which object to create. Mechanism: inheritance. The method cannot be
static: statics are not dispatched virtually, and the pattern loses its meaning. - Abstract Factory — an object whose interface creates a family of related products. Mechanism: composition: the client holds a reference to the factory.
struct Button { virtual void paint() = 0; virtual ~Button() = default; };
struct GuiFactory { // Abstract Factory
virtual std::unique_ptr<Button> makeButton() = 0;
virtual ~GuiFactory() = default;
};
A plain free function makeWidget() is a factory function, not a GoF pattern: the pattern implies polymorphism. If only one product varies — Abstract Factory is overkill; Factory Method suffices.
Builder
Constructs a complex object step by step, replacing a constructor with dozens of arguments. Each setter returns Builder& — by reference, otherwise each call copies the builder. Check cross-cutting invariants in build(), not in individual setters — otherwise invalid objects leak out.
class HttpRequest { /* ... */ };
class RequestBuilder {
std::string url_, method_ = "GET";
public:
RequestBuilder& url(std::string u) { url_ = std::move(u); return *this; }
RequestBuilder& method(std::string m) { method_ = std::move(m); return *this; }
HttpRequest build() const {
if (url_.empty()) throw std::logic_error("url required"); // invariant
return HttpRequest{/* ... */};
}
};
Do not introduce Builder for a two-field struct — that is overcomplication.
Prototype
Creates an object by copying an existing one through a virtual clone(). Return unique_ptr, not a raw pointer, and override clone() in every derived class — otherwise copying causes object slicing.
struct Shape {
virtual std::unique_ptr<Shape> clone() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this); // copy of exact type
}
};
Structural patterns
Adapter and Decorator
Both wrap an object but with different goals:
- Adapter changes the interface: wraps an object so it fits an interface the client expects. Behavior is the same — the surface is different.
- Decorator changes the behavior: wraps an object in the same interface, adding functionality around it.
// Decorator: same Stream interface, added compression
struct Stream { virtual void write(std::string_view) = 0; virtual ~Stream() = default; };
class CompressingStream : public Stream {
Stream& inner_; // wrapped object
public:
explicit CompressingStream(Stream& s) : inner_(s) {}
void write(std::string_view data) override {
inner_.write(compress(data)); // added behavior, interface unchanged
}
};
A Decorator's base Component must have a virtual destructor — otherwise deletion through a base pointer skips wrapper destructors. Adapters come in two flavors: object (composition — stores a reference to the adapted object) and class (private inheritance from it); object is more flexible. You cannot publicly inherit from the adapted object — its entire interface shows, and the adapter contract breaks.
Flyweight
Saves memory by sharing immutable (intrinsic) state across objects through a pool. The classic example is font glyphs: thousands of on-screen characters reference dozens of shared glyph descriptions. Only share immutable state — shared mutable state causes races and aliasing bugs. The flyweight pool must outlive all objects that reference it.
PIMPL — a C++ idiom
PIMPL (Pointer to IMPLementation) hides a class's private members behind a pointer to an incomplete type Impl. This removes the header's dependency on implementation details: change Impl and only one .cpp recompiles, not all its users. It also provides ABI stability: the visible class's size does not change.
// widget.h — header does not know Impl's structure
class Widget {
struct Impl;
std::unique_ptr<Impl> pimpl_;
public:
Widget();
~Widget(); // DECLARED in .h, DEFINED in .cpp
};
The key trap: the destructor must be declared in the header and defined in the .cpp where Impl is complete. If you rely on the implicit destructor, the compiler generates it in the header — where Impl is incomplete — and ~unique_ptr<Impl>() hits a static_assert about an incomplete type. Copying is not free either: unique_ptr is non-copyable, so you must write a copy constructor by hand with deep-copy Impl semantics. For small value types PIMPL is not worth it — heap allocation eats the win.
Cost of wrapper stacks
Decorators and adapters stack — and a deep stack of wrappers has a cost. Each wrapper is a separate heap allocation, so the stack is scattered in memory and does not fit cache well. Calls go through a base pointer, and the optimizer usually cannot devirtualize the stack — each level is an indirect call with inlining forbidden. If behavior is fixed at compile time — do not pay for runtime indirection, express it as a template.
Behavioral patterns
Strategy
Factors out a pluggable algorithm behind an interface so it can change independently of the host. In modern C++ — two flavors:
// Runtime Strategy — algorithm choice changes during execution
class Sorter {
std::function<bool(int, int)> cmp_;
public:
explicit Sorter(std::function<bool(int, int)> c) : cmp_(std::move(c)) {}
void sort(std::vector<int>& v) { std::sort(v.begin(), v.end(), cmp_); }
};
// Compile-time Strategy — strategy is a type parameter
template <class Compare>
class StaticSorter {
Compare cmp_;
public:
void sort(std::vector<int>& v) { std::sort(v.begin(), v.end(), cmp_); }
};
Compile-time versus runtime
This is a typical senior question. Runtime Strategy (via std::function or a virtual interface) lets you swap algorithms on the fly and keep the choice behind an ABI boundary — but you pay an indirect call and lose inlining. Compile-time Strategy (type parameter) inlines and optimizes fully — but each parameter set spawns its own instantiation (code bloat), and you cannot change the choice without recompiling users. Do not believe the optimizer will inline a virtual call by itself: devirtualization requires the concrete type to be known at the call site.
Observer
Notifies multiple subscribers when the subject changes — a "one-to-many" relationship. Appears simple at first but is a source of subtle bugs in C++.
class Subject {
std::vector<std::weak_ptr<Observer>> observers_; // weak_ptr, not raw pointer
public:
void notify() {
for (auto snapshot = observers_; auto& w : snapshot) // iterate COPY
if (auto o = w.lock()) o->update();
}
};
Observer as a lifetime and concurrency trap
Naive GoF Observer stores raw pointers to observers. In real code an observer often dies before the subject — and the list holds a dangling pointer that notify() dereferences into undefined behavior. Solutions: weak_ptr or mandatory explicit unsubscribe(). The second trap: iterating a live container: if the observer's callback calls unsubscribe(), it invalidates the loop iterator; iterate a snapshot-copy. The third: concurrency: if you hold the subject's mutex for the entire notify() cycle and the callback calls a subject method back — self-deadlock. Adding and removing observers from different threads also needs synchronization of the list.
State
An object changes behavior when its internal state changes — this is a finite state machine. The key idea: state classes hold the transition logic, not the context. If you dump transitions into context, it becomes a giant switch.
Hence a common senior task: incrementally refactor a god-object with switch into the State pattern so every step is shippable and reviewable. Order: first write characterization tests (else regressions slip through silently), then extract case-s into state classes one by one, and only when all cases are extracted — move transition logic into the states. Never mix the two refactorings — each step must be checkable separately. States often carry no data — use shared instances (static flyweight) to avoid allocating per transition.
Other behavioral patterns
- Command — encapsulates a request as an object: queues, undo, logging. For
undostore enough state or rollback is incomplete. Never capture references instd::function-commands — by the time the queue runs, they are dangling. - Chain of Responsibility — a request travels down a chain of handlers until one accepts it. You need a fallback at the chain end or the request silently disappears.
- Mediator — a mediator orchestrates "many-to-many" interaction between N components, removing direct links between them. Different from Observer: Observer is one-way "one-to-many" notification; Mediator is two-way orchestration. The mediator orchestrates, not implements the components' business logic.
- Visitor — adds operations to a type hierarchy through double dispatch without changing the types themselves. Justified when operations (N) are many and types (M) are few and stable. If the type hierarchy changes more often than operations — each new type forces all visitors to update; then virtual dispatch is better.
- Iterator — provides a uniform way to traverse a container. In C++ the pattern is implemented by STL itself:
begin()/end(), iterator categories, compatibility with range-based for and algorithms. A container without iterators is incompatible withfor (auto x : c)and<algorithm>. - Template Method — sets an algorithm's skeleton in the base class, leaving steps overridable. The template method itself is non-virtual — only the steps are virtual. Do not call virtual steps from the base class constructor: the vtable still points to the base, and dispatch won't reach the derived implementation.
Dependency management
Dependency Injection (DI) means passing an object its dependencies from the outside instead of creating them inside. In its simplest form DI is a constructor parameter; no framework is needed for it, and "DI" is not the same as "a DI framework".
// Without DI — the dependency is created inside, hidden, not substitutable
class ReportServiceBad {
FileLogger log_; // ❌ hard-wired to FileLogger
};
// With DI — the dependency is injected via the constructor, visible and substitutable
class ReportService {
ILogger& log_;
public:
explicit ReportService(ILogger& log) : log_(log) {} // ✅ this is DI
};
Inject an abstraction — an interface or a template parameter — not a concrete type: otherwise the dependency can't be substituted and the point is lost. The neighbouring anti-pattern is the service locator: instead of an explicit parameter, the object reaches into a global registry for its dependency. That hides dependencies again (they're not visible in the constructor signature), whereas DI makes them explicit.
The plugin boundary
A plugin system is the extreme case of dependency management: a module is loaded dynamically at runtime. The boundary between host and plugin in C++ is fragile, and three rules keep it intact:
- Export an
extern "C"factory, not classes directly. C++ name mangling differs between compilers and even compiler versions — a class symbol built by one compiler won't be found in a binary built by another. Only the C boundary is stable: anextern "C"function returning an object through an abstract interface. - Unload plugins in reverse order of loading. If plugin B depends on plugin A and A is unloaded first, the vtables of B's objects will point to code already unmapped from memory.
- Do not pass STL containers across the boundary. The layout of
std::stringandstd::vectordepends on the compiler, runtime version, and build flags. Use C types or pointers at the boundary.
The PIMPL idiom (see "Structural patterns") is the same dependency decoupling at the translation level: the header stops depending on implementation details.
Common errors and traps
| Error | Consequence |
|---|---|
| Pattern for pattern's sake, unnecessary abstraction layer | Worse readability and performance; code more complex than the problem |
| Singleton where you just need one instance per run | Hidden dependencies, untestable; use DI instead |
Returning pointer instead of reference from instance() | Client may delete or null it — second instance or crash |
| Virtual call from constructor (Template Method) | vtable not built yet — step doesn't dispatch to derived class |
Factory method declared static | No virtual dispatch — Factory Method does not work |
PIMPL: destructor not defined in .cpp with complete Impl | static_assert about destroying unique_ptr of incomplete type |
| Observer stores raw pointers to observers | Dangling pointer in list → undefined behavior on notify() |
unsubscribe() inside callback while iterating live list | Loop iterator invalidated during notify() |
Subject's mutex held for entire notify() | Self-deadlock if callback calls subject back |
| State transition logic dumped in context | Context becomes a giant switch |
| DIP interpreted as "every dependency is an interface" | Single-implementation interfaces: unnecessary call and dead navigation |
| Inheritance for code reuse | Tight coupling to base; composition was needed |
Interview relevance
Design patterns are one of the most frequent topics at the middle and senior level. But the test is not rote memorization of GoF — it is engineering judgment: do you know which problem a pattern solves and what it costs in C++?
What the interviewer is checking:
- Recognizing the problem, not memorizing 23 pattern names
- Distinguishing similar patterns: Adapter vs Decorator, Factory Method vs Abstract Factory, Mediator vs Observer, Proxy vs Decorator
- The cost of a pattern in C++: virtual call, heap allocation, code bloat, ABI boundary
- Compile-time versus runtime embodiment (Strategy) and conscious choice between them
- Lifetime and concurrency traps — especially in Observer
- When a pattern is unnecessary: SOLID overapplication, Singleton instead of DI, abstraction with no second implementation
Typical questions:
- How does Adapter differ from Decorator? And Factory Method from Abstract Factory?
- How do you implement a thread-safe Singleton and why is Meyer's static better than a pointer with mutex?
- Why does naive Observer become a lifetime and deadlock trap?
- ABI and inlining tradeoffs of compile-time versus runtime Strategy?
- How do you incrementally refactor a god-object
switchinto the State pattern? - What traps does PIMPL have and why must the destructor be in
.cpp?
Typical mistake: answering with a pattern's textbook structure, not naming its cost and context. A strong candidate shows they know when a pattern harms — that is what distinguishes engineering judgment from reciting the GoF catalog.