System Design Interviews, Step by Step · Classic Walkthroughs, Part 2

Capstone: Design a Ticket Booking System

Your turn: design concert ticket sales where a million fans arrive at once for 50,000 seats. Work through the six steps yourself, then compare with the model answer and rubric for each part.

Create a free account to save this page, take notes and track your progress. Sign in

The brief

"Design a ticketing system for big concerts. Sales open at 10:00; up to 1 million people try to buy 50,000 seats in the first minutes. Users pick specific seats, hold them for 10 minutes while paying, and must never be sold a seat someone else has."

Give yourself 45 minutes and run the six steps from the first lesson. Write your answers down, then open each question below to compare.

Things to decide

  1. Requirements and what's out of scope.
  2. Estimates: requests per second at 10:00, and what that means for the design.
  3. API and data model, including how a seat is held.
  4. The high-level design.
  5. Deep dives: preventing double booking; surviving the 10:00 spike; holds that expire.
  6. Trade-offs you'd mention.

A model to experiment with

Seat holds are the heart of the problem. This model shows a conditional update pattern: a hold succeeds only if the seat is free or its previous hold has expired, which is the same rule a database would enforce in one atomic statement.

Seat holds that expire, with no double bookingTry it yourself
HOLD_SECONDS = 600


class Seats:
    def __init__(self, seat_ids):
        self.state = {s: {"status": "free", "holder": None, "until": 0} for s in seat_ids}

    def hold(self, seat, user, now):
        # UPDATE seats SET status='held', holder=?, until=now+600
        #  WHERE id=? AND (status='free' OR (status='held' AND until < now))
        s = self.state[seat]
        if s["status"] == "free" or (s["status"] == "held" and s["until"] < now):
            s.update(status="held", holder=user, until=now + HOLD_SECONDS)
            return True
        return False

    def buy(self, seat, user, now):
        # UPDATE seats SET status='sold' WHERE id=? AND holder=? AND status='held' AND until >= now
        s = self.state[seat]
        if s["status"] == "held" and s["holder"] == user and s["until"] >= now:
            s["status"] = "sold"
            return True
        return False


seats = Seats(["A1", "A2"])
print("ana holds A1:", seats.hold("A1", "ana", now=0))
print("ben tries A1:", seats.hold("A1", "ben", now=5))
print("ben holds A2:", seats.hold("A2", "ben", now=5))
print("ana pays for A1:", seats.buy("A1", "ana", now=120))
print("ben never pays; cat tries A2 after 10 min:", seats.hold("A2", "cat", now=700))
print("ben pays too late:", seats.buy("A2", "ben", now=710))
print("final:", {k: (v["status"], v["holder"]) for k, v in seats.state.items()})

Output

ana holds A1: True
ben tries A1: False
ben holds A2: True
ana pays for A1: True
ben never pays; cat tries A2 after 10 min: True
ben pays too late: False
final: {'A1': ('sold', 'ana'), 'A2': ('held', 'cat')}

What's next

Deliverables

Requirements and estimates: what did you assume, and what do the numbers tell you?

Model answer

Functional: browse events, view a seat map, hold seats for 10 minutes, pay, get a ticket; queue fairly at peak. Non-functional: no seat sold twice (strong consistency for seats), stay up during the spike, fair ordering. Estimates: 1M users in a few minutes, each refreshing the seat map and trying holds, means tens of thousands of requests per second at 10:00, nearly all of which can't succeed (only 50,000 seats). So the design must absorb and shape the spike rather than let it hit the seat database.

Rubric

Strong answer: separates the read-heavy browsing from the contested writes, quantifies the spike, and concludes that a waiting room / admission control is needed. Weak answer: plans for average load or skips estimates.

How do you prevent two people from booking the same seat?

Model answer

Make the hold an atomic conditional update in a strongly consistent store: set a seat to 'held by user X until T' only if it's free or its hold has expired (UPDATE ... WHERE status='free' OR until < now), and sell it only if X still holds it and the hold hasn't expired. Unique constraints or row locks give the same guarantee. Expired holds become available automatically; a background job can also release them.

Rubric

Strong answer: an atomic compare-and-set or row lock at the database (not 'check, then write' in app code), hold expiry, and idempotent payment for the purchase step. Weak answer: relies on a cache or app-level checks that race.

How does the system survive 1 million people arriving at 10:00?

Model answer

Put a virtual waiting room in front: users get a queue position (served from a CDN/edge-friendly page and a lightweight queue service), and only a controlled number per second are admitted to the booking flow, sized to what the seat service and payment system can handle. Cache the event and seat-map pages aggressively (with short TTLs or pushed availability updates), rate-limit per user and per IP, and block bots. Payments go through the idempotent payment flow.

Rubric

Strong answer: admission control (waiting room), caching of read paths, rate limiting and bot defence, with clear reasoning about fairness. Weak answer: 'add more servers' or autoscaling alone, which can't scale a contested seat inventory.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?