Design Patterns
SOLID, GoF patterns, RAII, DRY/KISS/YAGNI, Singleton, Factory, Observer, Visitor, State, and PIMPL.
31 questions
JuniorCodeVery commonImplement the Singleton pattern (thread-safe, Meyer's).
Implement the Singleton pattern (thread-safe, Meyer's).
Meyer's Singleton uses a function-local static variable: C++11 guarantees magic statics are initialised exactly once in a thread-safe manner. Delete copy/move constructors and assignment to prevent extra instances.
Open full question →Common mistakes
- ✗Returning a pointer instead of a reference from
instance()— callers can accidentally delete the pointer or store nullptr; return a reference - ✗Implementing Singleton by storing a static pointer and calling
new— this is the pre-C++11 approach; it requires a mutex and is less clean than Meyer's static - ✗Not deleting copy/move operators — without
= delete, a caller can accidentally copy the singleton into a local variable, creating a second instance
Follow-up questions
- →How do you reset a Singleton in unit tests without exposing a public
reset()method? - →What is the 'dead reference problem' with Singleton and how does the Phoenix Singleton solve it?
JuniorDesignVery commonA class needs to vary one piece of behaviour — say, how it sorts or how it prices an order — and that choice should be selectable at runtime without rewriting the class. Which behavioural design pattern captures this, what is its core idea, and how is it expressed idiomatically in modern C++?
A class needs to vary one piece of behaviour — say, how it sorts or how it prices an order — and that choice should be selectable at runtime without rewriting the class. Which behavioural design pattern captures this, what is its core idea, and how is it expressed idiomatically in modern C++?
Strategy encapsulates an interchangeable algorithm behind a uniform interface so the host can swap behaviour at runtime. Classic form: virtual execute(); modern C++: std::function or templates.
Common mistakes
- ✗Using virtual call when the strategy never changes at runtime — pay for nothing
- ✗Storing a raw function pointer when you need to capture state — use
std::functionor a callable object - ✗Embedding business logic in the host instead of the strategy — defeats the pattern
Follow-up questions
- →When does
std::functioncost matter (small-object optimisation, indirect call)? - →How does a template-parameter strategy enable inlining?
MiddleTheoryVery commonAdvantages of composition over inheritance.
Advantages of composition over inheritance.
Composition ('has-a') beats inheritance ('is-a') for loose coupling, avoidance of fragile base class problems, easy mocking, and shallow hierarchies. Inheritance is correct only when LSP truly holds.
Common mistakes
- ✗Reusing code through inheritance instead of through composition — code reuse is not a valid reason for inheritance; use composition + delegation instead
- ✗Composing everything and never inheriting — some abstractions (polymorphic hierarchies) genuinely require inheritance; the principle is 'prefer', not 'always'
- ✗Using private inheritance as 'composition with access to internals' — this is usually a design smell; prefer composition with a member variable
Follow-up questions
- →How does the Mixin pattern use multiple inheritance for code reuse without the diamond problem?
- →In what scenario does private inheritance give an advantage over a plain member?
MiddleTheoryVery commonDifference between Abstract Factory and Factory Method.
Difference between Abstract Factory and Factory Method.
Factory Method: subclass overrides virtual create() for one product; uses inheritance. Abstract Factory: a factory creates a family of related products; uses composition.
Common mistakes
- ✗Calling any object-creating function a 'Factory Pattern' — the pattern implies polymorphism; a plain
makeWidget()is just a factory function, not the GoF pattern - ✗Using Abstract Factory when only one product varies — introduces unnecessary abstraction; Factory Method is simpler
- ✗Making factory methods
static— static methods can't be overridden (no virtual dispatch), defeating the purpose of Factory Method
Follow-up questions
- →How do you implement a type-safe object registry (self-registering factories) in C++?
- →What is a virtual constructor idiom in C++ and how does it relate to Factory Method?
MiddleTheoryVery commonWhat is the Observer pattern?
What is the Observer pattern?
Observer defines a one-to-many dependency: when a subject's state changes, it notifies all registered observers. Used for events, GUI callbacks, signal/slot.
Common mistakes
- ✗Holding raw observer pointers without lifetime management — an observer destroyed before unregistering leaves the subject calling a dangling pointer; use
weak_ptr - ✗Notifying observers while holding a lock on the subject — the observer's callback may call back into the subject, causing deadlock or reentrant modification
- ✗Forgetting thread safety — adding/removing observers and calling
notify()from different threads requires synchronisation on the observer list
Follow-up questions
- →How does Qt's signal/slot mechanism implement Observer without a shared base class?
- →How would you implement a thread-safe event bus using Observer in C++?
MiddleTheoryVery commonWhat is SOLID? Explain each principle.
What is SOLID? Explain each principle.
S — one reason to change. O — open for extension, closed for modification. L — subtypes usable where base is expected. I — split fat interfaces. D — depend on abstractions.
Common mistakes
- ✗Treating SOLID as absolute rules — LSP and ISP sometimes conflict with performance; document the tradeoff when you deviate deliberately
- ✗Splitting a class for SRP when the responsibilities always change together — if two concerns always evolve together, they belong in the same class
- ✗Interpreting OCP as 'never modify existing code' — the principle applies at the module/interface boundary; refactoring internals is fine
Follow-up questions
- →Show a C++ example where violating LSP causes a runtime bug.
- →How does the Dependency Inversion Principle relate to dependency injection frameworks?
JuniorTheoryCommonDescribe Singleton, Strategy, Template-Method, Decorator.
Describe Singleton, Strategy, Template-Method, Decorator.
Singleton: one instance via Meyer's static local. Strategy: interchangeable algorithm objects. Template Method: base fixes skeleton, virtual steps in subclasses. Decorator: adds behaviour.
Common mistakes
- ✗Using Singleton when you just need one instance per startup — many services don't need guaranteed global uniqueness; prefer dependency injection which is easier to test
- ✗Calling Template Method's virtual steps from a base constructor — the vtable isn't ready yet, so they don't dispatch to the subclass; call them only from ordinary methods
- ✗Chaining Decorators without thinking about object lifetime — if the inner object is destroyed while decorators hold raw references, they dangle; use shared_ptr
Follow-up questions
- →How does the NVI idiom differ from the pure virtual interface approach?
- →How do you implement a composable filter pipeline with Decorator in C++?
JuniorTheoryCommonWhat are design patterns and why are they used?
What are design patterns and why are they used?
Design patterns are named, reusable solutions to recurring design problems: templates for structuring classes and relationships, not code snippets to copy.
Common mistakes
- ✗Over-applying patterns — every problem doesn't need a pattern; unnecessary abstraction layers hurt readability and performance
- ✗Confusing patterns with algorithms — a sorting algorithm solves a specific computational problem; a pattern describes a structural relationship between classes
- ✗Treating patterns as mandatory architecture — start with the simplest working code and introduce a pattern only when the problem it solves actually appears
Follow-up questions
- →Which design patterns does the C++ Standard Library itself use?
- →What are anti-patterns? Give examples in C++.
JuniorDesignCommonThe behavioural pattern Iterator gives a uniform way to traverse a collection without exposing how it stores its elements. Explain how the C++ STL realises this idea — how containers expose traversal and how algorithms consume it — and how that differs from the classic object-oriented form of the pattern built on a class hierarchy.
The behavioural pattern Iterator gives a uniform way to traverse a collection without exposing how it stores its elements. Explain how the C++ STL realises this idea — how containers expose traversal and how algorithms consume it — and how that differs from the classic object-oriented form of the pattern built on a class hierarchy.
STL realises Iterator as a generic concept, not a class hierarchy: containers expose begin()/end() returning iterator-concept objects. Algorithms operate on iterator pairs, decoupled from container type.
Common mistakes
- ✗Implementing a container without iterators — incompatible with range-for and STL algorithms
- ✗Iterator that doesn't satisfy a category it claims (e.g. forward iterator that fails multipass)
- ✗Mixing iterators from two different container instances — UB
Follow-up questions
- →How do C++20 sentinels relax the requirement that begin/end return the same type?
- →Why does
std::list::sortexist instead of just usingstd::sort?
MiddleDesignCommonYou have a class whose behaviour you need but whose interface does not match what a client expects — for instance, a third-party logger with a different method set than your code calls. Explain what the structural pattern Adapter is and how it solves this, then contrast it with the structural pattern Decorator, focusing on what each changes about the wrapped object: its interface, its behaviour, or both.
You have a class whose behaviour you need but whose interface does not match what a client expects — for instance, a third-party logger with a different method set than your code calls. Explain what the structural pattern Adapter is and how it solves this, then contrast it with the structural pattern Decorator, focusing on what each changes about the wrapped object: its interface, its behaviour, or both.
Adapter wraps an object to fit a different interface the client expects: same behaviour, different surface. Decorator adds behaviour around the same interface.
Common mistakes
- ✗Conflating Adapter and Facade — Facade simplifies a subsystem; Adapter retargets one object
- ✗Inheriting publicly from the adaptee — exposes its full interface and breaks the adapter contract
- ✗Forgetting to forward const-correctness in adapted methods
Follow-up questions
- →When would you use object adapter vs class adapter (private inheritance)?
- →How does
std::functionact as an adapter?
MiddleDesignCommonConstructing an object that has many fields — several of them optional — through a single constructor leads to long, error-prone argument lists where the order is easy to get wrong. What is the creational pattern Builder, how does it separate the construction process from the finished object, and when should you reach for it instead of a constructor that takes many arguments?
Constructing an object that has many fields — several of them optional — through a single constructor leads to long, error-prone argument lists where the order is easy to get wrong. What is the creational pattern Builder, how does it separate the construction process from the finished object, and when should you reach for it instead of a constructor that takes many arguments?
Builder separates construction from representation: a fluent builder accumulates settings, then build() produces the target object. Use for many parameters or optional fields.
Common mistakes
- ✗Adding a builder for a 2-field struct — overengineering
- ✗Returning the builder by value from each setter — copies; return by reference (
Builder&) - ✗Forgetting to validate cross-field invariants in
build()— invalid objects leak out
Follow-up questions
- →How does Builder compare with named-parameter idiom (designated initialisers in C++20)?
- →When would you make Builder a friend of the target class?
MiddleTheoryCommonWhat are creational, structural, and behavioural patterns? Give examples.
What are creational, structural, and behavioural patterns? Give examples.
Creational control object creation (Singleton, Factory, Builder). Structural compose objects (Adapter, Decorator, Proxy). Behavioural define interactions (Observer, Strategy, Iterator).
Common mistakes
- ✗Memorising all 23 GoF patterns without understanding the problem each solves — interviews test recognition of problems, not pattern names
- ✗Confusing Factory Method with Abstract Factory — Factory Method uses inheritance (subclass overrides); Abstract Factory uses composition (object contains factory)
- ✗Applying Proxy and Decorator interchangeably — Proxy controls access to the same interface; Decorator adds new interface functionality
Follow-up questions
- →How is the Iterator pattern implemented in C++ through begin/end and range-for?
- →Where does the Command pattern appear in GUI frameworks and undo/redo systems?
MiddleDesignCommonAn editor needs to support undo/redo, queue operations to run later, and record sequences as macros — yet the code that triggers an action should stay independent of the code that performs it. What is the behavioural pattern Command, and which problems does turning a request into a first-class object let you solve that a plain function call cannot?
An editor needs to support undo/redo, queue operations to run later, and record sequences as macros — yet the code that triggers an action should stay independent of the code that performs it. What is the behavioural pattern Command, and which problems does turning a request into a first-class object let you solve that a plain function call cannot?
Command encapsulates a request as an object, decoupling sender from receiver. Each command has execute() (often undo()). Used for undo/redo, macro recording, async job queues.
Common mistakes
- ✗Forgetting to store enough state to undo — undo becomes lossy
- ✗Capturing references in
std::functioncommands — dangling when the queue runs later - ✗Mixing command and event semantics — events describe what happened, commands describe what to do
Follow-up questions
- →How would you implement a transaction with rollback using commands?
- →What's the difference between Command and Memento for undo?
MiddleDesignCommonYou want to add behaviour to an object — buffering or compression around a data stream, say — at runtime, layering several such additions in any combination, without subclassing for every combination. What is the structural pattern Decorator, and how do you implement it in modern C++ so that a decorated object stays usable everywhere the original was?
You want to add behaviour to an object — buffering or compression around a data stream, say — at runtime, layering several such additions in any combination, without subclassing for every combination. What is the structural pattern Decorator, and how do you implement it in modern C++ so that a decorated object stays usable everywhere the original was?
Decorator dynamically adds behaviour by wrapping a component with the same interface. Concrete decorators override methods around the delegated call.
Common mistakes
- ✗Forgetting a virtual destructor on
Component— leak when deleted through base pointer - ✗Stacking decorators without thinking about ownership — who deletes whom
- ✗Choosing decorator over inheritance for compile-time-fixed behaviour — extra indirection at runtime
Follow-up questions
- →How does CRTP-based static decorator differ in performance?
- →When does the Russian-doll structure of decorators become a debugging nightmare?
MiddleTheoryCommonWhat is Dependency Injection? Example.
What is Dependency Injection? Example.
DI is a technique where an object receives dependencies from outside instead of creating them. Forms: constructor (preferred), setter, interface. Benefits: testability, decoupling.
Common mistakes
- ✗Confusing DI with a DI framework — DI is a design principle; passing a reference to the constructor IS DI without any framework
- ✗Using service locator instead of DI — a service locator hides dependencies (callers reach into a global registry); DI makes them explicit in the constructor signature
- ✗Injecting concrete types — defeats the purpose; always inject via abstract interface or template parameter so the dependency can be substituted
Follow-up questions
- →How do you implement DI in C++ without virtual dispatch (template-based policy injection)?
- →What is an IoC container and what C++ libraries provide one?
MiddleTheoryCommonWhat is PIMPL? Advantages and disadvantages.
What is PIMPL? Advantages and disadvantages.
PIMPL hides private members in an Impl class in the .cpp; the header exposes only unique_ptr<Impl>. Pros: faster compilation, ABI stability. Cons: heap allocation, indirection, manual Rule of Five.
Common mistakes
- ✗Defaulting the destructor in the header instead of the
.cppwhereImplis complete —~unique_ptr<Impl>()then fires on an incompleteImpl, a static_assert/hard error - ✗Using PIMPL for small value types — the heap allocation negates performance benefits; PIMPL is for large classes with many dependencies
- ✗Forgetting to implement the copy constructor —
unique_ptris not copyable; you must deep-copyImplexplicitly if copy semantics are needed
Follow-up questions
- →How does PIMPL affect binary compatibility (ABI stability) when shipping a C++ library?
- →What is the difference between PIMPL and the Bridge pattern?
MiddleTheoryCommonWhat are the downsides of Singleton? When is it appropriate?
What are the downsides of Singleton? When is it appropriate?
Downsides: hidden global state, hard testing, tight coupling, thread-safe lazy init complexity, destruction order issues. Appropriate only with a real one-instance constraint (hardware device, log sink) and no substitution.
Common mistakes
- ✗Storing business logic state in a Singleton instead of passing it explicitly — makes the flow opaque and disallows parallelism with multiple instances
- ✗Double-checked locking with a raw pointer and a non-atomic check — classic UB before C++11; use Meyer's static or
std::call_onceinstead - ✗Not considering the destruction order when singletons depend on each other — a destructor accessing another already-destroyed singleton is UB
Follow-up questions
- →How does Meyer's Singleton guarantee thread-safe initialisation in C++11?
- →What is the Monostate pattern and how does it compare to Singleton?
MiddleTheoryCommonWhat is a state machine? Pattern State.
What is a state machine? Pattern State.
An FSM has finite states, event-driven transitions, and entry/exit actions. The State pattern models it as classes with a common interface; the context holds a pointer and swaps it on transition.
Common mistakes
- ✗Storing transition logic inside the context — the context becomes a giant switch; move each state's transition logic into the state class
- ✗Not handling invalid transitions explicitly — an event in an unexpected state should either be ignored with a log, or throw/assert; silent failure hides bugs
- ✗Allocating state objects on the heap for every transition — states are often stateless; use singleton state instances (static flyweight) to avoid allocations
Follow-up questions
- →What is a hierarchical state machine (HSM) and when do you need it?
- →How does
boost::smldiffer from a hand-written FSM in terms of runtime cost?
MiddleDesignCommonBoth the Template Method and Strategy behavioural patterns let you keep an overall algorithm fixed while varying individual steps. Explain what the Template Method pattern is, then contrast it with Strategy — in particular, how each one lets the variable parts differ and what language mechanism each relies on in C++.
Both the Template Method and Strategy behavioural patterns let you keep an overall algorithm fixed while varying individual steps. Explain what the Template Method pattern is, then contrast it with Strategy — in particular, how each one lets the variable parts differ and what language mechanism each relies on in C++.
Template Method fixes the algorithm skeleton in the base, deferring steps to virtual methods in subclasses. Strategy moves the whole algorithm out via composition. Template Method uses inheritance.
Common mistakes
- ✗Making the template method virtual — it shouldn't be; only the steps are
- ✗Calling virtual methods from the constructor of the base — they don't dispatch to the derived implementation
- ✗Using Template Method when only one subclass exists — premature abstraction
Follow-up questions
- →Why does virtual dispatch fail in a base-class constructor?
- →How can CRTP implement Template Method without runtime cost?
MiddleDesignOccasionalAn incoming request should pass through a series of processing stages — an HTTP middleware pipeline, for example — where each stage may handle it, transform it, or pass it on, and the sender shouldn't know which stage ultimately deals with it. What is the behavioural pattern Chain of Responsibility, and how would you implement it idiomatically in C++?
An incoming request should pass through a series of processing stages — an HTTP middleware pipeline, for example — where each stage may handle it, transform it, or pass it on, and the sender shouldn't know which stage ultimately deals with it. What is the behavioural pattern Chain of Responsibility, and how would you implement it idiomatically in C++?
Chain of Responsibility passes a request along a sequence of handlers; each one handles it or forwards. Used for middleware (HTTP pipelines), event filtering, logging chains.
Common mistakes
- ✗Forgetting a fallback at the end of the chain — request goes unhandled silently
- ✗Cycles in the chain — infinite forwarding
- ✗Mutating the request in a way that breaks downstream handlers' assumptions
Follow-up questions
- →How does HTTP middleware (e.g. boost.beast / cpp-httplib) realise this pattern?
- →Compare Chain of Responsibility with a switch over message types.
MiddleDesignOccasionalA program holds millions of small objects that share a lot of identical, unchanging data — think glyphs in a text layout or sprites that reuse the same texture — and the duplicated state blows up memory. What is the structural pattern Flyweight, how does it split an object's state to make this sharing safe, and where does it show up in real C++?
A program holds millions of small objects that share a lot of identical, unchanging data — think glyphs in a text layout or sprites that reuse the same texture — and the duplicated state blows up memory. What is the structural pattern Flyweight, how does it split an object's state to make this sharing safe, and where does it show up in real C++?
Flyweight reduces memory by sharing immutable intrinsic state across many objects, while extrinsic state is supplied by callers. Examples: string interning, font glyph caches, shared textures for many sprites.
Common mistakes
- ✗Sharing mutable state and getting subtle data races / aliasing bugs
- ✗Using Flyweight where the per-object overhead of
shared_ptrexceeds the saved bytes - ✗Forgetting that the flyweight pool must outlive all referencing objects
Follow-up questions
- →How would you implement string interning in a multithreaded program?
- →Compare flyweight with COW (copy-on-write) strings.
MiddleDesignOccasionalIn a dialog where many widgets must react to each other — enabling, disabling, updating one another — wiring every component to every other produces a tangle of N×N direct references that is hard to change. What is the behavioural pattern Mediator, and how does it restructure these many-to-many interactions to reduce that coupling?
In a dialog where many widgets must react to each other — enabling, disabling, updating one another — wiring every component to every other produces a tangle of N×N direct references that is hard to change. What is the behavioural pattern Mediator, and how does it restructure these many-to-many interactions to reduce that coupling?
Mediator centralises communication between many components: instead of N×N direct references, each component knows only the mediator. Used for UI dialogues, chat servers, air-traffic control.
Common mistakes
- ✗Letting the mediator hold business logic of components — it should orchestrate, not implement
- ✗Cyclic ownership: mediator owns components and components own the mediator without weak refs
- ✗Confusing Mediator with Observer — Mediator is bidirectional N-N orchestration; Observer is one-to-many notification
Follow-up questions
- →How would you avoid the mediator becoming a god-object?
- →Compare Mediator with an event bus.
MiddleDesignOccasionalSometimes you need a new object that is a copy of an existing one, but the caller holds only a base-class pointer and doesn't know the concrete type — and building from scratch would be expensive. What is the creational pattern Prototype, what problem does it address, and how is it typically realised in C++ so that copying the right concrete type works through a base-class handle?
Sometimes you need a new object that is a copy of an existing one, but the caller holds only a base-class pointer and doesn't know the concrete type — and building from scratch would be expensive. What is the creational pattern Prototype, what problem does it address, and how is it typically realised in C++ so that copying the right concrete type works through a base-class handle?
Prototype creates objects by cloning an existing instance instead of constructing from scratch — useful when configuration is expensive. Typically a virtual clone() returning std::unique_ptr<Base>.
Common mistakes
- ✗Returning a raw pointer from
clone()— caller must remember to delete; preferunique_ptr - ✗Forgetting to override
clone()in a derived class — slicing on copy - ✗Implementing clone as
make_unique<Derived>(*this)in a base — won't compile if base is abstract; needs override per type
Follow-up questions
- →How does CRTP help reduce boilerplate for
clone()? - →Why is Prototype useful when constructors are expensive (e.g. parsing config)?
MiddleCodeOccasionalWrite a cross-platform program that ensures only one instance runs.
Write a cross-platform program that ensures only one instance runs.
Two canonical approaches: on POSIX take an exclusive flock(LOCK_EX | LOCK_NB) on a PID file; on Windows create a named mutex via CreateMutex and check ERROR_ALREADY_EXISTS. Both release on exit.
Common mistakes
- ✗Checking for a PID in the file and then testing if that process is alive — has a TOCTOU race; use a file lock instead, which is atomic
- ✗Not writing the current PID to the lock file — makes debugging harder; you can't tell which instance holds the lock
- ✗Forgetting to close the fd before exec() in a fork/exec model — the lock would be released; use O_CLOEXEC or fcntl(F_SETFD, FD_CLOEXEC)
Follow-up questions
- →How do you signal the already-running instance to bring its window to front on Windows?
- →What happens to the file lock if the process is killed with SIGKILL?
MiddleTheoryOccasionalWhat is the Visitor pattern and when to use it?
What is the Visitor pattern and when to use it?
Visitor separates an algorithm from the object structure: accept(v) calls v.visit(*this) for double dispatch without dynamic_cast. Use when hierarchy is stable but operations change.
Common mistakes
- ✗Applying Visitor to a frequently-changing element hierarchy — a new element type forces updating every visitor; if elements change more than operations, prefer virtual dispatch
- ✗Forgetting to add the new element to every existing visitor — compiler won't catch a missing overload unless the Visitor base declares it as pure virtual
- ✗Using Visitor when
dynamic_castwould be clearer — Visitor is justified for N operations × M types; for 1-2 operations,dynamic_castis simpler
Follow-up questions
- →How does
std::visitwithstd::variantachieve compile-time exhaustiveness checking? - →What is double dispatch and why does C++ not support it natively?
SeniorDesignOccasionalA long-lived god-object drives all of its behaviour through one giant switch over a current-mode field — every method branches on the same enum, and adding a mode means touching code everywhere. You must restructure it so that each mode's behaviour is isolated and a new mode can be added without editing the others, while honouring these constraints: (1) the change ships in small steps — after every step the code compiles, passes its tests, and is releasable; (2) no big-bang branch that stays unmergeable for weeks; (3) existing behaviour must be preserved, proven, before any restructuring begins; (4) at no intermediate step may the system be left in a broken or untested state. Describe the migration you would perform and the order of the steps.
A long-lived god-object drives all of its behaviour through one giant switch over a current-mode field — every method branches on the same enum, and adding a mode means touching code everywhere. You must restructure it so that each mode's behaviour is isolated and a new mode can be added without editing the others, while honouring these constraints: (1) the change ships in small steps — after every step the code compiles, passes its tests, and is releasable; (2) no big-bang branch that stays unmergeable for weeks; (3) existing behaviour must be preserved, proven, before any restructuring begins; (4) at no intermediate step may the system be left in a broken or untested state. Describe the migration you would perform and the order of the steps.
Pin behaviour with characterisation tests first. Then introduce a State interface and extract one state class at a time, having the old switch delegate to it for that case while other cases stay untouched — each extraction compiles, passes tests, and ships. Once every case is a state class, replace the switch with a context pointer swap. Transitions move into the state classes last.
Common mistakes
- ✗Doing a big-bang rewrite of the whole switch, leaving the branch un-shippable and un-reviewable for weeks
- ✗Refactoring before writing characterisation tests, so a behavioural regression slips through silently
- ✗Moving transition logic into states before all cases are extracted, mixing two refactors and making each step hard to verify
Follow-up questions
- →Why must transitions move into the state classes only after every case is extracted?
- →How do characterisation tests differ from the unit tests you would write for the finished design?
SeniorDebuggingOccasionalWhy does a naive GoF Observer become a lifetime and concurrency hazard?
Why does a naive GoF Observer become a lifetime and concurrency hazard?
The subject holds raw observer pointers it does not own, so a destroyed-but-not-unsubscribed observer becomes a dangling call. Re-entrant notify() — an observer subscribing or destroying itself inside its callback — invalidates the iterator mid-loop. Fix with weak_ptr observers, a copied snapshot of the list before iterating, and deferred add/remove.
Common mistakes
- ✗Assuming the observer always outlives the subject — in real code observers are destroyed first and leave a dangling pointer in the list
- ✗Iterating the live observer container directly, so an observer that unsubscribes inside its callback invalidates the iterator
- ✗Locking the subject's mutex across the whole
notify()loop, so an observer callback that calls back into the subject self-deadlocks
Follow-up questions
- →How does a copied snapshot of the observer list interact with an observer that unsubscribes during notify()?
- →Why is
weak_ptrpreferred over an explicit unsubscribe-in-destructor contract?
SeniorDesignOccasionalDesign a plugin system for a C++ host application: plugins ship as separate shared libraries (.so/.dll), are discovered and loaded at runtime rather than linked at build time, and a host registry keeps track of the loaded ones. The hard constraint is binary compatibility — a plugin built with a different compiler or compiler version than the host must still load and work correctly, and the host must be able to create and destroy plugin objects across that boundary without relying on C++ name mangling or fragile object layouts. Describe the architecture: how the host and plugin agree on a stable contract, how a plugin is loaded and its objects created and destroyed, and what must never cross the boundary.
Design a plugin system for a C++ host application: plugins ship as separate shared libraries (.so/.dll), are discovered and loaded at runtime rather than linked at build time, and a host registry keeps track of the loaded ones. The hard constraint is binary compatibility — a plugin built with a different compiler or compiler version than the host must still load and work correctly, and the host must be able to create and destroy plugin objects across that boundary without relying on C++ name mangling or fragile object layouts. Describe the architecture: how the host and plugin agree on a stable contract, how a plugin is loaded and its objects created and destroyed, and what must never cross the boundary.
A C++ plugin system uses a stable C ABI factory (extern C create_plugin), an abstract IPlugin base, dynamic loading via dlopen/LoadLibrary, and a host registry. The IPlugin header must stay binary-stable.
Common mistakes
- ✗Exporting C++ classes directly without a C factory function — mangled names differ between compilers and even compiler versions; always use extern "C" at the boundary
- ✗Not unloading plugins in reverse order — if plugin B depends on plugin A and A is unloaded first, B's vtable points to destroyed code
- ✗Sharing STL containers across the plugin boundary —
std::string/std::vectorlayouts may differ across runtimes or compiler flags; use C types or pointers at the boundary
Follow-up questions
- →How does version negotiation work between host and plugin when the IPlugin interface evolves?
- →What is COM (Component Object Model) and how does it solve C++ ABI issues for plugins on Windows?
SeniorDesignOccasionalYou inherit a codebase where nearly every concrete class hides behind its own abstract interface that has exactly one implementation — pure-virtual headers, an extra indirection, and a name to chase for each one — justified to you as 'following SOLID'. Work out whether this is actually buying anything: explain how you would tell a genuine variation point from a speculative abstraction, decide which of these interfaces should stay and which should go, and describe the safe change for the ones that should go. Be explicit about the criterion you use to make the keep-or-remove call and about when the abstraction would be worth reintroducing later.
You inherit a codebase where nearly every concrete class hides behind its own abstract interface that has exactly one implementation — pure-virtual headers, an extra indirection, and a name to chase for each one — justified to you as 'following SOLID'. Work out whether this is actually buying anything: explain how you would tell a genuine variation point from a speculative abstraction, decide which of these interfaces should stay and which should go, and describe the safe change for the ones that should go. Be explicit about the criterion you use to make the keep-or-remove call and about when the abstraction would be worth reintroducing later.
A one-impl interface is speculative abstraction: it adds a header, a virtual call, and a name to navigate, but buys no flexibility — DIP and OCP are meant for genuine variation points, not every class. Diagnose by counting implementers and real test doubles. Collapse by inlining the interface into its sole impl and depending on the concrete type; reintroduce the abstraction only when a second implementation actually appears.
Common mistakes
- ✗Treating DIP as 'every dependency must be an interface' rather than 'depend on abstractions at genuine variation points'
- ✗Keeping a one-impl interface 'for testing' when the concrete class is already trivially testable or a fake adds no value
- ✗Calling the inline-the-interface refactor a violation of OCP, when OCP never demanded an abstraction that has no second implementation
Follow-up questions
- →What metric best distinguishes a speculative abstraction from a real variation point?
- →How do you keep a class testable after collapsing its single-impl interface?
SeniorTheoryOccasionalWhat are the ABI, binary-size, and inlining trade-offs of compile-time vs runtime Strategy?
What are the ABI, binary-size, and inlining trade-offs of compile-time vs runtime Strategy?
Compile-time Strategy — CRTP or std::variant dispatch — lets the optimiser inline the algorithm and devirtualise, but each strategy is a distinct type, so it bloats binary size via template instantiation and bakes the choice into the ABI. Runtime Strategy — a virtual interface — keeps one stable type and a small binary, allows swapping behaviour and plugins across an ABI boundary, but pays an indirect call the optimiser usually cannot inline.
Common mistakes
- ✗Believing the optimiser inlines through a virtual call by default — devirtualisation needs a known concrete type
- ✗Ignoring template-instantiation bloat — every strategy type multiplies the code that uses it
- ✗Choosing compile-time Strategy across an ABI boundary, where strategy choice must be swappable without recompiling callers
Follow-up questions
- →When does
std::variant-based dispatch beat both CRTP and a virtual interface? - →How does template-instantiation bloat interact with instruction-cache pressure on a hot path?
SeniorPerformanceOccasionalWhen does a deep Decorator or Adapter wrapper stack hurt performance and debuggability?
When does a deep Decorator or Adapter wrapper stack hurt performance and debuggability?
Each runtime wrapper adds a virtual call, a cache-unfriendly pointer hop, and a non-inlinable boundary, so a 6-deep stack turns one logical call into six indirect ones on a hot path. Debugging suffers: stack traces explode with near-identical frames and ownership becomes unclear. Collapse the stack with CRTP/static composition, or flatten the layers when behaviour is fixed at compile time.
Common mistakes
- ✗Assuming the optimiser will devirtualise a runtime decorator chain — through a base pointer it usually cannot
- ✗Ignoring cache effects — each wrapper is a separate heap allocation, so the chain scatters across memory
- ✗Adding wrappers for behaviour that never varies at runtime, paying indirection cost for a compile-time-fixed decision
Follow-up questions
- →How does a CRTP-based static decorator eliminate the per-layer virtual call?
- →How would you profile to confirm a wrapper stack, not the payload, is the bottleneck?