Low-level design
DP-01OOP Pillars and Composition over Inheritance
Four pillars as tools. Encapsulation protects an object's inside and enforces its rules. Abstraction chooses what the outside sees: the contract. Inheritance is the pillar that is easiest to overuse.
Favour composition. Give every combination of notification channels its own subclass and the classes explode. Make channels parts behind one interface instead: a new channel is one class.
Wallet.withdraw| Entity | Value object | |
|---|---|---|
| Same means | Same id | Same value |
| Changes | Status changes, still the same | Frozen record of one fact |
- A setter for every field.
privatealone is not encapsulation; the operations must enforce the rules. - A subclass for every combination of channels, so the count grows as 2^n − 1.
DP-02UML Class Diagrams for Interviews
A floor plan, not a photograph. A class diagram shows which rooms exist and which doors connect them, and leaves the furniture out. Each of the six relationships is pinned to the line of code that creates it.
Say the multiplicities. 1, 0..1, * and 1..* cover most diagrams. Say them aloud as you draw. A 1..* becomes a check where the object is built, such as a Book constructor that refuses an empty list.
| Composition | Aggregation | |
|---|---|---|
| Who creates the part | The whole | Created elsewhere |
| Part outlives whole | No | Yes |
| Example | Copy in a Book | Member in a Library |
- Drawing every field and method, so nobody can read the diagram.
- Pointing
LoanatBookinstead ofBookCopy; the member returns one physical object.
DP-03SOLID, Part 1: Single Responsibility, Open/Closed and Liskov
Single responsibility. A responsibility is a reason to change, tied to a group who ask for it. Finance, design and operations all edited one invoice class. Split it into a tax rule, a printer and a mailer.
Open/closed and Liskov. A varying rule becomes an interface, and a new coupon is a new class. A subclass must keep its parent's promises: a view-only account breaks withdraw, so it is not an Account.
apply_coupon for the fifth couponwithdraw works up to the balance| If-ladder | Interface per rule | |
|---|---|---|
| New coupon | Reopen the function | New class |
| Wiring | None | Registry or constructor edit |
- Reading "one responsibility" as "one method" or "one thing".
- A subclass that throws "not supported" from an inherited method.
DP-04SOLID, Part 2: Interface Segregation, Dependency Inversion and Injection
Interface segregation. Give each client only the methods it calls. The first gateway interface had 4 methods and the checkout called 1, so a test fake could only throw from the other 3.
Dependency inversion and injection. Point code at an abstraction you own, and pass the parts in through the constructor. Then a test can make it 11:30 pm and charge no real card.
| DIP | DI | Service locator | |
|---|---|---|---|
| Is | Design rule | Technique | Lookup |
| About | Arrow direction | How a part arrives | Class fetches its own part |
- A service locator: the dependency is hidden from the signature, and tests must fill and reset a shared registry.
- Confusing IoC (who calls whom at run time) with DIP (which way arrows point).
DP-05Coupling, Cohesion, DRY, KISS and YAGNI
Coupling and cohesion. Coupling is between modules: how much one must know about another. Cohesion is inside one: how much its parts belong together. Aim for low coupling and high cohesion.
DRY is about knowledge. The SMS greeting lived in 4 places and drifted. Give the wording one home. Two functions that look alike but change for different reasons should stay apart.
| DRY | YAGNI | |
|---|---|---|
| Acts on | Repeated knowledge | Guessed future needs |
| Risk if ignored | Copies drift | Unused framework |
- Believing DRY means no repeated lines.
- Building an abstraction on the first guess instead of the second or third real case.
DP-06Singleton, and Why It's Often an Anti-Pattern
One instance and a global way to reach it. Sometimes you need one; the global reach is the problem. It hides a dependency and shares state. Create one Config at start-up and pass it in.
Safe lazy creation. If you must have one, use the language's safe lazy form: a function-local static in C++, the holder idiom in Java, a module-level object in Python or JavaScript.
Config sees| Singleton | Registry (Multiton) | Container scope | |
|---|---|---|---|
| Instances | One, global | One per key | One per container |
| Test can build its own | No | Yes | Yes |
- "We only need one, so Singleton": needing one object isn't a reason to enforce and expose it globally.
- Lazy creation with no synchronisation, or double-checked locking without
volatilein Java. - A Singleton holding per-request state.
DP-07Factory Method and Abstract Factory
Create without naming the class. Three different things share the word "factory". A registry of constructors keyed by a config name is often all you need: a new channel is one class and one line.
Method vs Abstract Factory. Factory Method is one overridable method in a base class that creates one product. Abstract Factory is an object that returns a family of products that match, like payment, tax and invoice per country.
ConfirmationService edited to add a push channel| Factory | Builder | Prototype | |
|---|---|---|---|
| Makes an object | Finished, in one call | One object over many calls | By copying a template |
- Calling a static
create()Factory Method; nothing is overridden. - Abstract Factory with one product, or products that never need to match.
- A registry keyed by raw strings checked only on first use; check every configured name at start-up.
DP-08The Builder Pattern
Build in steps, check once. A builder assembles one object over many calls, validates every cross-field rule in build(), and hands back something immutable. Copy its lists so the result can't change.
Often you don't need it. Python and JavaScript can do the same job with keyword or options arguments. Reach for Builder when there are no named arguments, the object has many optional parts, or it can't exist half-built.
build()| Builder | JavaBeans setters | Keyword arguments | |
|---|---|---|---|
| Half-built object | No | Yes | No |
| Cross-field rule | In build() | No place to run | In constructor |
- A builder for a class with two required fields.
- Cross-field rules in the setters, where they can't see the other values.
- Sharing the builder's list with the product, so reuse mutates a finished object.
DP-09Prototype: Copying Objects Safely
Copy to create. Make a new object by copying an existing one, usually from a registry of templates. The copy is a few lines; what matters is what it still shares with the original.
Shallow vs deep. A shallow copy duplicates the object but shares nested objects; a deep copy duplicates everything it owns. Copy what the new object owns, share what it only uses. Immutable parts can just be shared.
| Prototype | Memento | |
|---|---|---|
| Copy kept for | Creating more objects | Restoring later |
| Example | Calendar event template | Text editor snapshot |
- Calling
copy.copy,clone()or spread a "deep copy"; they are shallow. deepcopyon an object that holds a service or a lock.- A C++ raw pointer member with the default copy: two objects share it.
DP-10The Adapter Pattern
Make a class fit an interface. Wrap a class you can't change so it implements the interface your code expects. Translate names, units and errors in that one class; a new vendor, or a test fake, is one new class.
Object adapter by default. It holds the adaptee, so it can wrap an instance it was given and works in every language. A class adapter inherits the adaptee and welds you to one concrete type.
| Adapter | Facade | Decorator | |
|---|---|---|---|
| Interface | Fits an existing one | New, simpler | Kept the same |
| Wraps | One class | Several classes | One object |
- A target that mirrors the vendor, so the checkout still depends on it.
- Leaking the vendor's exceptions instead of translating them.
- Business rules inside the adapter, so changing vendor changes your pricing.
DP-11The Decorator Pattern
Wrap to add behaviour. A decorator wraps an object with the same interface, so wrappers stack and each add-on is a class, not a subclass per combination. Order matters: the outermost wrapper sees the call first and the result last.
Skin, not guts. A decorator changes an object's skin and adds to whichever rule it wraps. A strategy changes its guts and replaces the rule. A decorator that changes the interface is really an Adapter.
| Decorator | Proxy | Strategy | |
|---|---|---|---|
| Purpose | Adds responsibility | Controls access | Replaces the rule |
| Who builds it | Client stacks | Often owns subject | Chosen once |
- Stacking without thinking about order, then being surprised that logging sees three attempts.
- Checking the concrete type of a decorated object, or using it as a key.
- Forgetting
functools.wraps, so every wrapped function is namedwrapper.
DP-12The Facade Pattern
One entry point. A facade gives a subsystem one simpler call, so callers say what they want and one class owns the sequence and the undo. Here place_order sits over 4 subsystems.
Simplifies, doesn't hide. The subsystem stays reachable and the facade keeps no rules of its own. The undo runs in reverse order: the refund first, then the stock.
place_order call over stock, payments, slots, notifications| Facade | Adapter | Mediator | |
|---|---|---|---|
| Interface | New, simpler | Fits existing | Two-way hub |
| Subsystem knows it | No | No | Yes, holds a reference |
- Saying a facade hides the subsystem.
- A subsystem rule in the facade, so two classes enforce it and one goes stale.
- A facade that grows prices, tax and promotions: a god class.
DP-13The Composite Pattern
Leaf and group alike. A client treats a leaf and a group through one interface; a group answers by asking its children, so the tree can be any depth. No if over a node's kind.
Where does add live. Transparent puts add on every node and a leaf refuses it at run time. Safe puts it only on the group, so the mistake is a compile-time error.
MenuNode, with 2 classes behind itif statements over a node's kind| Composite | Decorator | |
|---|---|---|
| Children | Many | One |
| Effect | Combines into one answer | Changes one child's answer |
- Branching on the kind in the client: the if-ladder the pattern removes.
- No cycle guard; the first bug report is a stack overflow.
- Sharing one node under two parents, so
price()counts it twice.
DP-14The Proxy Pattern
A stand-in with the same interface. A proxy controls access to the real object: create it lazily, check permissions, cache answers or cross a network. The client can't tell it apart, so it must keep the real contract.
Four kinds in the book. Virtual, protection, remote and smart reference. A cache needs a rule for when it is wrong.
invalidate| Proxy | Decorator | Adapter | |
|---|---|---|---|
| Interface | Same | Same | Changed |
| Reason | Control access | Add behaviour | Translate |
- Calling every wrapper a proxy; retries or a surcharge make it a decorator.
- A cache with no invalidation or expiry.
- Expecting identity to survive:
proxy == realis false.
DP-15Bridge and Flyweight
Bridge: split two axes. Split what is said from how it goes out, so M kinds and N channels need M + N classes, not M × N. Use it up front, and only when both sides keep growing.
Flyweight: share the intrinsic. Share one immutable object per kind through a factory, and keep what differs per seat outside it. It pays only at large counts.
| Bridge | Strategy | |
|---|---|---|
| Hierarchies | Two, both growing | One swappable interface |
| Decided | Up front | Per behaviour |
- Calling any delegation a Bridge; one class holding one swappable interface is Strategy.
- A mutable flyweight, or extrinsic state inside one such as the sold flag.
- Claiming savings without measuring.
DP-16The Strategy Pattern
One way per class. When one job can be done several ways, put each in a small class behind one interface. The code that needs it holds a strategy without knowing which. The choice happens once, in a registry.
Say the reason. "The fee rule is what the business changes most, so Checkout takes a FeePolicy and a new rule is a new class." Naming what varies and where the choice went beats naming the pattern.
fee(order)Checkout edited to add a fourth rule| Strategy | State | Template Method | |
|---|---|---|---|
| Varies by | Composition | Object switches itself | Inheritance |
| Chosen | From outside | By events | By subclass |
- A strategy for every
if, including branches that will never change. - The context checking what it holds:
isinstance(self.policy, MemberFee). - Floats for money; fees are whole rupees or paise.
DP-17The Observer Pattern
The subject tells its listeners. Order keeps a list of listeners and calls on_status(event) on each, so a fourth recipient is one new listener and Order is never edited.
Name what can go wrong. Notify after releasing the lock, and isolate a failing listener from the rest. Loop over a copy of the list so subscribing mid-round is defined.
Order edited to add a fourth listener| Observer | Chain | Mediator | |
|---|---|---|---|
| Who hears | Every listener | Handlers until one stops | Coordinated peers |
| Direction | One way | One way | Two way |
- No way to unsubscribe, so a closed screen is still called.
- Notifying while holding a lock, so a slow listener blocks others.
- One exception stops the rest.
DP-18The Command Pattern
A request as an object. Each edit knows how to execute and undo; the editor keeps an undo stack and a redo stack. A new kind of edit is a new class and the editor never changes.
Capture undo data in execute. Delete stores the text it removed, because what it removes isn't known until it runs. A macro undoes its steps in reverse order.
| Command | Memento | |
|---|---|---|
| Stores | The change | A snapshot of state |
| Use | Undo, queue, replay | Restore |
- Capturing undo data in the constructor.
- Forgetting to clear redo when a new command runs.
- Recording a command that failed; push it only after
executereturns.
DP-19The State Pattern
A state machine in one place. An order's behaviour depends on its status. Model states, events and a table of legal transitions, and refuse everything else in one place. Only the context commits a transition.
Table first, classes when needed. Use an enum with a transition table while states differ by little. Move to a class per state when states grow behaviour of their own. Both designs printed identical output.
| State | Strategy | Boolean flags | |
|---|---|---|---|
| Who switches | The object itself | Chosen from outside | Anyone |
| Bad combinations | Impossible | n/a | Delivered and cancelled |
- Boolean flags instead of a status.
- Silently ignoring an illegal event; refuse it with the state and event in the message.
- No lock around check-then-transition, so two threads both pass the check.
DP-20The Template Method Pattern
Order written once. A base class owns the skeleton, export_statement: fetch, format, deliver. Subclasses override only the varying steps. A new format is a small class.
Steps and hooks. Abstract steps must be overridden (extension, header, row, footer); a hook (before_row) is optional. If the format must change at runtime, hand it in as a strategy instead.
| Template Method | Strategy | |
|---|---|---|
| Varies | Part of an algorithm | The entire algorithm |
| By | Inheritance | Delegation |
- Overriding the template method itself, skipping whatever the base guarantees, such as sending the file.
- Making a real step a hook, so a format silently forgets
row. - Subclasses calling each other's steps; share a helper in the base class.
DP-21The Chain of Responsibility Pattern
Answer or pass on. Each handler either answers or passes the request on, and any may stop it. The order is decided where the chain is built, and a missing answer fails closed.
Different chains per customer. A new check is a new class, and trusted customers get a chain without the fraud check by editing 0 existing handlers.
| Chain | Decorator | Observer | |
|---|---|---|---|
| Can stop it | Yes | No, always forwards | No |
| Who hears | Until one answers | One wrapped object | Every listener |
- No end of the chain: falling off the end must be a decision, never a silent approval.
- A handler that forgets to pass on.
- Order by accident; decide it, comment it, test it.
DP-22The Iterator Pattern
Loop without knowing storage. The screen loops over the order history and knows nothing about pages or offsets. The iterator holds the position and fetches a page only when its buffer runs out.
Remember a cursor, not an offset. Keep the last id seen, so a new order arriving mid-scroll doesn't repeat a row. Pull lazily and stop when you have enough.
| Iterator | Visitor | Observer | |
|---|---|---|---|
| Who acts | You, in the loop | Operation brought to each element | Source calls listeners |
| Style | Pull | By element kind | Push |
- Exposing the internal list.
- A position by number in a changing collection; use a cursor.
- Looping twice over an iterator; the second loop silently does nothing.
DP-23Mediator and Memento
Mediator: one room. Cart, roster and pay desk affect each other, so give them one room. Each part tells the room what happened, the room applies the rules, and no part holds a reference to another.
Memento: sealed snapshot. The draft makes a sealed snapshot and autosave only keeps it. Copy on the way in and on the way out, and say how many you keep.
| Mediator | Observer | Facade | |
|---|---|---|---|
| Direction | Two way | One way | One way |
| Parts know it | Yes | Subject only | No |
- A mediator that every feature edits; it has become the application.
- A snapshot that shares state with the live object.
- Counting a refused event; a refusal must leave state and counts as they were.
DP-24The Visitor Pattern
Operations brought to the elements. Items only have accept; each new operation is a visitor class that never touches them. Two calls reach the right code, accept then visit_gadget: that is double dispatch.
Name the price. Item kinds must be stable and operations growing. A new kind of item edits every visitor. Say which list grows before you pick it.
| Visitor | Iterator | Strategy | |
|---|---|---|---|
| Code picked by | Element's kind | You, in the loop | Chosen from outside |
| Operation | One, many pieces | None | One, many ways |
- A visitor over kinds that keep changing.
- Skipping
acceptand usingisinstance: the ladder again. - A visitor that forgets to walk into a bundle, so children drop out of the total.
DP-25Concurrency Patterns: Producer–Consumer, Thread Pool, Read–Write Lock
Bounded queue, fixed pool. Orders arrive faster than we can price them, so put a bounded queue between them. Producers wait when it is full, so load pushes back instead of exhausting memory. A fixed pool drains it.
Read-write lock. A price list is read constantly and written rarely, so readers run together while a writer waits for all of them. Prefer the writer so it can't starve.
| Bounded queue + pool | Observer | Async and await | |
|---|---|---|---|
| Runs on | Worker threads | Caller's thread | One thread |
| Decouples when | Yes | No | Switches at await |
- An unbounded queue; say the limit and what happens at it.
- Checking a condition with
ifinstead ofwhile; a woken thread must check again. - Compare-and-set on a value that can change and change back; use a version number.
DP-26Anti-Patterns and Refactoring to Patterns
Pin, then refactor. Pin 4 orders first: 397, 350, 776 and 40 rupees. Change structure only, in small steps, and check the same totals after the last. Never change behaviour and structure in one step.
Diagnose before the cure. The checkout had 4 smells: a god class, if-ladders, strings standing in for types, and two singletons read from a method. Split along its four reasons to change: price, coupon, payment, notification.
| With a pin | No pin | |
|---|---|---|
| Behaviour changed? | Can say | Nobody can say |
| Red result means | One thing | Either |
- A pattern for every
if: four files for a rule that is always 5 percent. - Singletons "for convenience": tests pass alone and fail together.
- Calling it done when the ladder only moved.
DP-27Choosing Patterns in an LLD Interview
Ask, don't announce. Five questions for any prompt: what varies, what stacks on top, what has a lifecycle, who listens, what's undoable. A question that answers no gets a sentence, not a class.
Say the choice with its reason. "The discount rule is what marketing changes, so it's a strategy; the cap applies to any rule, so it's a wrapper; support reacts to every use, so the engine publishes events."
| Pair | Test | Answer |
|---|---|---|
| Strategy / Decorator | Replace, or add to any rule | Replace: Strategy |
| Observer / Command | Tell, or a request to queue | Request: Command |
| State / Strategy | Itself, or from outside | Itself: State |
- Opening with the list of patterns, before hearing the prompt.
- Naming the pattern without the reason, and never saying the cost.
- Putting the check in a listener; observers hear after the fact.
DP-28The Pattern Map: 30 Case Studies, One Table
One table of 30 rounds. 30 case studies against 18 pattern lessons, from one table. Use the map before the interview, not in it: read the rounds that cover your weakest patterns.
Reason from the prompt. "This prompt has a rule that varies, so I'll start with a strategy; I'm not adding a state machine unless the objects have a lifecycle." The sentence has to come from the prompt, not the table.
| Common | Rare | |
|---|---|---|
| Examples | Strategy, State | Visitor, Mediator, Template Method |
| Risk | Added without need | Skipped, then asked in a follow-up |
- Treating the table as a checklist: adding a Strategy because 24 of 30 have one.
- Reading rounds in an order that repeats the same pattern three times.
- Quoting the map as evidence; "this requirement varies" is the reason.
LLD-01Design a Parking Lot
Separate what changes from what doesn't. Spot choice and fee calculation sit behind SpotAssignmentStrategy and PricingStrategy, injected through the ParkingLot constructor. A new pricing rule or vehicle type is a new class, not an edited one.
Write the fit rule once. Ask whether a larger spot can take a smaller vehicle, then write that rule once in fits(). Vehicles get no subclasses; a subclass per vehicle with no behaviour leads to an isinstance ladder.
Two gates, one spot. Both gates call find_spot(), both see F0-M0 free, and two tickets get one spot. Make find and occupy one atomic step under one lock. Pricing touches no shared state, so it runs outside the lock.
| Scan for a spot | Min-heap per size | |
|---|---|---|
find_spot | Scan | Heap pop |
On leave() | Nothing | Push spot back |
- Coding before agreeing the scope.
- A
ParkingLotgod class with the pricing chain, spot search and printing all insidepark(). - Floats for money and
datetime.now()buried in the class, so the demo can't show a 3-hour bill.
LLD-02Design an LRU Cache
Hash map plus doubly linked list. A hash map alone can't move to front or remove the oldest in O(1), so pair it with a doubly linked list: removing a node needs its predecessor. Sentinels remove the edge cases.
Eviction behind an interface. BoundedCache holds an EvictionPolicy (LRUPolicy, FIFOPolicy) instead of subclassing a map, so LFU tomorrow is a new policy. A put on an existing key updates in place and evicts nothing.
get needs the lock too. "get is a read" is wrong: it moves a node to the front with four pointer writes, and two threads can corrupt the list. Call on_evict after releasing the lock.
| By count | By bytes | |
|---|---|---|
| Evict | One entry | In a loop until the new entry fits |
| Extra | Nothing | Running total, size on put |
- A list and
list.remove(key)to move to front: O(n). - Evicting on update, so
putof an existing key at capacity throws out an innocent key. - LRU hard-coded into the cache, so "make the policy pluggable" means a rewrite.
LLD-03Design a Rate Limiter
One interface, three algorithms. Token bucket, fixed window and sliding log sit behind RateLimiter, built from a config string through a factory map. try_acquire(cost) returns a Decision with remaining and retry_after, not a bare bool.
Inject the clock. A monotonic, injected Clock means tests use FakeClock instead of sleeping, and run in microseconds. Key limiters by (client, endpoint); choose the rule by (tier, endpoint).
Two races, name both. Inside a limiter, two threads that both read "1 token left" both get through; each limiter locks its read-compare-write. In the registry, two first requests create two buckets; fix it with double-checked creation.
| Token bucket | Strict window | |
|---|---|---|
| Bursts | Allowed | No |
| Used for | Search | OTP |
- One class with an
algorithmstring and anif/elifper method. - A background refill thread per bucket.
- A global lock around every check, so one hot client slows all others.
LLD-04Design an Elevator System
LOOK per car, dispatch in the controller. Each car keeps going while stops lie ahead, then reverses at the last one. A FIFO queue of floors zig-zags. Hall calls go to the controller; car calls go straight to that car.
Dispatch behind a strategy. DispatchStrategy picks the car; LookCostDispatch can be swapped for zoned or express rules. A car deciding it's the best with no view of the others is the bug.
One controller lock. One lock guards every change to a car's stops and every step(), so a call can't land mid-decision. Notify listeners after releasing it. An idle car given a stop must take a direction at once.
Direction + doors_open | State pattern | |
|---|---|---|
| Fits | Few states | States multiply |
| Example | Today's car | Fire alarm |
- Dispatch logic inside
Elevator. - Ignoring a hall call's direction, so a car going up stops for someone going down.
time.sleep(1)between floors instead of a simulation clock.
LLD-05Design a Movie Ticket Booking System
Hold, then book. Seats are held all-or-nothing for 10 minutes while the user pays, behind a SeatLockProvider. Re-check the hold at payment time; a late payment is refunded, not booked. Confirm is idempotent.
Don't charge inside the lock. Charging inside the lock makes a slow gateway block every other user of the show. Take the lock for the check and hold, release it, then pay.
Sort the seat ids. Two users wanting A2+A3 and A3+A2 who lock in their own order can deadlock, each holding one seat. hold() sorts seat ids. The payment-versus-expiry race is the one interviewers wait for.
| One mutex | Per-seat locks | |
|---|---|---|
| Fits | This design | Scale |
| Rule | Check all, take all | Acquire in sorted seat order |
- Check-then-book with no hold, or a lock held for the whole payment.
- Locking seats one by one and leaving the first locked when the second fails.
- No idempotency on confirm, so a retried callback creates a second booking.
LLD-06Design a Bill-Splitting App (Splitwise)
Strategy for split types. SplitStrategy with EqualSplit, ExactSplit and PercentSplit; no split_type string and if/elif ladder. Money is exact to the paisa and balances always sum to zero. Shares by 2:1:1 is a new class.
Simplify debts. Reduce pairwise debts to at most n − 1 transfers. Greedy is at most n − 1, not always the minimum. Record a settlement only when the payment is confirmed, not when a UPI link is tapped.
One lock per group. Every write takes the group's lock and only applies a delta, so concurrent expenses serialise per group while different groups never wait. In a database, insert, splits and balance change go in one transaction.
| Hand-updated balances | Derived from expenses | |
|---|---|---|
| Drift | Yes | No |
| Writes | Read-modify-write | One transaction |
- Floats for money.
- Rounding each share and forgetting the remainder, so ₹100 becomes ₹99.99.
- Storing balances and updating them by hand from several places.
LLD-07Design a Vending Machine
State, not ladders. Four states (idle, has money, dispensing, sold out) answer four events (insert, select, cancel, item dropped). 0 if state == branches. Every state needs an exit for every event, even if the exit is "refuse".
Change from a limited box. Greedy fails: ₹6 from {5, 2, 2, 2}. Check change before dispensing, and refuse with "exact amount only" rather than short-change silently. Hold inserted coins in escrow so cancel returns the same coins.
One lock serialises every event. The coin acceptor, keypad, drop sensor and remote refill all fire independently. One lock stops a refill changing a slot count mid-sale, and a coin landing between "change computed" and "change paid out".
if state == branches in the machine| In the state | In the context | |
|---|---|---|
| Balance | Lost on switch | Kept |
| State objects | Hold data | Shareable |
- One
VendingMachinewithif state == IDLE ...in every method. - States that hold data, so switching state loses money.
- Dispensing before checking change.
LLD-08Design a Logging Framework (Pub-Sub)
Three patterns, three jobs. Records pass a Chain of Responsibility (level filter, PII redaction), are published to sinks by topic and level (Observer), and formatted by a pluggable formatter (Strategy). Every link must do real work.
Async so a slow disk never blocks. AsyncSink queues records on a bounded queue and returns at once; one worker writes. When full it drops and counts rather than blocking. One sink's exception stops no other.
Copy-on-write subscribers. subscribe builds a new list and swaps the reference, so a thread mid-publish iterates the old list safely and the hot path takes no lock. Stateful sinks take their own lock around the write.
Block put() | Drop and count | |
|---|---|---|
| Slow disk | Stuck service | Lost log line |
- One
Loggerclass that formats, filters and writes to a hard-coded file. - Redacting only the message and leaking the card number in the context fields.
- No isolation between sinks.
LLD-09Design Snake and Ladder
One class, swappable rules. Snake and ladder are one jump type, not two classes. Dice (RandomDice, SeededDice, ScriptedDice) and EndRule are strategies, so tests control every roll and a game can be replayed.
Validate the board first. Reject a broken board before anyone rolls: for example a snake on 100 or a jump to square 0 or 101. Keep positions on the player only, so board and player never disagree after a snake.
Double tap online. Double-tap: both requests say "Asha", so she'd get two turns. One lock per game makes check-turn, roll, move and pass one step; the second tap finds it is no longer her turn.
| First to 100 | Rank everyone | |
|---|---|---|
| Game ends | Winner | One player left |
| Result | winner | ranking[0] |
- A
while Truegame loop with no turn check, so a double tap moves a player twice. - Calling the random generator inside the game.
- Voiding a three-sixes turn without moving the token back to where the turn began.
LLD-10Design a Chess Game
Pieces know geometry, the game knows legality. 6 piece classes know how they move; 1 game decides what's legal by asking whether a move leaves my king in check. Make-and-test handles pins and en passant exposing the king.
Undo restores everything. undo() restores the piece, the capture, the castling rights and the en passant square. Inheritance is used on purpose here for families: Piece, Slider, Stepper.
legal_moves() isn't a read. It plays and takes back every candidate on the shared board, so a reader would see a knight where none moved. Every public method takes the game's lock, an RLock since move() calls legal_moves().
undo() method| Checkmate | Stalemate | |
|---|---|---|
| No legal moves | Yes | Yes |
| In check | Yes | No, a draw |
is_valid_moveon each piece, blind to pins, checks, castling history and en passant.- Checking for check only when the king moves.
- A piece that stores its own position as well as the board.
LLD-11Design an ATM
State with guards, and a journal. 5 states, each button works in only some of them. Every failure after the bank says yes (a jam, a lost reply) puts the customer's money back, using a transaction journal and idempotent reversals.
Pick notes from what is loaded. The dispenser picks notes from what is actually loaded. Greedy fails: ₹600 from ₹500 and ₹200 notes. 3 wrong PINs and the card is kept, counted per card, not per machine.
Bank checks and debits atomically. Two cards on a joint account both read ₹10,000 and take ₹6,000, so the bank checks and debits in one step. In one ATM, never hold the lock across the bank call.
| Walk away | Reverse | |
|---|---|---|
| Debit happened | Customer loses money | Money put back |
| Reversal | None | Idempotent |
- Dispensing before the debit, or debiting with no plan to reverse when the notes jam.
- Checking the balance at the ATM, then debiting, so two cards on one account both succeed.
- Holding a lock across the bank call.
LLD-12Design a Library Management System
Title and copy are different. 1 title, many copies: Book and BookCopy are different classes. A returned reserved copy goes to the hold shelf for the next member in line.
Fines computed from dates. A FinePolicy strategy (PerDayFine): 14 days a loan, then ₹10 a day, capped at ₹300. Compute at return from the dates, so a missed or repeated daily job can't double-charge.
Three races. Two members borrow the last copy: find and mark ON_LOAN must be one step. A return races a walk-in: shelf the copy in the same step. A hold may expire at the desk.
| One library lock | Lock per title | |
|---|---|---|
| Fits | Start | Scale |
| Different titles | Wait | Never contend |
- One
Bookclass with anis_borrowedflag. - Returning a reserved title straight to the shelf, so a walk-in takes it from the member who waited.
- Search by scanning every title with
in: "art" matches "Earth".
LLD-13Design a Hotel Reservation System
Inventory per night, half-open stays. Count rooms per room type per night, not specific room numbers, which fragment the calendar. Stays are [check-in, check-out), so one guest arrives the day another leaves.
Stack pricing rules per night. 3 rules stack per night: weekend, occupancy, long stay, in a PricingRule list. Price at the hold, not at payment, so the guest pays what was shown. Holds last 15 min and expire lazily.
Overselling is check-then-act. Two guests both see one deluxe room free. Make check free, quote, take one step under a per-type lock. Every operation first sweeps that type's expired holds under the same lock, so no background thread is needed.
| Hotel-wide lock | Per room type | |
|---|---|---|
| Suite vs standard | Wait for nothing | Independent |
| Counters touched | All | Own type's |
- Comparing stays with
<=, so a guest arriving the day another leaves is refused. - Holds that never expire, so abandoned checkouts quietly sell out the weekend.
- Prices recomputed at payment time.
LLD-14Design a Ride-Sharing Service
Index by location. Search radius 3 km over a grid of 1 km cells; never scan every driver. MatchingStrategy and FareStrategy are swappable. nearby() is only a hint, so try_reserve makes the assignment atomic.
Strict trip states, locked surge. 5 trip states, and every other transition is refused. Surge is locked in at request time (2.0x cap), not computed at drop-off, so the rider pays a price they saw.
Three shared things, three locks. The index lock is held briefly. Each driver's lock is taken only by try_reserve and release. Each trip's lock sits in move(): Cancel and Start in the same millisecond, one gets InvalidTransition.
| Global lock | Index, driver, trip | |
|---|---|---|
| Bookings in the city | Wait in line | Only same-driver contend |
- Search, then assign with no atomic check on the driver, so two riders get the same car.
- A status string with no transition rules, so a cancelled trip can be completed and billed.
- Fare logic inside
Trip.
LLD-15Design a Food Delivery App
Idempotent charge, price snapshot. Checkout snapshots prices and charges with an idempotency key, so a double tap on "Place order" charges ₹580 once. Moves each order through a strict state table (OrderStatus, ALLOWED).
Refund by stage, exactly once. 100% before the kitchen accepts, 50% while it cooks, 0% once packed. Compute the refund and move to CANCELLED in one critical section, so 50 concurrent cancels give 1 refund.
Decide under the lock, call outside. One service lock guards transitions and the partner pool. Decide and change state under it; call the outside world after. Checkout prices under the lock, releases, charges, then retakes it to create the order.
| Charge inside lock | Charge outside | |
|---|---|---|
| Slow gateway | Blocks every order | Blocks no one |
| Double tap | Risky | Same key charges once |
- A status field with no transition rules, so a cancelled order can be marked ready.
- Reading prices at delivery time, so a menu change alters what the customer pays.
- Refunding before checking the state, so concurrent cancels refund twice.
LLD-16Design a Meeting Room Scheduler
Sorted per-room intervals. Intervals are [start, end), so 10:00–11:00 and 11:00–12:00 share a room. A sorted per-room structure needs 2 binary searches to find every overlapping booking. Choose the smallest room that fits with SmallestFit.
Series all-or-nothing. Book 4 weekly stand-ups together or not at all: check all four weeks in one room's lock before inserting any. Don't store local times; recurring meetings go wrong at daylight-saving changes.
One lock per room. Two requests both see Everest free at 10:00 and both insert. Make check and insert atomic, per room. With no room named, book() locks each candidate in turn, never two at once: no deadlock.
| Global lock | Per room | |
|---|---|---|
| Phone booth vs boardroom | Wait | Independent |
| Series | n/a | One lock for all weeks |
- An overlap test with
<=, so back-to-back meetings clash. - Booking a series week by week, leaving three weeks booked when the fourth fails.
- Sending notifications inside the lock.
LLD-17Design an In-Memory File System
One node type. 2 node types, 1 interface (Composite): a directory's size is the sum of its children's. One normaliser and one walker resolve paths, so /home/asha/./notes/../readme.md becomes /home/asha/readme.md.
Guard moves and quotas. A move needs a cycle check, or a directory detaches from the tree. The 10,000-byte quota is checked before any byte is written, inside the lock. mkdir -p must not fail on an existing directory.
One lock is the right first answer. Per-directory locks need a global order for mv, and a walk would need hand-over-hand locking. In memory every operation takes microseconds, so one re-entrant lock is right; a readers-writer lock is next.
| One re-entrant lock | Per-directory | |
|---|---|---|
mv | Simple | Take locks in a global order |
du | Consistent | Needs a snapshot |
isinstancechecks in every operation instead of a shared node interface.- A move with no cycle check.
- Per-directory locks taken in whatever order a request names them, so two crossing moves deadlock.
LLD-18Design a Text Editor with Undo and Redo
Every edit is a command. Each edit is a command that undoes itself; the editor keeps undo and redo stacks with 100 steps of history. A delete stores the deleted text. Edit through a command, never directly in type().
Group keystrokes into undo steps. "Hello world" typed in one go is 2 undo steps, one per word; 3 backspaces undo as 1. Undo also restores the cursor. Clear the redo stack on a new edit.
Lock around the one UI thread. Edits happen on one UI thread; autosave and spell-check read around it. A re-entrant lock in every public method shows readers text before or after an edit. save() an immutable Snapshot; write outside.
| Commands | Whole-document snapshots | |
|---|---|---|
| Stores | The change | Every keystroke's copy |
| Cost | 100 characters | 5,014,950 characters |
- Snapshots of the whole document on every keystroke, with no idea what they cost.
- Forgetting to clear the redo stack on a new edit.
- Undo that restores text but leaves the cursor wherever it was.
LLD-19Design an Online Auction
Validate and append under one lock. Each bid must beat the last by a minimum step. If validation and append are separate, 20 equal bids all pass. Under the lock one wins; 19 re-read the leader and are rejected.
Soft close, 5 states. A late bid moves the deadline to 2 minutes after it. 5 states: scheduled, open, sold, unsold, cancelled. Don't trust the timer alone: reject a bid past the deadline even before the timer runs.
Bid versus deadline. A bid at 59:59.9 and the closing timer at 60:00.0 both take the auction lock, so one goes first. If the bid wins, the soft close pushes the end out.
| Owner per auction | Conditional write | |
|---|---|---|
| Lock | Stays local | None |
| How | Partition by auction_id | UPDATE … WHERE price = :seen_price, retry on zero rows |
- Checking the amount, then appending, in two steps.
- One global lock for every auction, so a hot item slows the whole site.
- No idempotency key, so a mobile retry turns one bid into two.
LLD-20Design a Stock Exchange Order Book
Price-time priority, one matcher. Limit orders rest at their price; market orders fill what they can and never rest. Trades happen at the resting order's price. Cancel is O(1); O(log P) only when an order opens a new price level.
A command queue, no lock in the book. Gateway threads validate, give an id and queue a command; 1 matcher thread per symbol runs them one at a time. The queue decides who was first; the book has 0 locks.
Symbols run in parallel, one is sequential. Two symbols never interact, so they run in parallel. One symbol is sequential, which price-time priority needs anyway. A market order into an empty book is cancelled at once.
| Reduce quantity | New price or bigger quantity | |
|---|---|---|
| Is | Same order | Cancel plus new |
| Queue place | Keeps time priority | Back of the queue |
- Floats for prices, so 101.1 + 0.1 isn't 101.2 and levels split in two.
- Trading at the incoming order's price, not the resting order's.
- A lock and many matcher threads, so event order depends on the scheduler.
LLD-21Design a Task Scheduler with Dependencies
Reject cycles up front. A Kahn's-algorithm cycle check, O(V + E), runs when a batch is submitted, so a cyclic batch never sits in PENDING forever. A task runs only when its unmet dependency count hits 0.
Retry with backoff, cancel downstream. 3 attempts for the flaky test, waiting 0.05 s then 0.10 s. One cancel skips everything downstream. Idle workers wait on a condition with a timeout, not a sleep loop.
Releasing children is a race. Two workers finish package's two dependencies at once. Without the lock both read unmet as 2 and write 1, and package never starts. Every state change is under _cv; fn runs outside it.
| In process | Many machines | |
|---|---|---|
| Ready tasks | Heaps | Pulled with a lease |
unmet | In memory | In the database |
- Running
fnwhile holding the scheduler lock, so four workers run one task at a time. - Retrying immediately, in a tight loop, against a dependency that's down.
- No cycle check, so
wait()never returns.
LLD-22Design a Key-Value Store with TTL and Transactions
Two kinds of expiry. Lazy on read, plus active in bounded sweeps. Lazy alone never frees keys nobody reads again; a full scan holds the lock for the whole scan. Eviction is a strategy: LRUEviction or NoEviction.
Transactions as a layer. BEGIN adds a layer of changes, not an O(n) copy. Writes stay invisible until the outermost commit. ROLLBACK undoes only the innermost of 3 nested transactions.
Reads take the lock too. A read may delete an expired key and tells the eviction policy the key was used: in such a store, get writes. Commit checks every version read, or raises Conflict and changes nothing.
| Layered commit | MVCC | |
|---|---|---|
| Reads see | Versions checked at commit | Newest version at or below start number |
| Cost | Conflict, client retries | Short version chain per key |
- Copying the whole store on
BEGIN. - Writing straight to the store inside a transaction, so other sessions see uncommitted data.
- No version check at commit, so two read-modify-write transactions silently lose an update.
LLD-23Design Tic-Tac-Toe (N×N, K in a row)
Win detection without scanning. A board of any size, N and K. Each move touches at most four lines, so there are 0 full-board scans. Counters work for K = N; a short walk handles any K.
Undo restores everything. 1 undo() restores the board, the turn, the status and the checker's counters. A human, a script and a bot are all a Player, so 0 if ladders care who plays.
One reentrant lock in Game. It guards play, undo and would_win, so a move and an undo can't interleave and a spectator can't see a bot's lookahead mid-way. Reentrant because the bot's choose runs inside step().
| Counters | Walk | |
|---|---|---|
| Works for | K = N | Any K |
| Per move | Update lines | Short walk |
- Hard-coding 3×3 with eight explicit lines.
- Counters for K < N: a row counter reaching K means nothing when the row has N cells.
- Advancing the turn before validating the move.
LLD-24Design a Car Rental System
Peak concurrency, not a count. Overlap is tested by peak concurrency over the range, not by counting overlapping bookings, which turns away customers a car was free for. Book a car class, not a specific car, at reservation.
Pricing as a chain, in integer cents. 4 pricing rules in a chain, each adding auditable lines to a quote. Integer cents, rounded half up, in one helper. At return: late fee, damage fee, and whether the car rejoins.
Check-then-act on the last car. Two customers reserve the last economy car at once; availability check and insert are one step, so RentalService holds one lock around reserve, cancel, pickup and return_car. Pricing is pure and needs no lock.
| Service-wide lock | Per (branch, class) | |
|---|---|---|
| Fits | Until profiling says otherwise | The next step |
- Booking a specific car at reservation time, so a late return breaks a later booking.
- One
calculate_pricewith nested ifs; the customer can't see why. datetime.now()inside the service, so tests can't repeat.
LLD-25Design a Digital Wallet
Two entries, sum zero. Every transfer is 2 ledger entries that sum to 0, in integer minor units. A refund is a new transaction linked by id; never edit or delete an entry. A hold reduces availability, not the balance.
Idempotent under retry. 1 idempotency key per request: send it twice, move the money once. Check the key inside the lock. LimitPolicy (a daily limit now) is a strategy.
Lock in account-id order. A→B and B→A: locking src then dst gives each thread one lock and a wait for the other: a cycle. Take both in account-id order through with_locks. Never lock while calling anything slow.
| One global lock | Per account | |
|---|---|---|
| Correct | Yes | Yes |
| Throughput | One thread | Parallel |
a.balance -= x; b.balance += xwith no ledger: a crash between the lines loses money.- A key checked outside the lock, so a retry can post twice.
- Locking
srcthendst: two people paying each other deadlock the system.
LLD-26Design an Inventory and Order Management System
on_hand and reserved. 2 numbers per SKU per warehouse: on_hand and reserved. The invariant reserved <= on_hand is checked and changed under one lock. A single quantity can't be held, expired or partly shipped.
Allocation and partial shipping. AllocationStrategy (FewestShipments, NearestFirst) spreads an order across warehouses. Ship what's on the shelf, not what was promised, or on_hand goes negative. Reserve all lines or none.
Two locks, one order. Every read-modify-write on a counter is under Inventory's lock. OrderService has its own lock and takes the inventory lock inside, always in that order, so no deadlock. Re-entrant because reserve calls expire.
| Process lock | Database | |
|---|---|---|
| Check | Under Inventory lock | WHERE on_hand - reserved >= :qty |
| Expiry | Sweep | Job |
- A single
quantityfield andquantity -= 1at checkout. - Reserving line by line, leaving earlier lines held when a later one fails.
- A background timer thread for expiry that can't be tested; pass a time parameter.
LLD-27Design a Notification Service
One interface, many channels. 3 channels (email, SMS, push) behind 1 Channel interface. One dead provider must not silence the rest. A FailoverChannel wrapping two providers looks like one channel to the service.
Rules before every attempt. 2 rules in a chain, run before every attempt: opt-out and quiet hours. 3 attempts at most, 30 s then 60 s apart. Schedule the next attempt; never sleep in the retry.
Lock the heap and dedupe map. Many threads call notify; others call tick. The lock stops two ticks popping the same delivery and a duplicate slipping between check and record. Provider calls and listeners run after release.
Channel interface| One heap by (due, id) | Heap by (due, priority, id) | |
|---|---|---|
| Promo flood | Delays codes | Separate or ordered |
- One
send_allthat stops at the first exception. - Retrying every failure, including a revoked token or invalid number.
- Dedupe inside the retry path, so a retried delivery is dropped.
LLD-28Design an Airline Booking System
Two limits, not one. Tickets sold (a number) and seats assigned (labels) are different. A seat map alone can't express overbooking. OverbookingPolicy (NoOverbooking, PercentOverbooking) beats if seats < capacity + 5.
Policies and a state table. RefundPolicy tiers refunds by fare class and time to departure. 6 booking states and one table of legal moves. A hold keeps seats and capacity for 15 minutes.
One step for check and write. Two customers want the last seat; both pass if check and write are separate. One lock makes expire, the capacity check, seat checks and writes one step. The second group gets SeatTaken.
OverbookingPolicy and RefundPolicy| Per service | Per flight | |
|---|---|---|
| At airline scale | Too coarse | The unit |
| In a database | Conditional update, unique (flight, seat) |
- Overbooking as an
ifinsidehold, not a policy. - Splitting a group at the gate, or booking some passengers and failing the rest.
- Refund rules as an
ifper fare name.
LLD-29Design a Restaurant Management System
One ticket per station. 1 order becomes 1 ticket per station, with its own display queue; the order is ready only when every ticket is. Derive order status from tickets rather than storing it by hand.
Exact splits, whole orders. A sold-out dish rejects the whole order and no stock is spent. $40.12 split three ways is 13.38 + 13.37 + 13.37, always adding to the bill. A walk-in is just a booking that starts now.
Three small locks. The Menu lock makes "check every dish, then take them" one step; two waiters ordering the last burger: one succeeds, the other gets SoldOut. The Kitchen lock covers queues, ticket states and completion. Listeners run after release.
EqualSplit | ByItemSplit | |
|---|---|---|
| Shares by | Equal parts | Items, or weights |
| Always adds to bill | Yes | Yes |
- One queue for the whole kitchen: the fryer waits behind the oven's twenty-minute pizza.
- A stored
Order.statusupdated by hand, which disagrees with the tickets. - Separate code for a walk-in and a reservation, so they double-book a table.
LLD-30Design a Traffic Signal Controller
A validated plan, not hard-coded. Timings are configuration that is validated: the default plan is 30 green, 4 yellow, 2 all-red, then 20, 4 and 2. A phase with two conflicting greens refuses to exist. Pass time in; never sleep.
Safe failure. A fault leaves every approach flashing red; a stuck green isn't safe. An emergency override never jumps to green: the yellow and all-red still run in full. A second monitor checks the plan again.
One lock, events after release. Sensors, buttons and the clock call in on different threads. Every public method takes the controller's single lock, runs to completion, collects events, and calls listeners only after releasing it.
| Fixed plan | PlanSelector | |
|---|---|---|
| Plan | One | Picked when a cycle starts |
| Rush hour | Code change | Another selector |
sleep()between lights: the demo takes a minute and can't be tested.- The emergency override jumps straight to green, skipping yellow and all-red.
- A pedestrian window that can run into the yellow.