Skip to content
System designInterview prepArchitecture

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.

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.

DimensionHigh-level designLow-level design
1. Main questionHow do the parts work together?How does this part behave correctly?
2. ScopeServices, storage, integrations, deploymentModules, objects, functions, records
3. RequirementsCapacity, latency, availability, consistencyInvariants, validation, errors, edge cases
4. Typical artifactsArchitecture and data-flow diagramsClass and sequence diagrams, contracts, pseudocode
5. Data decisionsOwnership, access patterns, partitioningFields, indexes, constraints, transaction boundaries
6. Failure handlingTimeouts, isolation, retries, recoveryExceptions, idempotency checks, safe state transitions
7. EvidenceEstimates and architectural tradeoffsExecutable 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:

  1. At most one active hold or confirmed booking may own a seat for an event.
  2. A hold expires after five minutes unless it is confirmed.
  3. 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.

Rendering Mermaid diagram...
Scroll horizontally to explore the diagram →

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.

Rendering Mermaid diagram...
Scroll horizontally to explore the diagram →

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

Rendering Mermaid diagram...
Scroll horizontally to explore the diagram →

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:

CODE EXAMPLE
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.

Rendering Mermaid diagram...
Scroll horizontally to explore the diagram →

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 HLDIf 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

  1. 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.
  2. Using a cache as the source of truth for reservations. A fast, stale read cannot decide exclusive ownership.
  3. Treating a timeout as a failed payment. The remote operation may have succeeded. Reconcile the outcome.
  4. Replacing behavior with pattern names. Saying “Repository” or “Strategy” does not specify atomicity, errors, or state transitions.
  5. 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.

Filed under System design · Updated Sep 12, 2026
Architecture

URL Shortener System Design: From Short Codes to Scale

Design a URL shortener with capacity estimates, Base62 IDs, collision handling, redirect caching, editable links, and asynchronous analytics.

Read the guide
Databases

SQL vs NoSQL: How to Choose in a System Design Interview

Choose SQL or NoSQL using real access patterns, data models, consistency requirements, and an online-store example with diagrams and decision tables.

Read the guide
YOUR NEXT STEP

Turn the diagram into a decision.

See how system design concepts work, one visual lesson at a time.

Start exploring ↗
← Back to the blog