How Would You Design Ticketmaster? A System Design Interview Breakdown

If you think system design interviews are about drawing boxes and naming Kafka, Redis, and microservices, you are probably preparing the wrong way.

How Would You Design Ticketmaster? A System Design Interview Breakdown

Imagine this.

A Ed Sheeran concert is about to go live.

10,000 seats are available.

A few seconds before the sale starts, 1 million people open the website.

Everyone searches for the same event.

Everyone sees the same seats.

Thousands of people click the same seat.

And now the system has one very simple question to answer:

Who gets the seat?

Getting this wrong is not just a performance problem. It can mean two users getting the same ticket.

That is what makes a ticket-booking system such an interesting system design problem.

And the surprising part is that the hardest part is not searching for events or displaying a seat map.

The hardest part is handling consistency, temporary reservations, sudden traffic spikes, and a huge number of reads without making the system unnecessarily complicated.

A good way to approach this problem is not to start drawing microservices.

Start with the user.

Step 1: Understand What the User Actually Needs

Before thinking about databases or caches, write down the core user actions.

For a basic ticket-booking platform, there are three:

  1. Search for an event
  2. View an event
  3. Book a ticket

That’s it.

You could add payments, notifications, recommendations, reviews, admin dashboards, refunds, and many other features.

But if the interviewer asks you to design the ticket-booking flow, don’t immediately build the entire company.

Focus on the core problem.

A simple user journey looks like this:

User

Search for an event

Open event

View seats

Select a seat

Reserve seat

Make payment

Confirm booking

This flow already tells us a lot about the system.

The Most Important Part: Non-Functional Requirements

This is where many system design interviews go wrong.

Candidates often say:

“The system should be scalable, highly available and reliable.”

Technically, that’s true.

But it doesn’t tell the interviewer anything useful.

Instead, ask:

What is special about this particular system?

For ticket booking, there are a few very important answers.

1. Booking needs strong consistency

Suppose seat A10 is available.

Two users click it at almost exactly the same time.

User A ──→ A10
User B ──→ A10

We cannot allow:

A10 → User A
A10 → User B

There can only be one winner.

This is the classic double-booking problem.

So the booking part of our system needs strong consistency.

2. Search needs high availability

Now compare that with searching.

Suppose an administrator adds a new event.

If someone doesn’t see that event for the next few seconds, that’s usually acceptable.

The system should still respond quickly.

So we can make an important distinction:

Search / View Events

High availability

Booking

Strong consistency

This is a much better system-design answer than simply saying:

“The system prefers consistency.”

Different parts of the system can have different requirements.

There Is Another Problem: Traffic Is Not Constant

Most ticket sales are relatively normal.

But occasionally, something huge happens.

Think about:

  • A major concert
  • The Super Bowl
  • The World Cup
  • A very popular movie release

Suddenly, millions of users may arrive at the same time.

So our scalability requirement isn’t simply:

“The system should scale.”

It is:

The system should handle sudden traffic spikes for extremely popular events.

That’s a much more useful requirement.

One More Observation: Reads Are Much Higher Than Writes

Think about the number of people searching for an event.

Maybe:

1000 people

search/view event

10 people

actually book

The exact numbers will vary, but the important idea is:

Reads are much more frequent than successful bookings.

That affects almost every design decision we make later.

Step 2: Identify the Core Data

Before designing APIs, identify the important entities.

For our system, we can start with:

Event
Venue
Performer
Ticket
User

The most interesting one is Ticket.

A simplified ticket might look like:

{
id: "T123",
eventId: "E100",
seat: "A10",
price: 5000,
status: "AVAILABLE"
}

The status can eventually become:

AVAILABLE
RESERVED
BOOKED

That tiny status field is actually going to become very important.

Step 3: Design the APIs

Now map APIs to the functionality we identified.

Search events

GET /events/search

Possible parameters:

?q=edsheeran
&location=mumbai
&type=concert
&date=2026-09-10

It returns lightweight event information.

View an event

GET /events/{eventId}

The response might contain:

{
event: {...},
venue: {...},
performer: {...},
tickets: [...]
}

The client can use this information to display the event page and seat map.

Booking Is Different

Here’s where things become interesting.

Booking is actually a two-step process.

You don’t immediately turn a seat into a booked seat.

Instead:

AVAILABLE

RESERVED

BOOKED

For example:

10:00:00
User selects A10
A10 → RESERVED

The user now gets, say, 10 minutes to complete payment.

If payment succeeds:

RESERVEDBOOKED

If the user disappears:

RESERVED → AVAILABLE

This is a common pattern in ticketing and other reservation systems.

Think about airline seats, hotel rooms, restaurant tables, and similar systems.

The API Can Reflect This Two-Step Flow

We can have:

POST /tickets/{ticketId}/reserve

and later:

POST /tickets/{ticketId}/confirm

Notice something important here.

We don’t need the client to send:

{
userId: "123"
}

The server should already know who the user is through authentication information such as a session or JWT.

Otherwise, a malicious client could simply change:

userId = 123

to another user’s ID.

That’s a small detail, but these small details often show good engineering thinking.

The First Version of the Architecture

Now we can finally start drawing boxes.

A simple architecture could look like this:

 ┌──────────────┐
│ Client │
└──────┬───────┘


┌──────────────┐
│ API Gateway │
└──────┬───────┘

┌────────────┼────────────┐
↓ ↓ ↓
Event Service Search Booking
│ Service Service
│ │ │
└────────────┼────────────┘

PostgreSQL

The API Gateway can handle things like:

  • Routing
  • Authentication
  • Rate limiting

The services then handle their specific responsibilities.

But don’t over-engineer this immediately.

A useful system design principle is:

Start simple. Add complexity only when you can explain what problem it solves.

Why PostgreSQL?

For the core ticket data, a relational database such as PostgreSQL is a reasonable choice.

Why?

Because booking needs strong consistency and transactions.

Imagine two users trying to purchase the same ticket.

We want the database to guarantee that only one transaction successfully changes the ticket from available to booked.

The important question in an interview isn’t:

“SQL or NoSQL?”

A better question is:

What properties does my database need?

Here, we care about:

  • Transactions
  • Consistency
  • Relationships between entities
  • Reliable storage

PostgreSQL satisfies those requirements.

Another database could potentially work too.

The technology name is less important than the reasoning behind the choice.

The First Big Bug: Reservations Never Expire

Suppose we implement reservation like this:

ticket.status = "RESERVED";

The user gets 10 minutes.

But what happens if they:

  • Close their laptop?
  • Lose internet?
  • Abandon checkout?
  • Simply change their mind?

Nothing.

Our database still says:

A10RESERVED

Forever.

Now imagine another user looking at the seat map.

A10 appears unavailable.

But nobody actually owns it anymore.

That’s a bug.

Solution #1: Store the Reservation Time

We could add:

{
status: "RESERVED",
reservedAt: "10:00:00"
}

Then when looking for available tickets:

SELECT *
FROM tickets
WHERE status = 'AVAILABLE'
OR (
status = 'RESERVED'
AND reservedAt < NOW() - INTERVAL '10 minutes'
);

This works.

But now the database logic becomes more complicated.

A ticket can have:

status = RESERVED

while logically it is already available because the reservation expired.

That makes the data model harder to reason about.

Solution #2: Run a Cron Job

Another option is a scheduled job.

Every few minutes:

Find RESERVED tickets

Check reservation time

Older than 10 minutes?

Change status → AVAILABLE

This is much cleaner.

But there is a problem.

Suppose a reservation expires at:

12:00

and our cron job runs at:

12:09

The ticket was supposed to be available for 9 minutes, but our system still considers it reserved.

That’s a delay.

For some systems, this may be acceptable.

For a high-demand ticketing system, we can do better.

Solution #3: Use a Distributed Lock With TTL

This is where Redis becomes useful.

Instead of permanently storing the temporary reservation in PostgreSQL, we can store it in Redis with a TTL (Time To Live).

For example:

ticket:T123 → RESERVED
TTL → 600 seconds

After 600 seconds, Redis automatically removes the key.

So:

User selects A10

Redis locks A10

TTL = 10 minutes

User pays

PostgreSQL → BOOKED

Or:

User selects A10

Redis locks A10

User leaves

10 minutes pass

Redis automatically removes lock

A10 becomes available

No cron job.

No periodic cleanup.

No stale reservation.

Why Can’t We Just Keep the Lock in Memory?

Because our service will have multiple instances.

Imagine:

Booking Service 1
Booking Service 2
Booking Service 3
Booking Service 4

If the lock exists only inside Service 1’s memory, Service 2 won’t know about it.

That’s dangerous.

Redis gives all service instances a shared view:

 ┌──────────────┐
Redis
Ticket Locks
└──────┬───────┘

┌──────────┼──────────┐
↓ ↓ ↓
Service 1 Service 2 Service 3

This is why a distributed lock is useful here.

But What If Redis Goes Down?

This is a very good follow-up question.

Suppose Redis crashes.

Some reservations may disappear.

Two users could potentially reach the payment stage believing they have the same seat.

But the final booking still goes through the strongly consistent database.

So PostgreSQL becomes the final source of truth.

One user successfully books.

The other gets an error.

That’s not a great user experience, but we have preserved the most important guarantee:

The same ticket cannot be successfully booked twice.

This is an important system-design trade-off.

Not every failure can be made invisible.

Sometimes the goal is to make sure the system fails safely.

Now Let’s Fix Search

Our first search implementation might look like:

SELECT *
FROM events
WHERE name LIKE '%edsheeran%';

It works.

But it can become painfully slow at scale.

Why?

Because the database may need to scan a large number of rows to find matching text.

We need something designed specifically for search.

That’s where a search engine such as Elasticsearch/OpenSearch can help.

How Does a Search Engine Help?

Instead of repeatedly scanning the entire database, a search engine creates indexes.

Imagine these events:

Event 1 → Ed Sheeran Mumbai
Event 2 → Coldplay Mumbai
Event 3 → Ed Sheeran Delhi

The search index can maintain relationships roughly like:

"Ed Sheeran"

Event 1
Event 3

"Mumbai"

Event 1
Event 2

Now a search for:

Ed Sheeran + Mumbai

can quickly narrow down the relevant events.

Search engines can also support things such as:

  • Text search
  • Filters
  • Dates
  • Locations
  • Geospatial queries

But Should Elasticsearch Be Our Primary Database?

Usually, no.

The primary database should remain the durable source of truth.

We can think of the architecture as:

 PostgreSQL
/ \
/ \
Source of truth \

Search Index

The search index is optimized for reading and searching.

PostgreSQL stores the actual data.

How Do We Keep Them in Sync?

One simple approach:

Update Event

PostgreSQL

Search Index

But what happens if PostgreSQL succeeds and Elasticsearch fails?

Now the two systems disagree.

One common solution is Change Data Capture (CDC).

Conceptually:

PostgreSQL

Change Event

Stream

Worker

Search Index

Whenever important data changes, the change can be propagated to the search system.

The exact implementation can vary, but the important idea is:

Keep the primary database as the source of truth and propagate changes to the search index.

What About Millions of People Searching for the Same Event?

Suppose Ed Sheeran tickets go on sale.

Millions of users search:

Ed Sheeran The search system might receive enormous numbers of identical requests.

Caching can help.

For example:

"Ed Sheeran"

Cache

Search results

A CDN can also cache responses for short periods if the results are suitable for caching.

But caching works best when many users request the same thing.

If every query contains highly specific parameters, cache hits become less likely.

And if the product later introduces personalized search results, blindly caching the response for everyone would no longer work.

Again, the important thing is not simply:

“Use Redis.”

The important question is:

Are the results identical enough that caching makes sense?

The Seat Map Has Another Problem

Imagine the user opens an event page.

They see:

A1 AVAILABLE
A2 AVAILABLE
A3 AVAILABLE
A4 BOOKED

But one second later, somebody else books A2.

The user’s screen still says:

A2 → AVAILABLE

They click it.

The server says:

Sorry, someone already took that seat.

That’s frustrating.

For popular events, this can happen constantly.

So we may want the seat map to update in near real time.

Option 1: Long Polling

The client sends a request and keeps it open for some time.

The server can respond when there is an update.

Then the client asks again.

This is relatively simple.

For pages where users spend a short amount of time, this may be perfectly reasonable.

Option 2: Server-Sent Events

If we want a persistent connection where the server can push updates to the browser, Server-Sent Events (SSE) are another option.

For example:

Seat A10RESERVED

Server

SSE connection

Browser

A10 becomes unavailable

SSE is useful when communication mainly needs to go:

ServerClient

WebSockets are bidirectional, but we don’t necessarily need bidirectional communication for this particular use case.

Again:

Don’t choose the most complicated technology. Choose the simplest technology that satisfies the requirement.

Now Comes the Real Traffic Problem

Everything above can work reasonably well.

Until a huge event goes live.

Imagine:

1,000,000 users

10,000 seats

You don’t want one million users hitting the booking services simultaneously.

Even if your backend can technically handle the load, the user experience can become terrible.

People may see seats disappear instantly.

Requests may fail.

Retries can create even more traffic.

The solution is surprisingly simple.

Build a Virtual Waiting Queue

Instead of:

1,000,000 users

Booking Service

we introduce:

1,000,000 users

Virtual Waiting Queue

Small batch of users

Booking System

The queue acts as a choke point.

For example:

1,000,000 users waiting

Allow 1,000 users

They book

Allow next 1,000

Continue

Redis can be used to implement the queue.

The exact ordering strategy can vary.

It could be:

  • First-come-first-served
  • Randomized
  • Priority-based

The product requirements decide what is fair.

This Is One of My Favorite System Design Lessons

Notice what happened.

We didn’t make the backend infinitely powerful.

Instead, we controlled how many users were allowed to reach it.

That’s often a better engineering solution.

Sometimes:

The best way to scale a system is not to process more traffic.

It’s to control the traffic before it reaches the expensive part of the system.

What About Scaling the Database?

Now we can think about traditional scaling techniques.

Our services can scale horizontally:

 Load Balancer

┌─────────┼─────────┐
↓ ↓ ↓
Service 1 Service 2 Service 3

We can add more instances as traffic increases.

For the database, if the data becomes large enough, sharding may become relevant.

But don’t blindly say:

“We’ll shard the database.”

First ask:

Why do we need sharding?

Maybe the data fits comfortably on one PostgreSQL instance.

Maybe we need more read capacity.

Maybe read replicas are enough.

Maybe caching removes most of the read traffic.

Architecture should follow the problem.

And This Is Where Back-of-the-Envelope Math Actually Helps

A common interview habit is to start calculating:

Users = 10 million
QPS = 50,000
Storage = 20 TB
...

Then conclude:

“This is a very large system.”

But what did we learn?

Not much.

A better approach is to calculate when the answer can change your architecture.

For example:

Can our ticket data fit on one database?

If yes, don’t shard just because sharding sounds impressive.

If no, then ask:

What should we shard by?

Maybe:

eventId

because most ticket queries are event-specific.

The math should answer a design question.

Not simply exist for the sake of having numbers.

We Can Also Cache Event Data

Remember our earlier observation?

Reads are much more common than writes.

Events, venues, and performers usually don’t change very often.

Tickets are different.

Tickets change frequently.

So we can cache relatively static data:

Redis
Event
Venue
Performer

while keeping dynamic ticket availability in the database/lock system.

That means viewing an event can become much cheaper.

Instead of repeatedly asking the database for everything:

Event details → Cache
Venue details → Cache
Performer → Cache
Tickets → Database + Lock

This is a very useful caching pattern:

Cache data that is expensive to read and changes infrequently.

Putting Everything Together

A simplified architecture now looks something like this:

 ┌───────────────┐
│ Client │
└───────┬───────┘

┌───────────────┐
│ API Gateway │
└───────┬───────┘

┌──────────────────┼──────────────────┐
↓ ↓ ↓
Event Service Search Service Booking Service
│ │ │
↓ ↓ ↓
Redis OpenSearch Redis Lock
│ │
↓ ↓
PostgreSQL ←──────────────────────────────┘


CDC / Stream


OpenSearch

And for massive events:

 Users

Virtual Waiting Queue

Allowed Users

Booking Service

Redis Lock

PostgreSQL

This design is not about adding every technology you know.

Each component exists because it solves a specific problem.

The Bigger System Design Lesson

If I were preparing for a system design interview, I would not memorize this architecture.

I’d memorize the thinking process.

Start here:

Requirements

Core Entities

APIs

Simple Architecture

Find Bottlenecks

Deep Dive

Optimize

Validate Requirements

And every time you add a component, ask:

What problem does this solve?

Redis?

Temporary distributed locks, caching, queues.

Search engine?

Fast text and filtered search.

SSE?

Push seat updates to clients.

Waiting queue?

Protect backend services from massive traffic spikes.

PostgreSQL?

Durable, strongly consistent source of truth for booking.

Once you start thinking this way, system design becomes much less about drawing boxes and much more about making engineering trade-offs.

What Would You Do Differently?

Now imagine you’re in the interview.

You have designed the system.

The interviewer asks:

“What happens if Redis goes down?”

Or:

“What if 10 million users search for the same concert?”

Or:

“What if a user reserves a ticket and never pays?”

Or:

“What happens if two people click the same seat at exactly the same time?”

These are not random follow-up questions.

They are testing whether you understand the weaknesses of your own design.

And honestly, that’s one of the biggest differences between memorizing system design and actually understanding it.

Can you find the failure points before the interviewer does?

Final Thought

The interesting thing about designing a ticket-booking system is that the basic version is actually pretty easy.

  • Search.
  • View.
  • Book.

A few APIs.

A database.

Done.

The real engineering starts when reality enters the picture.

  • What if two users want the same seat?
  • What if someone abandons checkout?
  • What if the search database becomes slow?
  • What if one million people arrive at once?
  • What if the cache disappears?
  • What if the seat map becomes stale?

Those questions force us to introduce consistency, TTL-based locks, search indexes, caching, real-time updates, and virtual waiting queues.

And that’s the mindset I would carry into any system design interview:

Don’t start by asking, “Which technology should I use?”

Start by asking:

“What problem am I trying to solve?”

Then choose the simplest architecture that solves it.

That’s much closer to how real software engineering works.

And if your system can explain why every box exists, you’re probably doing system design the right way.

From Tech By Neha Gupta

  • 👏 Enjoyed the article? Don’t forget to leave a clap.
  • 💬 Have thoughts or questions? Share them in the comments.

Before you go

  • Please take a moment to like the post and follow the writer!
  • Did you know that over 400,000 developers share what they’re building, learning, and discovering across our platforms every month? Learn how you can contribute here