HLD vs LLD: 7 Key Differences, Explained with One System
Learn the difference between high-level and low-level design through a ticket booking system, architecture diagrams, code, and an interview checklist.
+ reserve(seatId)+ confirm(holdId)+ release(holdId)In this article
“Design a ticket booking system.” Five words, two very different conversations. One is about traffic, storage, and failures. The other is about seat holds, method contracts, and what happens when two people press Book at the same time.
Both are system design. The difference is the level of detail you need to make the next decision.
HLD vs LLD at a glance
The boundary is useful, but it is not a universal standard. An API or a database schema can appear in either discussion; the important distinction is how much detail the decision requires.
| Dimension | High-level design | Low-level design |
|---|---|---|
| 1. Main question | How do the parts work together? | How does this part behave correctly? |
| 2. Scope | Services, storage, integrations, deployment | Modules, objects, functions, records |
| 3. Requirements | Capacity, latency, availability, consistency | Invariants, validation, errors, edge cases |
| 4. Typical artifacts | Architecture and data-flow diagrams | Class and sequence diagrams, contracts, pseudocode |
| 5. Data decisions | Ownership, access patterns, partitioning | Fields, indexes, constraints, transaction boundaries |
| 6. Failure handling | Timeouts, isolation, retries, recovery | Exceptions, idempotency checks, safe state transitions |
| 7. Evidence | Estimates and architectural tradeoffs | Executable behavior and focused tests |
Think of them as two zoom levels on the same system. A component diagram without ownership rules leaves too much unanswered. A polished class hierarchy cannot rescue a system that sends every read to an overloaded database.
One example: booking a concert seat
We will design reserved seating for a concert. A user chooses a seat, holds it briefly, pays, and receives a confirmed booking.
Our example has three explicit rules:
- At most one active hold or confirmed booking may own a seat for an event.
- A hold expires after five minutes unless it is confirmed.
- Payment results may arrive late or more than once.
For a capacity exercise, assume 100,000 people open the seat map during a one-minute sale window, each refreshing every ten seconds. While all are active, that is about 10,000 seat-map reads per second. This is an illustrative workload, not a benchmark. Request bursts and skew toward a popular event still matter.
The HLD: responsibilities and data flow
Start with a booking application, a transactional database, a cache for browsing, and a payment provider. These are logical responsibilities; the application can begin as one deployable service.
Separate browsing from booking
Seat-map reads can use a short-lived cache. A stale map may show a seat that has just been taken; the reservation endpoint must check authoritative state and return a conflict. Cache availability never proves that a seat is still bookable.
All writes for a seat go through the booking application to one authoritative database owner. Do not try to enforce the no-double-booking rule by reading from an asynchronously replicated read replica.
If traffic grows, scale stateless application instances first. Partitioning by event is a possible later choice, but a single popular concert can still overwhelm one partition. You would need admission control, a waiting room, or a finer partition strategy based on observed load.
Make failure boundaries explicit
Payments cross a network boundary. A timeout does not prove that a charge failed. Give each payment attempt a stable idempotency key, persist its identity, and reconcile uncertain outcomes before creating a new attempt.
Keep the database transaction short; never hold a seat-row lock while waiting for a payment network call. After confirmation, store a notification event in an outbox in the same transaction as the booking. A worker can retry email delivery without rolling back the confirmed seat.
Works well
- A transactional owner makes the seat invariant easier to enforce.
- Cached browsing reduces read pressure without deciding who owns a seat.
- An outbox lets notification delivery recover independently.
Watch out for
- One very popular event can remain a hot spot.
- A cached seat map can disappoint a user at reservation time.
- Payment reconciliation and outbox retries add operational work.
These are HLD decisions because they establish ownership, dependencies, and failure boundaries. They still leave an important question open: what does a correct reservation operation do?
The LLD: contracts and state transitions
Zoom into the booking application. We need a service that coordinates the workflow, a repository that performs atomic seat transitions, and a payment adapter that records and reconciles payment attempts.
The names are less important than the contracts. tryHold must be an atomic operation, not a convenient name for a separate read followed by an unconditional write. Dependency injection can help test the payment boundary; it does not make a database update atomic.
Specify the seat lifecycle
A confirmed booking is terminal in this simplified model. Refunds and cancellations after confirmation would need their own policy and transitions.
The transition from Held to Confirmed must check the hold ID, its owner, its expiry, and the associated payment result. If an expiry worker and a confirmation handler race, both must use conditional updates against the same authoritative record. A stale job must never release a newer hold.
Put the invariant in the database
For a PostgreSQL implementation, one possible schema and reservation operation are:
CREATE TABLE event_seats (
event_id bigint NOT NULL,
seat_id bigint NOT NULL,
state text NOT NULL DEFAULT 'available'
CHECK (state IN ('available', 'held', 'confirmed')),
hold_id uuid,
held_by bigint,
hold_expires_at timestamptz,
PRIMARY KEY (event_id, seat_id)
);
UPDATE event_seats
SET state = 'held',
hold_id = $3,
held_by = $4,
hold_expires_at = statement_timestamp() + interval '5 minutes'
WHERE event_id = $1
AND seat_id = $2
AND (
state = 'available'
OR (state = 'held' AND hold_expires_at <= statement_timestamp())
)
RETURNING hold_id, hold_expires_at;This example assumes PostgreSQL’s default Read Committed isolation. When concurrent updates compete for the same row, an updater waits and re-evaluates its condition against the updated row. Only the eligible transition succeeds. If no row is returned, the application should report a reservation conflict rather than pretend the hold exists. Under stricter isolation levels, also handle transaction serialization failures.
The primary key gives each event-seat pair one authoritative row. The conditional update enforces this particular transition. A complete implementation also needs record-shape constraints, authorization, payment records, booking uniqueness, and idempotency storage; this snippet demonstrates the reservation mechanism only.
PostgreSQL documents the concurrent-update behavior in its transaction isolation reference. Its explicit locking guide explains row locks in more detail.
Define repeatable requests and uncertain outcomes
Suppose a reservation succeeds, but the HTTP response is lost. The client retries. Without request idempotency, a new request might see its own seat as unavailable and incorrectly report failure.
Persist the request key, a fingerprint of its inputs, and the result with the reservation transaction. A retry with the same key and inputs returns the original result. Reusing the key with different inputs should fail validation.
Idempotency does not mean “ignore all duplicates.” It means the application defines which operations are the same and preserves their outcome. Payment providers have their own key scope and retention rules; check those before depending on them. Stripe’s idempotent requests documentation, for example, describes how it handles repeat requests.
How to approach each interview
Confirm the expected scope before committing to a diagram. “System design” may mean a distributed architecture discussion, an object-oriented design exercise, or a mix. There is no universal interview format.
Clarify the outcome
Ask which user flow matters and what must never go wrong. For booking: assigned seats or general admission, hold duration, expected peak traffic, and the no-double-booking rule.
Choose the right zoom level
For HLD, show ownership, read and write paths, and dependencies. For LLD, define contracts, state transitions, and the data that supports them. Explain your starting point to the interviewer.
Walk one request all the way through
Follow a reservation from the client to durable storage and back. State which operation is atomic, where timeouts occur, and how the caller learns the outcome.
Test the design with a failure
Try a duplicate request, a late webhook, or two users selecting the same seat. A good answer explains what the system does next and why the invariant still holds.
Use this checklist when reviewing your answer:
| If the conversation is about HLD | If the conversation is about LLD |
|---|---|
| Can I justify each major component? | Can I describe each operation’s inputs and outcomes? |
| Have I separated read and write requirements? | Are legal and illegal state transitions explicit? |
| Do I know which system owns each fact? | Is concurrency handled at the authoritative store? |
| Have I identified hot spots and failure boundaries? | Do retries and duplicate events preserve correctness? |
| Can I explain what changes at higher load? | Can I name tests that could disprove my design? |
Five mistakes that weaken a design
- Drawing microservices before defining responsibilities. A box is not automatically a separate service. Start with ownership and split deployment boundaries when there is a reason.
- Using a cache as the source of truth for reservations. A fast, stale read cannot decide exclusive ownership.
- Treating a timeout as a failed payment. The remote operation may have succeeded. Reconcile the outcome.
- Replacing behavior with pattern names. Saying “Repository” or “Strategy” does not specify atomicity, errors, or state transitions.
- Stopping at the happy path. Ask what happens after response loss, expiry races, repeated webhooks, and partial outages.
A useful design lets another engineer predict the system’s behavior when something goes wrong.
Frequently asked questions
Is LLD just writing classes?
No. Classes are one way to structure implementation. Functional modules, database constraints, algorithms, API contracts, and state machines can all be part of low-level design. The goal is precise behavior, regardless of programming style.
Should HLD always come before LLD?
An architectural sketch is often a helpful starting point, but design is iterative. A concurrency problem discovered during LLD can change a service boundary or storage choice in HLD. Move between the two as evidence changes.
Do API contracts and database schemas belong to HLD or LLD?
Both can discuss them. HLD may establish the API’s responsibility and which service owns a database. LLD spells out request fields, status codes, indexes, constraints, and transaction behavior. Detail and purpose determine the level.
Which should I learn first for interviews?
Use the role’s interview format to set your emphasis. Learn enough architecture to explain how a request flows, and enough implementation design to explain how one component preserves its guarantees. Practice the same problem at both levels.
Practice both levels on one problem
Sketch a ticket booking architecture in ten minutes. Then choose one operation, reserve, and specify the atomic transition, response contract, and retry behavior.
Check your design against these cases:
- Two users reserve the same seat concurrently: exactly one active hold wins.
- A successful reservation response is lost: the retry returns the same result.
- An old expiry job runs after a new hold: it cannot release the new owner’s seat.
- A payment webhook arrives twice: there is one booking and no repeated transition.
- A successful payment arrives after expiry: the compensation policy runs.
Build the concepts behind those decisions with database transactions, idempotency, and the outbox pattern. For object-oriented design practice, visit Low Level Design Mastery.
HLD connects the responsibilities. LLD makes each responsibility precise. You understand the system when you can move between the diagram and the behavior without losing the guarantee you promised.