Advanced System Design Tutorial 0/70 lessons ~6 min read Lesson 67

    Low-Level Design Interview Practice

    A candidate who had just passed TechLearningPro's high-level loop was asked, in forty minutes, to design the classes for a campus garage: two gates, three vehicle sizes, a price th

    Course progress0%
    Focus
    15 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Low-Level Design Interview Practice

    Advanced System Design & Software Architecture · Lesson 67 · Architecture Practice, Interviews and Capstone

    Lesson Overview

    A candidate who had just passed TechLearningPro's high-level loop was asked, in forty minutes, to design the classes for a campus garage: two gates, three vehicle sizes, a price that finance can change, and a rule that two lanes must not sell one stall. They drew boxes labeled "ParkingService" and "Database," then wrote a single park() method that checked a list and appended to it. The interviewer asked what happens when both gates call park together. The candidate said "we will use synchronization" and did not point at the check and the update. The high-level loop had scored a 3.4. This loop scored under 2, because the invariant lived in a sentence instead of in a critical section.

    Low-level design interviews ask for types, responsibilities, state transitions, and the concurrency or failure behavior of one process. They are the deep dive of System Design Interview Framework, held long enough to draw a class diagram and write the interface that makes a bug hard to represent. Lesson 20 already designed TechLearningPro's garage in full: inventory, strategies, repositories, and tests. Lesson 35 already designed distributed rate limits. Lesson 56 already designed the notification platform. This lesson does not replace those packets. It trains the forty-minute cut: what you clarify, which types you commit to, which pattern you refuse, and how a rubric scores the result.

    Three problems carry the practice. The garage is the state-and-lock problem. An in-process rate limiter is the algorithm-and-time problem. A notification dispatcher is the ports-and-policies problem. Java is the notation, using java.time, java.util.concurrent, and interfaces you could implement without a vendor SDK. Where a channel really sends mail, the code stops at a port. The adapter would call that provider's published HTTP API. The interview does not invent one.

    You will leave with a clock, three class diagrams, one fully walked dispatcher, and a rubric that distinguishes "named a pattern" from "the failing interleaving is impossible."

    Learning Objectives

    By the end of this lesson you will be able to:

    1. Separate a low-level prompt from a high-level prompt and switch clocks without mixing the artifacts.
    2. Decompose a brief into actors, state, invariants, and the operations that cross a state edge.
    3. Assign responsibilities so a god class is an explicit choice you can defend or reject.
    4. Publish Java interfaces for the stable concepts and keep policies behind those interfaces.
    5. Decide when a Strategy, a State object, or a plain method is the design you can finish in the hour.
    6. Place a lock, an atomic structure, or a single-threaded owner around a check-then-act.
    7. Name the interleaving a test must force, including the one that should fail to take the same resource twice.
    8. Draw a class diagram and one sequence that match the interfaces, including the unhappy path.
    9. Score an LLD loop on responsibilities, invariants, extensibility, concurrency, and testability.
    10. Say what you are deferring to a database, a queue, or another service, so the in-process design stays honest.
    11. Practice the garage, the limiter, and the dispatcher without reciting the full case-study packets.
    12. Recover when the interviewer adds a requirement at minute twenty-five, by changing a policy instead of rewriting the model.

    Prerequisites

    • Lessons 11 through 13: objects, class diagrams, and sequence diagrams. The interview diagram is a subset of those notations.
    • Lessons 14 through 17: pattern names. Use a name only after the responsibility is clear.
    • Lesson 18, repositories, when the design has a collection whose storage you want to hide. The in-memory fake is enough in the room.
    • Lesson 19, dependency injection. In the interview, constructor injection is the sentence "the dispatcher receives its ports." A container tour is out of scope.
    • Lesson 20, the garage case study. This lesson's parking section is the interview slice. The full pricing and persistence packet stays there.
    • Lesson 35, distributed limiting and load shedding. The limiter here is one process. You should be able to say why a fleet needs a shared counter the interview may not have time to build.
    • Lesson 56, the notification platform. The dispatcher here is the in-process policy around a send port, not the multi-tenant pipeline.
    • Lesson 66, the seven-move clock. LLD spends the diagram minutes on types instead of on service boxes.

    Detailed Core Concepts

    What an LLD hour is asking

    The prompt sounds like a product and the expected output is a model. "Design a parking lot" means: which types own stalls and tickets, how a vehicle occupies a stall, how money is computed, and what two threads may do. "Design a rate limiter" means: the decision function, the data structure, the time source, and the behavior at a boundary. "Design a notification dispatcher" means: how a message becomes a channel attempt, how priority and consent are applied, and how a failed send is retried without a second OTP.

    Four artifacts score the loop.

    • A state model the interviewer can trace. Tickets move from issued to paid. Deliveries move from queued to sent, failed, or dead.
    • Interfaces that name the operations and hide the policy you expect to change.
    • One invariant enforced by construction or by a critical section, not by a comment.
    • A test sentence. "Two threads park one stall; one gets a ticket and one gets empty." If you cannot say the assertion, the design is still a story.

    The hour is not an HLD hour with classes sprinkled on top. If you spend twenty minutes on Kafka and regions, you have answered the wrong prompt. Say so if the interviewer was ambiguous, then ask which level they want. Lesson 66's framework is how you ask. This lesson is how you spend the time after they say "classes."

    The LLD clock

    Forty minutes is the common slot. Forty-five is the same clock with a longer test conversation.

    MoveMinutesArtifact
    Clarify behavior and invariants0–6Use cases, states, out of scope
    Core types6–14Nouns that own data, not a service god-object
    Interfaces and policies14–22Java-shaped methods, what is injected
    Collaboration22–30Class diagram and one sequence
    Concurrency or failure30–36The critical section or the retry rule
    Tests and trade-offs36–40Assertions, what a database would change

    When a new requirement arrives at minute twenty-five, look at the policy interface first. A new vehicle type should be a new fitter, not a new ParkingLot. A new channel should be a new ChannelPort, not a new dispatcher. If the new requirement changes an invariant, stop and rewrite the state model. Do not patch an invariant with a boolean.

    Responsibilities before patterns

    Write nouns from the prompt, then give each noun one reason to change. For the garage the nouns are stall, vehicle, ticket, price, and clock. The inventory owns which stalls are free. The fitter answers whether a vehicle may use a stall. The price policy answers money from a ticket and two instants. The lot facade, if you even have one, calls those three. It does not contain the rules.

    A pattern name comes after that split. Strategy fits the fitter and the price because both are replacements. State fits a ticket if transitions get their own behavior. A Factory is rarely worth the ink in a forty-minute loop. Singleton is usually a disguised global. If you reach for it, you owe the interviewer the lifecycle: one inventory per process, created at the edge, passed in.

    SOLID shows up as consequences. A switch on vehicle type inside park means a new type edits the inventory. That is the open-closed miss. A stall that computes price, prints a receipt, and talks to a gate has more than one reason to change. Split it. The interviewer is listening for the reason, not for the acronym.

    Interfaces the hour can finish

    Write methods with inputs that carry units. Instant now comes from a Clock the test controls. Money is a value type with a currency, not a double. Identifiers are types or records, not bare strings passed in five orders.

    java
    public interface SpotInventory {
    Optional<Assignment> tryAssign(Vehicle vehicle, Instant now);
    void vacate(StallId stallId, TicketId ticketId, Instant now);
    }
    public interface PricingPolicy {
    Money quote(Assignment assignment, Instant leftAt);
    }

    Optional empty means "no legal stall," which the gate turns into a full sign. It is a normal outcome, not an exception. Exceptions are for broken invariants: vacating a stall you do not hold. Say that distinction out loud. Interviewers use it as a proxy for whether you have operated an API.

    Keep the domain free of a framework. No annotation required to explain the design. Constructor parameters are the adapters. Lesson 5 and lesson 6 are the architecture reason. In the room it also saves you from drawing a container.

    Concurrency as a critical section

    Most LLD bugs in this course's loops are check-then-act.

    1. Thread A sees stall 12 free.
    2. Thread B sees stall 12 free.
    3. Both assign it.

    The fix is to make the observation and the update one critical section, or to use a structure whose operation is atomic for that update. A ReentrantLock around only the write still lets both threads pass the check. A concurrent list does not help if the algorithm is "scan, then set" as two calls.

    Say which lock you hold and what it guards. A single lock around the inventory is correct and may be coarse. Striping by floor is an optimization you mention after correctness. Lock ordering matters if a ticket lock and a stall lock can be taken in opposite orders. Prefer one lock that guards the inventory map until the interviewer asks for throughput.

    Clock must be injectable. Instant.now() inside the domain makes the test sleep. java.time.Clock is the JDK tool. Pass Instant into the method if you want the signature to show the dependency even more clearly. Either is defensible. Pick one.

    For the limiter, the time source is the algorithm. A token bucket that calls the wall clock inside a non-atomic update will hand the same token to two threads. The lock, or an atomic structure, includes the refill math.

    State machines you can draw

    Draw states before classes when the product is a lifecycle. A delivery is QUEUED, SENDING, SENT, RETRY_WAIT, or DEAD. Legal edges are the methods. markSent from QUEUED is illegal. The compiler will not save you unless the type structure makes the illegal call unrepresentable, which is often too heavy for the interview. A method that checks state and throws, covered by a test, is enough. Say that you chose tests over an algebraic encoding because the hour is forty minutes.

    Idempotency is a state fact. A second send for a delivery already SENT returns the stored receipt. It does not call the port again. That is the OTP bug in class form.

    Extensibility that fits the clock

    Interviewers add a requirement to see where the design flexes.

    AdditionDesign that absorbs itDesign that rewrites
    A new vehicle sizeA new StallFit implementationif ladders in park
    A weekend priceA new PricingPolicyConstants inside the ticket
    A new limit algorithmA new RateLimiterEdits to a servlet
    A new channelA new ChannelPortA dispatcher that knows SMTP

    If the addition forces a rewrite, say so and move the rule behind an interface while you still have minutes. That recovery scores well. Pretending the if ladder was "simple and therefore better" scores well only if you also say the condition under which you would split it, and the interviewer agrees the set is closed.

    The LLD rubric

    Six rows, scored 1 to 4. Weights favor the invariant and the types. A weighted 3.0 is a solid pass.

    DimensionWeightWhat a 3 requiresWhat a 4 adds
    Problem framing10%States, actors, and an explicit out-of-scope listA new requirement is classified as policy or invariant before code changes
    Responsibilities20%Each type has one reason to change; facade is thinThe god class is rejected with a named change that would have hurt
    Interfaces15%Methods show inputs, outputs, and empty versus errorTime and money are values; ports hide vendors
    Invariant25%Check and update share a critical section or an atomic opThe losing interleaving is described and matched to a test
    Extensibility15%One policy is replaceable without editing the workflowThe interviewer add is absorbed by a new implementation
    Communication15%Diagram and code use the same type namesClock management and a spoken trade-off

    A candidate can score 4 on a pattern name and 1 on the invariant. The weighted total will not look like a pass. That is intentional. The garage loop in the opening failed on the invariant row, and the weight makes that fatal.

    What you defer without hiding it

    In-process LLD omits the network. Say the omission. "This inventory is one JVM. Two gates in two processes need a conditional update in a database, the same shape as the trip accept in lesson 66. The critical section is the requirement; the lock is the single-process mechanism." That sentence connects the loops and prevents you from claiming a synchronized method is a distributed lock.

    For the dispatcher, defer the provider's HTTP details. The port returns a receipt or a classified failure: rejected, throttled, or unknown. Unknown means the send might have happened. The retry policy must not treat unknown as a clean failure for an OTP. That is the whole distributed-systems insight the LLD needs. Lesson 37 is the longer version.

    Architecture and Design Diagrams

    The three problems share a shape: a facade, a policy, a store, and a clock. The diagram that matters is the one for the problem you were given, not a framework of frameworks.

    mermaid
    classDiagram
    class Dispatcher {
    +accept(Command) DeliveryId
    +tick(Instant) void
    }
    class Delivery {
    +DeliveryId id
    +Channel channel
    +DeliveryState state
    +int attempts
    }
    class ChannelPort {
    <<interface>>
    +send(Outbound) SendOutcome
    }
    class ConsentGate {
    <<interface>>
    +permit(RecipientId, Channel) boolean
    }
    class RetryPolicy {
    <<interface>>
    +nextDelay(Delivery, SendOutcome) Optional~Duration~
    }
    class DeliveryStore {
    <<interface>>
    +insert(Delivery) void
    +update(Delivery) void
    }
    Dispatcher --> DeliveryStore
    Dispatcher --> ChannelPort
    Dispatcher --> ConsentGate
    Dispatcher --> RetryPolicy
    Dispatcher --> Delivery

    The garage interview slice is smaller than lesson 20's full model and uses the same idea: inventory and policies, with the clock outside.

    mermaid
    classDiagram
    class SpotInventory {
    <<interface>>
    +tryAssign(Vehicle, Instant) Optional~Assignment~
    +vacate(StallId, TicketId, Instant) void
    }
    class StallFit {
    <<interface>>
    +fits(Vehicle, Stall) boolean
    }
    class PricingPolicy {
    <<interface>>
    +quote(Assignment, Instant) Money
    }
    class Gate {
    +enter(Vehicle, Instant) EnterResult
    +exit(TicketId, Instant) Receipt
    }
    Gate --> SpotInventory
    Gate --> PricingPolicy
    SpotInventory --> StallFit

    The sequence that fails a weak design is the double enter. Draw it even if you also draw the happy path. Interviewers remember the race they can see.

    mermaid
    sequenceDiagram
    participant LaneA
    participant LaneB
    participant Inventory as SpotInventory
    LaneA->>Inventory: tryAssign(car, t)
    LaneB->>Inventory: tryAssign(car, t)
    Note over Inventory: one lock covers scan and mark
    Inventory-->>LaneA: Assignment stall 12
    Inventory-->>LaneB: empty

    The limiter's structure is an interface with two implementations the tests can swap. The diagram is intentionally small. If the diagram grows past what you can speak in five minutes, you have started an HLD.

    mermaid
    classDiagram
    class RateLimiter {
    <<interface>>
    +tryAcquire(LimitKey, Instant) Decision
    }
    class TokenBucketLimiter
    class SlidingWindowLimiter
    class Decision {
    +boolean allowed
    +Duration retryAfter
    }
    RateLimiter <|.. TokenBucketLimiter
    RateLimiter <|.. SlidingWindowLimiter
    RateLimiter ..> Decision

    Technical Explanation

    Clarifying questions that change types

    Ask questions that add a type or an invariant. For the garage: which vehicles exist, may a large vehicle take a small stall, is the price a function of duration, who may change the price, and is the design one process at one gatehouse or many processes? For the limiter: who is the key, what is the limit, is a burst allowed, is the decision local to one JVM, and what should the caller do when refused? For the dispatcher: which channels, is an OTP allowed to wait behind a campaign, what is consent, and is a timeout a retry or an unknown?

    Write the invariant in the same words you will use in the test. "Two enters, one assignment." "A key never receives more permits than the refill math allows, including two threads at the same instant." "A delivery id is sent at most once when the outcome is known, and an unknown outcome is visible to the retry policy."

    How to talk through code

    Write the interface, then the fields of the stateful class, then the method that holds the invariant. Narrate the lock in the method, not in a speech before the code. Use names from the diagram so the interviewer can point. If you misspell a transition, correct it on the diagram in the same minute. Drift between the picture and the code is a communication deduction.

    Prefer records for values that do not change: Decision, Money, SendOutcome, Assignment. Prefer an enum for a closed state. Prefer an interface for a policy. Prefer a final class for the inventory, with the lock as a private field. These are ordinary Java choices. They are not a library.

    Token bucket, said precisely

    A bucket holds a count of permits, a capacity, a refill rate in permits per second, and a last-refill instant. On tryAcquire, compute how many permits the elapsed time has added, cap at capacity, then either decrement or refuse. The refill and the decrement are one critical section. A refusal includes retryAfter so the caller can set Retry-After at the HTTP edge. The limiter itself returns a decision. It does not throw a web exception. That keeps it testable without a servlet stack.

    A sliding window stores timestamps of recent permits, drops those older than the window, and refuses when the remainder is at the limit. It is fairer at a window boundary and more expensive in memory. Fixed windows are easy and allow twice the limit across the boundary. Say that if you choose them, and say it is acceptable only when the product tolerates the spike. Lesson 35 makes the same distinction for a shared store. Here the store is the JVM.

    This design is local. Two JVMs each allow the full rate. If the prompt says "one user across the fleet," you have been moved to HLD. Say: the decision function stays; the counters move to a shared store with an atomic increment; the local class is the specification of the math.

    Dispatcher outcomes

    Classify what a port can return.

    • Accepted. The provider took the message. Store the provider message id. State becomes SENT. Do not send again.
    • Rejected. A permanent no, such as a bad address or a consent denial you should have caught earlier. State becomes DEAD or REJECTED. Do not retry.
    • Throttled. The provider asked you to wait. Honor the delay. This is a property of the outcome, not a string you parse from an imaginary SDK.
    • Unknown. Timeout or a connection drop after you wrote the request. The provider might have accepted it. For an OTP or any exactly-meaningful message, do not blindly send a second body. Surface UNKNOWN and let a reconciliation path, or a human for low volume, decide. For a campaign mail, a duplicate is cheaper, and the policy may retry. The policy chooses. The port only classifies.

    Priority is two queues or a single queue with a comparator. OTP and password mail sit ahead of digests. A digest must not occupy the only worker forever. A fair approach is a reserved share of attempts per tick for the high-priority class. Say the number as an assumption: four of five attempts in a tick are allowed to be bulk, one slot is reserved for OTP if any OTP is waiting. The number is a product choice.

    Consent is a gate in front of send. The dispatcher asks ConsentGate. A false result completes the delivery as SKIPPED and does not call the port. Marketing can be skipped. An OTP channel is configured so the gate returns true. That configuration is data, so security mail is not a special if buried in a loop you will get wrong at minute thirty-eight.

    Parking fit and price

    A stall has a size and a set of features, such as a charger. A vehicle has a size and needs. StallFit returns whether this vehicle may take this stall. The inventory scans stalls under the lock, asks the fitter, and marks the first fit occupied with the ticket id. Scanning a few hundred stalls under one lock is fine for a campus garage. Say the complexity. If the interviewer says "ten thousand stalls and a hundred gates," you mention an index by size, still under the same lock, or you move assignment to SQL as in the deferral sentence.

    Price is a pure function of the assignment and the exit instant. Hourly and flat policies implement the same interface. The ticket stores the instants and the stall. It does not store a mutable "current price" that a second thread can half-update. Money uses minor units. double for currency is a mistake you can name in one sentence: binary fractions cannot represent 0.10 exactly. A long of minor units plus a currency code is enough.

    Tests you describe even if you do not type them

    Three tests per problem are the set that maps to the rubric.

    • The happy path returns the value and the new state.
    • The boundary refuses: lot full, bucket empty, consent false.
    • The race or the retry: two threads, one winner; or unknown outcome does not double-send an OTP.

    Name the clock. "At t0 the bucket is full. At t0 plus half a refill period, one permit exists." A test that sleeps has not used the Instant you put in the signature.

    Scoring notes you can reuse

    Quote the lock or the atomic section under the invariant row. Quote the interface under extensibility by imagining the interviewer's add. If the candidate cannot place the add, the row is a 2 even if the first version worked. Quote a divergent diagram under communication. Adjectives do not fill a row.

    Practical Implementation or Design Walkthrough

    This is a forty-minute design of an in-process notification dispatcher for TechLearningPro. The platform around it is lesson 56. The interview stops at one JVM that accepts commands, respects consent and priority, calls a port, and records an outcome. Workers, databases, and provider HTTP are named as the next boundary, not designed.

    Minute 0–6

    Actors: the product code that asks for a send, the dispatcher, the channel ports, a consent source, and a learner who must receive an OTP even while a campaign is running.

    In scope: email, SMS, and push; a priority of TRANSACTIONAL or BULK; consent; retries for throttled and for rejected-as-temporary; a dead state after a bounded number of attempts; idempotent accept of the same delivery id.

    Out of scope: template rendering, provider selection markets, multi-region, the actual HTTP calls, and digest scheduling. Rendering happens before accept. The command carries a body that is already text.

    Invariant: for a TRANSACTIONAL delivery, ChannelPort.send is invoked again only when the previous outcome was THROTTLED or a temporary reject. An ACCEPTED or UNKNOWN does not auto-send.

    Assumption: one process, a few hundred sends per second, so a lock around the delivery map is an acceptable start.

    Minute 6–22: types

    java
    public enum Channel { EMAIL, SMS, PUSH }
    public enum Priority { TRANSACTIONAL, BULK }
    public enum DeliveryState { QUEUED, SENDING, SENT, WAITING, SKIPPED, UNKNOWN, DEAD }
    public record DeliveryId(String value) {}
    public record Outbound(DeliveryId id, Channel channel, String destination, String body) {}
    public enum OutcomeKind { ACCEPTED, REJECTED, THROTTLED, UNKNOWN }
    public record SendOutcome(OutcomeKind kind, Optional<String> providerMessageId,
    Optional<Duration> retryAfter) {}
    public interface ChannelPort {
    SendOutcome send(Outbound message);
    }
    public interface ConsentGate {
    boolean permit(String recipientId, Channel channel, Priority priority);
    }
    public interface RetryPolicy {
    Optional<Duration> nextDelay(Priority priority, int attemptsSoFar, SendOutcome outcome);
    }
    public interface DeliveryStore {
    Optional<Delivery> find(DeliveryId id);
    void insert(Delivery delivery);
    void save(Delivery delivery);
    }

    RetryPolicy returns empty when there should be no further send. A default policy: ACCEPTED and REJECTED and UNKNOWN return empty for TRANSACTIONAL. THROTTLED returns the given delay, or thirty seconds if the port gave none, until five attempts, then empty. BULK may treat UNKNOWN as retryable up to three attempts because a duplicate campaign mail is tolerable. Say that product choice. Do not hide it inside a loop.

    The dispatcher:

    java
    public final class Dispatcher {
    private final DeliveryStore store;
    private final ConsentGate consent;
    private final ChannelPort email;
    private final ChannelPort sms;
    private final ChannelPort push;
    private final RetryPolicy retry;
    private final Object monitor = new Object();
    public Dispatcher(DeliveryStore store, ConsentGate consent,
    ChannelPort email, ChannelPort sms, ChannelPort push,
    RetryPolicy retry) {
    this.store = store;
    this.consent = consent;
    this.email = email;
    this.sms = sms;
    this.push = push;
    this.retry = retry;
    }
    public DeliveryId accept(Submit submit) {
    synchronized (monitor) {
    Optional<Delivery> existing = store.find(submit.id());
    if (existing.isPresent()) {
    return existing.get().id();
    }
    Delivery created = Delivery.queued(submit);
    store.insert(created);
    return created.id();
    }
    }
    public void tick(Instant now) {
    Delivery next = claimNext(now);
    if (next == null) {
    return;
    }
    if (!consent.permit(next.recipientId(), next.channel(), next.priority())) {
    store.save(next.skipped());
    return;
    }
    SendOutcome outcome = portFor(next.channel()).send(next.outbound());
    store.save(next.after(outcome, retry, now));
    }
    }

    claimNext runs under the same monitor. It selects a QUEUED or due WAITING delivery, preferring TRANSACTIONAL if one exists and the last few claims have all been bulk, and marks it SENDING before releasing the lock. The port is called outside the lock. That is deliberate. Holding the monitor across a provider call blocks OTP acceptance behind a slow SMS. The cost is that a crash while SENDING leaves a row that a startup sweep must move to UNKNOWN. Say that sweep. It is the crash story, and it is the reason SENDING is a state and not a log line.

    after applies the policy: ACCEPTED becomes SENT and stores the provider id; a delay becomes WAITING with notBefore; empty optional becomes DEAD or UNKNOWN based on the kind. A second tick must not claim a SENT row. That is a query condition, tested directly.

    Minute 22–30: diagram and sequence

    Use the dispatcher class diagram from the previous section. The sequence for an OTP is: accept inserts QUEUED; tick claims SENDING; consent permits; the SMS port returns ACCEPTED; state is SENT. The sequence for a timeout is the same until the port returns UNKNOWN; state is UNKNOWN; no automatic second send.

    Minute 30–36: the bug you are preventing

    Two workers call tick. Both see the same OTP queued. Without claimNext flipping state under the monitor, both call send. The learner gets two codes. The critical section is the claim, not the send. Say why the lock cannot cover the send: latency and a deadlock with any lock the port might take. The single-threaded alternative is also valid: one thread runs tick in a loop. Then the monitor is optional for claims and still useful for accept versus claim if accept runs on a request thread. Pick one model. The code above is the two-thread model.

    Idempotent accept covers the client retry. It does not cover two different ids for one logical OTP. That is the caller's key choice. Say it. The dispatcher cannot invent the business idempotency of "one code per login" if the caller mints a new id per retry. The contract is: same id, one delivery row.

    Minute 36–40: tests and trade-off

    Tests: duplicate accept returns one row; consent false skips and the port mock shows zero calls; two threads tick one OTP and the port shows one send; UNKNOWN leaves the state and the port is not called again on the next tick; bulk UNKNOWN may be called again if the policy says so.

    Trade-off: the in-memory monitor is the design for one node. The refusal is a distributed lock around send. The reopen condition is a second dispatcher process, which moves claimNext to a conditional update in the database: UPDATE delivery SET state = 'SENDING' WHERE id = ? AND state = 'QUEUED'. Same invariant, different mechanism. Lesson 56 is where that SQL lives in the platform. You were not asked to design the platform.

    Score

    Framing 3, responsibilities 3, interfaces 4, invariant 4, extensibility 3, communication 3. Weighted about 3.4. The extensibility 3 is because a new channel still requires a constructor parameter. A map from Channel to ChannelPort would be the 4. Mention that map if you have a minute. It is a one-line improvement.

    Enterprise Real-World Scenarios

    Scenario 1: the campus garage loop, scored

    TechLearningPro runs a garage for weekend intensives. Two entry lanes, one exit, motorcycles, cars, and vans, a handful of charger stalls, and a price list finance edits in a form. The forty-minute prompt is the interview slice of lesson 20. The candidate draws ParkingLot with park, unpark, and a List<Spot>. Price is hours * 10 inside unpark. The fitter is an if on an enum. They mention "a lock" and cannot say whether it covers the scan.

    Score: framing 2, because charger stalls and finance-owned prices were in the prompt and absent from the model. Responsibilities 1, one class. Interfaces 2, no policy seam. Invariant 1, the lock is a word. Extensibility 2, a new price edits unpark. Communication 2, no sequence. Weighted near 1.5. The debrief quote the panel should keep is: "Did not make scan-and-mark atomic."

    A passing answer uses SpotInventory, StallFit, and PricingPolicy as in the class diagram. tryAssign holds one lock, asks the fitter, and marks the stall before release. vacate checks the ticket id so one lane cannot free another vehicle's stall. Price is computed at exit from instants. The candidate says: two processes require the conditional update from the lesson 20 persistence section; this design is the gatehouse JVM. That sentence is a 4-level invariant comment if the critical section is also real. Practice this prompt until the lock is in the method on the first try. The full case study is the answer key for persistence, not a script to recite.

    Scenario 2: quiz-submit limiter, local then fleet

    During the exam window from lesson 66, a single API node was supposed to protect the submit path from a broken client that retries in a tight loop. An engineer designs an in-process limiter in the LLD loop: key is learner id plus exam id, five submits per minute, burst of two, token bucket, Clock injected, decision mapped to HTTP 429 with Retry-After at the controller. The interview is thirty-five minutes. They finish the bucket and the test where two threads at the same instant cannot both take the last permit.

    The staff follow-up, still inside the loop, is: "There are twelve nodes." The candidate who says "put synchronized on the controller" fails the follow-up. The candidate who says "the RateLimiter math stays; the permit state moves to a shared atomic counter; local limiting still helps as a first shed; the shared counter is the fleet truth" passes. They have kept the LLD and named the HLD boundary. Lesson 35 is the shared-counter design. You do not re-derive Redis in the remaining three minutes. You do say that a fixed window will allow a boundary double-burst and that you would keep the token bucket for that reason.

    The enterprise consequence is visible on exam day. A limiter that is accidentally per node multiplies the allowed retries by twelve. The primary sees the cliff the capacity review thought it had capped. The LLD interview is where that multiplication should have been said out loud.

    What to carry from both

    The garage fails people who forget the critical section. The limiter fails people who hide a fleet behind a JVM lock. The dispatcher fails people who retry UNKNOWN for an OTP. Those three failure modes are the practice set. Rotate them. Scoring only the happy path will not move your invariant row.

    Best Practices

    • Ask which level, LLD or HLD, when the prompt could be either. Then commit.
    • Write the invariant as a test sentence before you write fields.
    • Give policies interfaces and give workflows a thin coordinator.
    • Pass Instant or a Clock. Keep wall time out of the core.
    • Represent money as minor units. Represent closed lifecycles as enums.
    • Hold the lock over the check and the update. Release it before any port call.
    • Classify external outcomes so retry policy does not parse free text.
    • Describe three tests: success, refusal, and the race or the duplicate.
    • When the design is one process, say what two processes would use instead.
    • Absorb an interviewer add by a new implementation of an existing interface when the invariant still holds.
    • Keep type names identical on the diagram and in the code.
    • Prefer a correct coarse lock over a striped lock you cannot finish.

    Common Mistakes and Anti-Patterns

    The service blob. One class holds stalls, prices, printing, and the gate protocol. It happens because the prompt says "design a parking lot," and a lot sounds like one object. The consequence is that finance's price change edits gate logic. Split inventory, fit, and price. Leave a facade only if the gate needs a single entry method.

    The decorative lock. The candidate says "thread-safe" and synchronizes the setter that stores a stall after both threads have already chosen it. The check is outside. The consequence is the double assign the prompt exists to prevent. Move the decision inside the locked region, or use one atomic structure that performs the whole decision.

    Pattern bingo. Strategy, factory, singleton, observer, and builder appear in the first ten minutes without a varying rule. It happens because pattern vocabulary feels like seniority. The consequence is a diagram you cannot implement before the clock ends. Add a pattern when a second implementation is real or clearly coming.

    Vendor types in the domain. The dispatcher returns a type from a provider SDK, or the limiter throws a framework HTTP exception. It happens because production code often looks like that. The consequence is an untestable core and an interview that stalls on a library you cannot remember. Put the SDK in an adapter that maps into SendOutcome or Decision. If you do not remember the provider's fields, you have lost nothing, because you were not supposed to invent them.

    Retry of the unknown. A timeout is coded as "try again immediately" for every priority. The OTP arrives twice and one code is already stale. Classify UNKNOWN and let RetryPolicy see the priority. The port does not decide the business retry.

    A clock that sleeps. Tests use Thread.sleep, or production calls Instant.now() in the bucket. The race test becomes flaky and the interview cannot walk a boundary. Pass the time in.

    Silent distribution. A static synchronized method is described as the limit for "the system." With several nodes it is several limits. Say the process boundary every time you show a lock.

    Troubleshooting and Design Review

    Symptom in the mockLikely causeCorrection
    Interviewer asks "where is the race?" and you repeat the happy pathInvariant was never written as an interleavingDraw two clients and mark the first line that must be inside the lock
    Adding SMS edits the dispatcher loopChannel behavior lives in if blocksMove send to ChannelPort and select the port by enum
    Price tests fail when the clock movesDuration math uses a mutable field updated by a background threadCompute price as a function of two instants
    Two tickets for one stall in the test you finally wroteLock covers the write, scan is outside, or the collection is copied unsafelyHold one lock for the whole tryAssign body that decides
    OTP duplicates after a timeoutUNKNOWN mapped to the same path as THROTTLEDSplit the enum and cover both in tests
    Diagram says Ticket, code says ReceiptRenames were not pushed throughFreeze names at minute fourteen and only add types
    You ran out of time before any testCollaboration was over-drawnStop the diagram at the types you have code for
    A 4 on extensibility and a 1 on invariantPatterns were rewarded by the wrong rubricRe-score with the weights in this lesson before the debrief

    When you review a colleague's mock, ask them to point at the line that makes the bad interleaving impossible. If they point at a class name, the invariant row is not a 3. If they point at a synchronized block, read the block and see whether the decision is inside it. That review habit is the same one lesson 69 applies to services, at the scale of a method.

    Interview Questions and Answers

    Q1. How is a low-level design interview different from a high-level one?

    The low-level hour produces types, state, and a critical section or a policy seam inside one process. The high-level hour produces a scope sentence, a bottleneck, service responsibilities, and a failure the user can see. Both need an invariant. They spend the minutes after framing on different artifacts. A class diagram does not satisfy an HLD prompt about regions and fan-out. A fleet diagram does not satisfy an LLD prompt about two threads in park. If you are unsure, ask. Then use the clock from this lesson or from lesson 66, and do not mix them in one drawing.

    Q2. How do you decide that a rule deserves its own interface?

    A rule deserves an interface when a second implementation is already in the prompt or is the obvious interviewer add, and when the workflow should not change if the rule changes. Garage prices and stall fit pass that test. Channel sends pass that test. The loop that claims the next delivery does not: there is one claiming algorithm until someone asks for a second. Extra interfaces without a varying rule spend minutes you needed for the race. The rubric's extensibility row is satisfied by one real seam, not by five.

    Q3. Where should the lock sit in an assignment method?

    Around the entire decision: the read of "what is free," the fit check, and the write that marks the stall taken. A lock around only the write allows two threads to choose the same stall and then serialize the corruption. A lock around the whole process, including a call to a payment provider or a printer, is correct and too wide. It will time out under a slow port. The pattern is: decide under the lock, act outside, reconcile if the outside action fails. For parking, the outside action is the gate arm. If the arm fails, you vacate inside the lock. Say that compensating path. It is the difference between a sketch and an operable gatehouse.

    Q4. Why is Optional empty a better "lot full" signal than an exception?

    A full garage is a normal business outcome. The gate turns it into a sign. Exceptions are for states the type system or the invariant says should not happen, such as vacating a ticket the inventory does not hold. Using exceptions for full lots trains callers to use exceptions for control flow and makes the success path look like the failure path in logs. In the interview, say which outcomes are empty and which are thrown. Then use the empty outcome in the sequence diagram so the interviewer sees the gate stay up.

    Q5. How do you represent money and time so the tests are exact?

    Time is an Instant passed in, or a java.time.Clock owned by the edge and not by the stall. Tests supply the instants of entry and exit and assert a duration. Money is a minor-unit long and a currency. Hourly price rounds by a rule you state, such as round up to the next hour, or charge per minute. The test uses amounts the rule decides exactly, such as ninety minutes at 200 minor units per hour. double will eventually fail a test that expected 0.10, and it will fail in production the same way. You do not need a money library to say this. A record is enough.

    Q6. What is the right depth of a Strategy in a forty-minute loop?

    One interface, two implementations named even if only one is written out, and a constructor that receives the interface. For price, write the hourly class and state that a flat evening price is another class with the same quote method. Do not build a decorator stack, a registry, or a configuration language. If the interviewer asks "how does finance change the rate without a deploy?", the answer is that the policy reads a value object loaded at the edge, still behind the interface. The domain method stays a function. The reload mechanism is one sentence, not a new subsystem.

    Q7. How do you handle a provider timeout in the dispatcher?

    Map it to OutcomeKind.UNKNOWN inside the adapter, which is the only place that sees a socket. The core stores UNKNOWN and asks RetryPolicy. For transactional mail, the default policy does not schedule another send, because the first may have left the provider. For bulk, the policy may retry with a cap. Reconciliation, if you mention it, is a later read of the provider's published status endpoint by the provider message id when you have one, and a human or a hold state when you do not. Do not invent a private "did it send" API. Do not treat a timeout as a clean REJECTED, which would mark an OTP dead while the code is already on the phone.

    Q8. When does an in-process limiter stop being the design?

    When the limit must hold across more than one process, or when a restart must not reset the budget. Say that boundary as soon as you show the lock. The local class remains the specification of refill math and of retryAfter. The fleet design replaces the fields with a shared atomic update and keeps the same Decision. If you only have a minute, that sentence is the whole answer. Implementing a networked counter from memory in the last minute usually corrupts both designs. Lesson 35 is where the shared algorithm is specified with care, including window boundaries.

    Q9. How do you recover when the interviewer adds "and it is multi-threaded" at minute thirty?

    Stop drawing new types. Take the method that does the check-then-act and wrap the decision in one lock. State what the lock guards: the map from stall id to occupancy, or the bucket's permits and timestamp. Mention the interleaving that is now impossible. If two locks appear, name the order. If you cannot finish striping, say the coarse lock is the submission and striping is a follow-up justified by a contention measurement you have not taken. Panels score this recovery highly when the lock actually covers the decision. They score it poorly when you add synchronized to a getter.

    Q10. How should a reviewer use the LLD rubric in a hiring debrief?

    Quote the critical section or its absence under the invariant row, which is 25% of the score. Quote one interface under extensibility. A candidate with a beautiful diagram and a race scores under 2.5 and does not pass on diagram quality. A candidate with coarse locking, a clear port, and a test sentence for the loser scores as a hire even if they never said "hexagonal." Compare quotes between interviewers. If one interviewer awarded a 4 for "used design patterns" and cannot find the race, rescore. The weights exist so pattern fluency cannot outvote a double-assigned stall.

    Q11. What do you say about persistence if the prompt never mentions a database?

    Give the collection an interface, DeliveryStore or an inventory map, and implement the interview against memory. Say which operation must become a conditional update when the store is shared: the claim of a delivery, the assignment of a stall. Name the unique key if one falls out of the invariant, such as one open ticket per stall. Stop there. Schema design is a different minute budget. Pretending the HashMap is durable is the mistake. A repository interface plus one sentence of SQL shape is the senior answer. Lesson 18 is the pattern. You do not need to draw it unless the interviewer asks where the interface lives.

    Q12. How do you practice LLD so you stop missing races?

    Once a week, write only the invariant method for one prompt, with a test that starts two threads. Prompts rotate through assign-a-resource, take-a-permit, and send-at-most-once. Time-box the interfaces to fifteen minutes and the race to ten. Score the invariant row before you score anything else. Keep a list of locks that were too narrow. The goal is a hand that puts the decision inside the critical section without a reminder. Full case studies in lessons 20 and 22 are for depth after the mock, when you want to see persistence and payment states done carefully. They are not the script for minute ten.

    Hands-On Exercises

    Exercise 1: dispatcher, closed book

    Problem. In forty minutes, design the notification dispatcher from the walkthrough without looking at it. Then open the walkthrough and mark every invariant you missed.

    Tasks.

    1. Write states, the OTP retry rule, and the out-of-scope list.
    2. Declare ChannelPort, ConsentGate, RetryPolicy, and the outcome enum.
    3. Show accept idempotency and claimNext under one monitor.
    4. Call the port outside the lock. State the crash sweep for SENDING.
    5. Describe the five tests from the walkthrough.
    6. Say what changes when a second process runs tick.

    Done when. Your page contains one critical section and one outcome kind that does not auto-retry for transactional mail.

    Exercise 2: garage plus a late requirement

    Problem. Thirty-five minutes for the campus garage. At minute twenty-five, the interviewer adds: vans may take two adjacent car stalls, or none.

    Tasks.

    1. Start with StallFit and a locked tryAssign.
    2. Absorb the add by changing the fitter and the inventory's mark step so both stalls commit or neither does, still under one lock.
    3. Explain why a fitter that only returns a boolean is no longer enough, and what it returns instead: zero or more stall ids.
    4. Score extensibility honestly. If you rewrote Gate, the row is not a 4.

    Done when. The partial assignment cannot be observed by the other lane, and price still depends only on instants and a policy.

    Evaluation rubric

    Assessment areaWeightScore 3 means
    Problem framing10%States and the OTP or stall invariant are written before code
    Responsibilities20%Inventory, fit, and price, or port, consent, and retry, are separate
    Interfaces15%Outcomes are enums or records, not vendor types
    Invariant25%The decision and the write share one critical section
    Extensibility15%The late add is a policy or a return-type change, not a new god class
    Communication15%Diagram names match the code, including the unhappy sequence

    A missing race test caps the invariant row at 2. A design that only works "if we assume one thread" caps that row at 1 when the prompt said two lanes or two workers.

    Lesson Summary

    A low-level interview is a forty-minute model of state, interfaces, and one enforced invariant. The garage requires the scan and the mark to share a lock. The limiter requires refill and decrement to share a lock, and it requires you to say when the lock stops being the fleet. The dispatcher requires a claim-before-send critical section, a port that classifies timeouts as unknown, and a retry policy that will not send a second OTP. Patterns are seams for rules that vary. They are not the score.

    The rubric weights the invariant at 25%. A pattern-rich race is a failed loop. Practice by writing the critical section and the two-thread test first. Lesson 20, lesson 35, and lesson 56 remain the production packets. Lesson 68 practices the high-level clock on the case studies. Use the level the prompt asked for.

    Further Reading

    • java.time.Clock — the time source tests can control.
    • java.util.concurrent.locks.ReentrantLock — an explicit lock when synchronized is the wrong scope to explain.
    • RFC 6585 — 429 and the relationship between Decision.retryAfter and Retry-After.
    • RFC 9110 — idempotent methods, when you discuss a second accept of the same delivery.
    • Martin Fowler, Patterns of Enterprise Application Architecture — repository and gateway, as the ports in this lesson, not as a checklist to recite.
    • Lesson 20, lesson 35, lesson 37, and lesson 56 in this course — the full garage, the distributed limiter, idempotency, and the notification platform this hour compresses.
    Ready to mark this lesson complete?Track your journey across the entire course.