Skip to content

Exercises: 09 OOP, Design Patterns and Architecture

Every exercise from every module, in one place. The full statement and the "done when" condition are in section 6 of each module; this list gives the short form, the difficulty and a hint. Build everything with:

g++ -std=c++17 -Wall -Wextra -Werror -pthread -fsanitize=address,undefined -o ex file.cpp && ./ex

Use -fsanitize=thread (instead of address) for anything with threads.

Suggested path if time is short: do every easy exercise in modules 01, 03, 06, 08, 09 and 11, then the medium ones in 09 and 11, then exercise 11.6 (timed practice) until you can finish each LLD problem in 45 minutes.

Module 01: OOP for a C programmer

# Level Exercise Hint
1.1 easy Add size to the C function-pointer store and to the virtual Store; compare failure modes A forgotten C slot is a null call at run time; a missing pure virtual override is a compile error when you instantiate.
1.2 easy Turn one of your C modules into a class with private state Find the invariant first (e.g. head < capacity), then make every mutation a member function that keeps it.
1.3 medium Shape as a hierarchy and as std::variant; compare sizeof and allocations Hierarchy: one heap block per shape plus a vptr each; variant: size of the largest alternative plus a tag, stored inline.
1.4 medium Fix the dangling pointer from find() two ways std::map nodes never move; or look up by id inside transfer after any call that can reallocate.
1.5 hard Benchmark virtual (mixed and uniform types), variant, template Uniform types make the indirect branch perfectly predicted; the remaining gap is lost inlining and pointer chasing.

Module 02: SOLID in plain words

# Level Exercise Hint
2.1 easy Add DEL without touching KvService Open/closed: register a new operation object in the table the service reads.
2.2 easy ReadOnlyStore that throws on put; fix with ReadableStore This is Liskov plus interface segregation: split the interface so the read-only type never promises put.
2.3 medium Clock interface and TtlStore decorator tested with FixedClock The decorator stores expiry per key and asks the clock on every get; the test advances the fake clock.
2.4 medium Split one real C function into parse / rule / storage on paper Write the storage interface from the rule's point of view: only the queries the rule needs.
2.5 hard Generic contract(make_store) test template Call it with lambdas returning unique_ptr<Store>; use assert with a message string via && "msg".

Module 03: Ownership and object lifetime design

# Level Exercise Hint
3.1 easy Ownership diagram of your work RPC server Every dashed arrow to something not owned by an ancestor is a potential use-after-free; list each one.
3.2 easy Swap member order of metrics_ and conns_ and predict shutdown Members are destroyed in reverse declaration order; a connection's destructor that touches metrics must die first.
3.3 medium Generation counters for BufferPool handles Store a generation per slot, bump it on release, compare in get().
3.4 medium Arena that runs destructors on reset() Keep a vector of (ptr, dtor) pairs; a captureless lambda converts to void(*)(void*).
3.5 hard Two-thread server: I/O thread owns connections, worker gets ids Workers never hold Connection*; replies carry a ConnId the I/O thread looks up and drops if gone.

Module 04: Creational patterns

# Level Exercise Hint
4.1 easy Add a "cached" kind to make_store The factory composes: construct the memory store, then wrap it.
4.2 easy Cross-field validation in ServerConfigBuilder Validate in build(), not in the setters, because the order of setter calls is free.
4.3 medium Replace a MetricsRegistry singleton with DI and show state leaking between tests Run two scenarios in one process; the singleton keeps counts from the first.
4.4 medium Health-checked ConnectionPool::acquire() Loop: pop, check alive(), discard dead ones, construct a new one if the pool runs dry.
4.5 hard Self-registering factory and the static-library trap An unreferenced object file in a .a is never pulled in; fix with --whole-archive or explicit registration calls.

Module 05: Structural patterns

# Level Exercise Hint
5.1 easy erase() through the C API adapter Map the C return code to bool; only throw for real failures, not "not found".
5.2 easy TimeoutStore decorator in two stack positions Outside the retry it times the whole retried call; inside it times each attempt.
5.3 medium Negative caching in CachingStoreProxy Cache std::nullopt with a short TTL and invalidate on put of that key.
5.4 medium Age rule as a leaf and as a composite, plus Not Composites win when rules are assembled from configuration; leaves win for clarity of one fixed rule.
5.5 hard Split pimpl into three files and observe rebuilds and the destructor error ~unique_ptr<Impl> needs the complete Impl; define the destructor in the .cpp where Impl is complete.

Module 06: Behavioural patterns

# Level Exercise Hint
6.1 easy TieredFee strategy from a threshold table Capture the vector by value in the lambda ([tiers = std::move(t)]).
6.2 easy AuditLog unsubscribes inside its own callback notify() iterates a snapshot of ids and re-checks each id still exists before calling.
6.3 medium Connection state machine as enum + switch Remove a case and compile with -Wall to see -Wswitch name the missing enumerator.
6.4 medium Add Refund to the variant and to the classic visitor Variant: every visit without a generic fallback fails to compile. Classic: only visitors missing the new visit overload fail.
6.5 hard timeout middleware with a worker thread Capture the request by value (or shared_ptr) in the worker; the caller's stack frame is gone after the timeout.
6.6 hard Bidirectional RingBuffer::iterator Store a logical position (0..size) instead of a raw index; wrap only when dereferencing.

Module 07: Concurrency patterns

# Level Exercise Hint
7.1 easy Bound of 1 vs 1000 in producer-consumer Small bound: more blocking and context switches; large bound: more memory and latency hidden in the queue.
7.2 easy Non-blocking try_push Same lock, check size() == cap, return false instead of waiting.
7.3 medium Reproduce and fix thread-pool self-deadlock Record worker thread ids at start; if the caller is a worker, run the child task inline.
7.4 medium shared_mutex config vs RCU snapshot benchmark Every shared_lock writes the mutex's reader count: one cache line bouncing between cores.
7.5 hard Supervisor actor that restarts a crashed account actor Wrap the actor loop in try; on exception send Died{name} to the supervisor with the last state.
7.6 hard Minimal epoll reactor plus thread pool and eventfd Replies go through a queue; the worker writes to the eventfd; the loop drains replies and looks up ConnId, dropping unknown ones.

Module 08: Modern C++ alternatives to classic patterns

# Level Exercise Hint
8.1 easy Third store type in vector<AnyStore> It only needs put and get members with the right signatures; no base class.
8.2 easy CountingLog policy A stateless policy (static counter) keeps the empty base optimisation, so sizeof does not change.
8.3 medium Small-buffer optimisation for AnyStore alignas(std::max_align_t) unsigned char buf[32], placement new, and a flag or function pointer to know which storage is in use.
8.4 medium Escape event and Paused state in the variant framer Without the fallback overload, the compiler lists each missing (state, event) pair as an error.
8.5 hard Virtual decorator stack vs policy store benchmark Use -O2, a volatile sink for results, and perf stat to see instructions per call; also compare size of both binaries.
8.6 hard Find the pipeline loop in assembly -O2 -S -fverbose-asm; search for the loop label; with std::function look for call *%rax or similar.

Module 09: Designing a server codebase

# Level Exercise Hint
9.1 easy Add EXISTS to m09_layered_server.cpp Touch protocol (new variant alternative), serve (new lambda) and one service function; storage already has get.
9.2 easy RequestIdLogger decorator Wrap common::Logger, hold the id, prefix each message; create one per request inside serve.
9.3 medium FlakyStorage that fails as Unavailable, never as "not found" The port must be able to say "failed": return Result<optional<string>> from get, and map Unavailable to -ERR try again.
9.4 medium CMake target graph that forbids service -> storage includes Give each target only its own src/<layer> as an include directory; link only along the allowed arrows.
9.5 hard Characterization tests, then split a legacy handler Capture outputs for at least 15 inputs first, including malformed ones; refactor only while all pass.
9.6 hard One-page architecture for the loan-origination server Use the Module 12 template; the outbox and unit-of-work decisions each need a rejected alternative.

Module 10: API design and code quality

# Level Exercise Hint
10.1 easy BranchId strong type and a swapped call that fails to compile Distinct tag types make Strong<A, int> and Strong<B, int> unrelated types.
10.2 easy Observe [[nodiscard]] turning into an error -Werror promotes -Wunused-result; restore the attribute afterwards.
10.3 medium ChaosLedger fake that fails post on demand A public bool fail_next flag in the fake is fine; tests own their doubles.
10.4 medium std::hash for Strong Specialise template <class Tag, class Rep> struct std::hash<Strong<Tag, Rep>> and hash get() inside it.
10.5 hard Asynchronous shadow route with a timeout Run the new handler via std::async or a small pool, never wait for it on the request path; compare results in the background.
10.6 hard Review a real function with the checklist Write findings as "problem, risk, suggested change"; at least one per group (design, correctness, quality).

Module 11: Low-level design interview walkthroughs

# Level Exercise Hint
11.1 easy ElectricCar needing a charger spot Model chargers as a separate size class or a per-spot flag; only the fit rule and allocator change.
11.2 easy NUMEQUALTO alias and END command in the KV interpreter Keep the store unchanged; this is protocol-layer work only.
11.3 medium Byte-based capacity in the LRU Track bytes_used_; evict in a loop until the new entry fits; reject entries larger than capacity.
11.4 medium Sliding window counter rate limiter estimate = prev_count * (1 - elapsed_fraction) + curr_count; refuse if estimate >= limit.
11.5 hard SLA escalation in the workflow engine driven by SchedulerCore Schedule an escalation job in the StartReview action; cancel it in the actions of Approve and Reject.
11.6 hard Timed practice: all seven problems in 45 minutes each Write the clarifying questions first, every time; compare your diagram with the one in the code file header.

Module 12: UML and communicating design

# Level Exercise Hint
12.1 easy ASCII class diagram of m11_parking_lot.cpp vector<Level> is composition with *; the allocator and fee policy are owned std::functions, so also composition.
12.2 easy PlantUML for the rate limiter diagram KeyedRateLimiter *-- "*" Algorithm, Algorithm <|-- TokenBucket, and so on.
12.3 medium Sequence diagram of tcpkv shutdown Signal handler and worker threads are separate lifelines; use ->> for the wake-pipe write and condition variable notify.
12.4 medium Trade-off table: where eligibility rules live Criteria: round trips, testability, consistency across callers, deploy process, scaling tier, who can change rules.
12.5 hard Two-page design doc for a master-data cache, presented in 10 minutes Record yourself; check each row of the presentation table is covered and that the decision appears in the first minute.