← Back to books

The LLD Interview, Decoded

You have forty-five minutes, a shared editor, and a stranger who has just said: "Design a parking lot."

Most candidates fail this in the first ninety seconds. Not by writing bad code — they haven't written any code yet — but by choosing the wrong first move. They start typing a Vehicle class. Or they ask no questions and design for a scope nobody wanted. Or they spend twelve minutes on a beautiful UML diagram and then run out of time with no working method bodies.

The low-level design interview is not a test of whether you know what a class is. It is a test of whether you can take an ambiguous, under-specified problem and drive it — under time pressure, out loud — to a small, defensible object model that compiles.

This chapter gives you the framework for doing that. Every problem chapter in this book runs the same six steps in the same order, so by Chapter 19 the framework should feel less like a checklist and more like a reflex.

What This Interview Actually Is

It helps to know what the LLD round is not, because candidates routinely bring the wrong instincts from adjacent rounds.

It is not the DSA round. Nobody wants your O(log n) seat lookup. If a HashMap and a for loop express the intent clearly, use them. Algorithmic cleverness that obscures the design is a net negative here — the opposite of the coding round, where it is the entire point.

It is not the system design round. There is no load balancer, no Kafka topic, no read replica, no CDN. The scope is one process, one heap. When a candidate starts drawing boxes labelled "Redis" in an LLD interview, the interviewer's note reads did not understand the question.

It is not a trivia quiz on the Gang of Four. Naming the Visitor pattern earns nothing. Recognizing that pricing rules will change every quarter and therefore belong behind a PricingStrategy interface — and saying so — earns everything. Patterns are the vocabulary, not the content.

What it is: a simulation of the most common thing a working engineer actually does. A product manager describes a feature in vague prose. You turn it into classes and interfaces that a teammate can extend six months from now without rewriting your work. The parking lot is a pretext. The real question is always: when the requirements change, does your design bend or break?

Q> ### Interviewer Says Q> Q> "Design a parking lot." Q> Q> That's the whole prompt. It is deliberately under-specified. The vagueness is not laziness — it is the first thing being scored. An interviewer who wanted to test your typing speed would have handed you a spec.

What Is Actually Being Scored

Interviewers at most companies fill in a rubric with four or five rows. The exact wording varies; the substance almost never does. Here is the composite, and roughly what each row is worth.

Signal Weight What earns a strong mark
Requirements & scope ~15% You asked clarifying questions, then stated a scope out loud and stuck to it, including explicit non-goals.
Object model ~30% Entities map to real concepts. Responsibilities are cleanly separated. Relationships (has-a, is-a, uses) are deliberate, not accidental.
Use of abstraction ~20% The things that will change are behind interfaces. The things that won't are concrete. You can justify each choice.
Working code ~20% Core classes have real method bodies. It would compile. The happy path runs end to end.
Extensibility under pressure ~15% When the interviewer adds a requirement in minute 40, you absorb it by adding a class, not by editing five existing ones.

Read that table again with an eye on what is missing. There is no row for number of classes. No row for completeness. No row for pattern count. A design with eight well-chosen classes and one working flow beats a design with twenty-five stubs, every time.

The single highest-leverage row is the last one. It is the row that separates the offer from the "no hire — could code, couldn't design." And it is decided long before minute 40, by choices you made in minute 10.

W> ### Trap W> W> Designing for a scope nobody asked for. Candidates who add license-plate OCR, a mobile app, and a loyalty programme to the parking lot are not demonstrating ambition. They are demonstrating that they cannot scope, which is the one thing every senior engineer must do daily. Breadth without a working core reads as panic.

The Six-Step Framework

Every problem chapter in this book follows these six steps. Learn them here; apply them fourteen times.

Step 1 — Clarify and Scope (≈5 minutes)

Ask questions whose answers would change your design. That filter matters. "How many cars?" is a bad question in an LLD round — the answer doesn't change a single class. "Can a vehicle occupy more than one spot?" is an excellent question, because "yes" forces a fundamentally different Spot model.

Three questions, asked well, is usually enough. Then say the scope out loud:

"So: multiple floors, three vehicle sizes, hourly pricing that will change, and multiple entry gates operating concurrently. I'm going to treat payment processing and license-plate recognition as out of scope — I'll define an interface at the boundary and stub it. Sound right?"

That sentence does an enormous amount of work. It converts a vague prompt into a contract, and it gets the interviewer to agree to that contract before you spend forty minutes on it. If they wanted payment in scope, they will tell you now rather than at minute 40.

Every problem chapter opens with this dialogue in full, then boxes the resulting scope: functional requirements, explicit non-goals, and the one or two constraints that will actually drive the design.

Step 2 — Identify Entities (≈5 minutes)

Underline the nouns. ParkingLot, Floor, Spot, Vehicle, Ticket, Gate, Payment. That's your first-pass candidate list, and it is deliberately dumb — noun extraction is a starting heuristic, not an answer.

Now prune it, using one hard question per noun: does this thing have identity or behavior of its own?

A Ticket has identity — you can look one up, and it changes state. Keep it.

A VehicleSize does not. It is a closed set of three values with no behavior. That's an enum, not a class hierarchy. Candidates who build abstract class Vehicle with Car, Truck, and Motorcycle subclasses — each of which overrides nothing — have added three files and zero information. We will meet this exact mistake, in detail, in Chapter 8.

Step 3 — Model Relationships (≈5 minutes)

For each surviving pair of entities, decide: has-a, is-a, or uses. Then sketch it. Boxes and arrows on the whiteboard; a Mermaid diagram if you're in a shared doc.

This step is short by design. Five minutes, not twelve. The diagram exists to align you and the interviewer, not to be beautiful. The moment the shape is agreed, stop drawing and start typing — an interviewer watching you tidy up arrow alignments at minute 20 is watching you fail.

Two defaults that will serve you well:

Prefer composition to inheritance. Inheritance is the tightest coupling in object-oriented programming. It is the right tool when there is a genuine is-a relationship and subtypes vary behavior. It is the wrong tool when you just wanted to share a field. Chapter 2 shows how Kotlin's by delegation gives you the reuse without the coupling.

Push volatility behind interfaces. Ask of every rule: will this change? Pricing changes. Allocation policy changes. Notification channels multiply. The Ticket having an id and a entryTime does not change. Volatile things get an interface; stable things stay concrete. This one question, applied consistently, will produce most of the pattern choices in this book without you ever having to reach for a pattern catalogue.

Step 4 — Design Behavior (≈5 minutes)

Now walk the primary use case, method call by method call, out loud:

"A car pulls up. EntryGate.admit(vehicle) asks the SpotAllocator for a free spot of the right size. The allocator returns a Spot, we mark it occupied, we create a Ticket, we hand it back."

This is where patterns enter — and they enter by name, because naming them is a compression device that makes the rest of the conversation faster. "SpotAllocator is a Strategy, so nearest-first and level-fill are swappable" communicates in one sentence what would otherwise take a paragraph.

But notice the order. You reached for the pattern because the walk-through exposed a decision point that will change. You did not scan a list of patterns looking for somewhere to put one. That direction — problem first, pattern second — is the whole difference between design and cargo cult.

Step 5 — Code the Core (≈20 minutes)

This is the largest block, and candidates consistently under-allocate it.

Code in layers, each of which stands alone:

  1. Value objects — enums, data classes, value classes. Fast, and they make the next layer readable.
  2. Entities — the things with identity and mutable state.
  3. Interfaces — the volatility boundaries you identified in Step 3.
  4. One concrete implementation of each interface. Just one. Say "and a FlatRatePricing alongside it, same interface" — the interviewer will believe you.
  5. The orchestrator — the class that wires it together and exposes the use case.
  6. The demo — the main() that proves it runs.

Two rules for this block. First, real method bodies, not stubs. A design where every method body is TODO() has demonstrated nothing; the whole claim of an object model is that responsibilities are placed correctly, and a stub places nothing.

Second, single-threaded first, concurrency second. If the problem has concurrency (Chapters 9, 13, 16, 17, 18, 19 all do), get the sequential logic correct and then do a deliberate second pass: "Now — two gates admit simultaneously. Spot.occupy() is a check-then-act, so it races. I'll guard the allocator with a Mutex." Weaving locks in from line one produces code that is both wrong and unreadable, and it costs you the chance to narrate the concurrency decision, which is itself a scored signal.

Step 6 — Extend and Defend (≈5 minutes)

The interviewer will add a requirement. This is not a curveball; it is the point of the exercise, and it is scheduled. "Now add electric vehicle charging." "Now make pricing depend on time of day."

A strong answer sounds like: "Charging is a property of the spot, not the vehicle, so Spot gains an hasCharger flag and EvSpotAllocator implements the existing SpotAllocator interface. Nothing else changes." You are pointing at the Open/Closed Principle without having to say its name.

A weak answer sounds like: "I'd add a boolean to Vehicle, and then in ParkingLot.park() I'd add an if…" — each if a small confession that the abstraction wasn't there.

If your design genuinely cannot absorb the change, say so. "This would require me to change Ticket and PricingStrategy both — that's a sign my Ticket is doing too much. Given more time I'd extract a Rate object." Honest diagnosis of your own design scores far better than a bluffed defence. Interviewers have seen the bluff before.

The Clock

Forty-five minutes, and the failure mode is always the same: too long on the diagram, not enough on the code.

Minute Activity
0–5 Clarify, scope, agree non-goals
5–10 Entities and pruning
10–15 Relationships and diagram
15–20 Behavior walk-through, name the patterns
20–40 Code
40–45 Extension question, defend, admit weaknesses

The twenty-minute mark is a hard gate. If you are not typing code by minute 20, you will not finish, and an unfinished LLD interview is a failed one no matter how elegant the diagram was. Set a mental alarm.

T> ### Kotlin Edge T> T> Kotlin buys you back roughly a third of the coding block versus Java. data class gives you equals, hashCode, toString, and copy for free. sealed interface plus when gives you exhaustive state machines the compiler checks. object is a Singleton without the double-checked-locking ritual. by gives you delegation without an inheritance hierarchy. T> T> This is not cosmetic. Those saved minutes are the difference between a working main() and a screen full of TODO(). Chapter 2 is entirely about spending them well.

Running the Framework: A Miniature

Enough theory. Let's run all six steps on a problem small enough to fit in one sitting: a coin-operated turnstile. It is deliberately trivial — you will meet the same machinery at full scale in Chapter 7 — but it exercises every step of the framework end to end, and it gives us a reason to set up the repository you will use for the rest of the book.

Step 1 — Clarify. Does the turnstile stay unlocked after one person passes? No — one coin, one entry. What if someone pushes without paying? It stays locked; we record the attempt. Refunds? Out of scope.

Scope: two states, two events, one entry per coin. Non-goals: refunds, multiple coin denominations, physical hardware.

Step 2 — Entities. Nouns: turnstile, coin, push, state. Coin and Push have no identity and no behavior — they are events, a closed set of two. That's a sealed interface, not two classes with fields. State is likewise a closed set: Locked, Unlocked. The only thing with identity and mutable state is the Turnstile itself.

Step 3 — Relationships. Turnstile has-a State. Turnstile uses Event. That's the entire diagram, which is the point: when the model is this small, don't dress it up.

Step 4 — Behavior. Turnstile.handle(event) maps (currentState, event) to (newState, effect). Two states times two events is four transitions — a total function, and Kotlin's when over sealed types will make the compiler prove we covered all four.

Step 5 — Code.

{title="ch-01-turnstile/src/main/kotlin/com/cracklld/ch01/Turnstile.kt"}

package com.cracklld.ch01

/** The closed set of things that can happen to a turnstile. */
sealed interface Event {
    data object Coin : Event
    data object Push : Event
}

/** The closed set of states a turnstile can be in. */
sealed interface State {
    data object Locked : State
    data object Unlocked : State
}

/** The observable consequence of a transition. */
sealed interface Effect {
    data object Unlock : Effect
    data object Lock : Effect
    data object Thankyou : Effect
    data object Alarm : Effect
}

data class Transition(val state: State, val effect: Effect)

class Turnstile(initial: State = State.Locked) {

    var state: State = initial
        private set

    private val _log = mutableListOf<Effect>()
    val log: List<Effect> get() = _log.toList()

    fun handle(event: Event): Transition {
        val transition = when (state) {
            State.Locked -> when (event) {
                Event.Coin -> Transition(State.Unlocked, Effect.Unlock)
                Event.Push -> Transition(State.Locked, Effect.Alarm)
            }
            State.Unlocked -> when (event) {
                Event.Coin -> Transition(State.Unlocked, Effect.Thankyou)
                Event.Push -> Transition(State.Locked, Effect.Lock)
            }
        }
        state = transition.state
        _log += transition.effect
        return transition
    }
}

Look at what the language did for us. All four transitions are visible in one screenful, arranged as a table. There is no if (state == LOCKED && event == COIN) chain to misread. And if a product manager adds a third state tomorrow, the code will not compile until every when handles it — the compiler becomes the reviewer that catches the case you forgot. That is the single strongest argument for sealed in an LLD interview, and it is worth saying out loud when you type it.

Note also what we didn't build. There is no TurnstileStateFactory. There is no AbstractEventHandler. The problem has four transitions; a design with more classes than transitions is a design that has lost the plot.

{title="ch-01-turnstile/src/main/kotlin/com/cracklld/ch01/Demo.kt"}

package com.cracklld.ch01

fun main() {
    val turnstile = Turnstile()

    val script = listOf(
        Event.Push,  // freeloader
        Event.Coin,  // honest customer pays
        Event.Coin,  // pays twice, poor soul
        Event.Push,  // and enters
        Event.Push,  // tries again, locked out
    )

    script.forEach { event ->
        val before = turnstile.state
        val (after, effect) = turnstile.handle(event)
        println("${before.name()} --[${event.name()}]--> ${after.name()}  ($effect)")
    }

    println("\nFinal state: ${turnstile.state.name()}")
    println("Effects: ${turnstile.log.joinToString { it.name() }}")
}

private fun Any.name(): String = this::class.simpleName ?: "?"

Step 6 — Extend. "Add an emergency-open mode: after a fire alarm, the turnstile stays open until reset."

Add data object Emergency : Event, data object Open : State, and a Reset event. The compiler now fails on every when, walks you to each hole, and you fill them. Three new declarations, zero edits to existing transitions. That is the Open/Closed Principle behaving exactly as advertised — and it is demonstrable, not asserted, which is precisely what the interviewer wants to see.

And the tests, which are how you prove the claim rather than merely making it:

{title="ch-01-turnstile/src/test/kotlin/com/cracklld/ch01/TurnstileTest.kt"}

package com.cracklld.ch01

import kotlin.test.Test
import kotlin.test.assertEquals

class TurnstileTest {

    @Test
    fun `coin unlocks a locked turnstile`() {
        val t = Turnstile(State.Locked)
        assertEquals(Transition(State.Unlocked, Effect.Unlock), t.handle(Event.Coin))
    }

    @Test
    fun `pushing a locked turnstile raises the alarm and stays locked`() {
        val t = Turnstile(State.Locked)
        assertEquals(Transition(State.Locked, Effect.Alarm), t.handle(Event.Push))
    }

    @Test
    fun `pushing an unlocked turnstile admits one person and relocks`() {
        val t = Turnstile(State.Unlocked)
        assertEquals(Transition(State.Locked, Effect.Lock), t.handle(Event.Push))
    }

    @Test
    fun `a second coin is pocketed with thanks`() {
        val t = Turnstile(State.Unlocked)
        assertEquals(Transition(State.Unlocked, Effect.Thankyou), t.handle(Event.Coin))
    }

    @Test
    fun `one coin buys exactly one entry`() {
        val t = Turnstile()
        t.handle(Event.Coin)
        t.handle(Event.Push)
        t.handle(Event.Push)

        assertEquals(State.Locked, t.state)
        assertEquals(listOf(Effect.Unlock, Effect.Lock, Effect.Alarm), t.log)
    }
}

Five tests, and the last one is the one that matters: it encodes the requirement ("one coin, one entry") rather than a single transition. In an interview you will rarely have time to write tests — but you should still say, at the end, "the invariant I'd test first is that one coin buys exactly one entry." Knowing what to assert is itself a design skill, and vocalizing it costs you ten seconds.

Setting Up the Repository

Every chapter from here is a runnable Gradle module. Clone once, run anything.

{title="settings.gradle.kts"}

rootProject.name = "crack-lld-kotlin"

include(
    "ch-01-turnstile",
    // ch-02 … ch-20 added as you go
)

{title="gradle/libs.versions.toml"}

[versions]
kotlin = "2.3.0"
coroutines = "1.10.2"
junit = "5.11.4"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

{title="ch-01-turnstile/build.gradle.kts"}

plugins {
    alias(libs.plugins.kotlin.jvm)
    application
}

kotlin {
    jvmToolchain(21)
}

dependencies {
    testImplementation(libs.kotlin.test)
    testImplementation(platform(libs.junit.bom))
}

application {
    mainClass.set("com.cracklld.ch01.DemoKt")
}

tasks.test {
    useJUnitPlatform()
}

Two commands, and they will work identically in every chapter of this book:

./gradlew :ch-01-turnstile:run
./gradlew :ch-01-turnstile:test

The run output:

Locked --[Push]--> Locked  (Alarm)
Locked --[Coin]--> Unlocked  (Unlock)
Unlocked --[Coin]--> Unlocked  (Thankyou)
Unlocked --[Push]--> Locked  (Lock)
Locked --[Push]--> Locked  (Alarm)

Final state: Locked
Effects: Alarm, Unlock, Thankyou, Lock, Alarm

There is no framework here, and there will not be one later. No Spring, no dependency-injection container, no database. An LLD interview has none of those, and every one you add buries the object model under configuration — which is the one thing the interviewer is trying to look at.

The Four Ways Candidates Fail

After enough interviews, the failures sort into four bins. Three of them are decided in the first ten minutes.

The Silent Architect. Designs beautifully, in their head. The interviewer sees a candidate staring at a blank screen, and cannot score a design they never heard. Your reasoning is not a byproduct of the interview — it is the interview. Narrate every choice, including the ones you reject.

The Pattern Tourist. Arrives having memorized twenty-three patterns and is determined to spend them. Produces a SpotAllocatorFactoryBuilder. Every abstraction has a carrying cost, and unpaid-for indirection reads as inexperience, not sophistication — a senior engineer knows that the second-hardest thing in design is knowing when not to abstract.

The Stub Farmer. Twenty-five classes, every method body TODO(). Nothing is demonstrated: a design's whole claim is that responsibilities sit in the right place, and a stub places nothing. Eight classes with real bodies and a running main() beat this every time.

The Over-Scoper. Adds features nobody asked for and finishes none of them. Covered above; still the most common failure of the four, and the easiest to avoid — just say your scope out loud in minute 4 and hold the line.

A> ### Scorecard A> A> A strong 45 minutes covers: an agreed scope with explicit non-goals; a pruned entity list with the pruning justified; one or two abstractions placed at genuine volatility boundaries; real method bodies for the primary flow; one extension absorbed without editing existing classes; and an honest word about what the design does badly. A> A> Book-only depth (don't attempt live): exhaustive test suites, full concurrency hardening, every alternative implementation of every interface, persistence.

What's Next

The framework is now yours. What it needs is the vocabulary to execute it fast.

Chapter 2 is about writing Kotlin that is actually Kotlin — sealed hierarchies, data and value classes, object, and by delegation — rather than Java with different keywords. Chapter 3 turns SOLID from five slogans into five refactors you can perform under a clock. Chapter 4 builds the ten-pattern toolkit that covers roughly ninety per cent of what this interview will throw at you. Chapter 5 takes a paragraph of prose and turns it into a class diagram in five minutes flat.

Then, from Chapter 6, we stop preparing and start designing — fourteen problems, the same six steps, every one of them ending in code that runs.

X> ### Exercise X> X> Before turning to Chapter 2, run the turnstile module. Then add the emergency-open extension from Step 6 yourself: a Emergency event, an Open state, and a Reset. X> X> Do it by adding the two data object declarations first and letting the compiler tell you where the holes are. Watch how it walks you to every missing transition. That experience — the compiler as an exhaustiveness checker for your state machine — is the single most useful thing Kotlin gives you in an LLD interview, and it is worth feeling once before you read the next chapter.