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.

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:
- Search for an event
- View an event
- 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 bookingThis 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 ──→ A10We cannot allow:
A10 → User A
A10 → User BThere 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 consistencyThis 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 bookThe 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
UserThe 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
BOOKEDThat 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/searchPossible parameters:
?q=edsheeran
&location=mumbai
&type=concert
&date=2026-09-10It 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
↓
BOOKEDFor example:
10:00:00
User selects A10A10 → RESERVED
The user now gets, say, 10 minutes to complete payment.
If payment succeeds:
RESERVED → BOOKEDIf the user disappears:
RESERVED → AVAILABLEThis 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}/reserveand later:
POST /tickets/{ticketId}/confirmNotice 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 = 123to 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
│ │ │
└────────────┼────────────┘
↓
PostgreSQLThe 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:
A10 → RESERVEDForever.
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 = RESERVEDwhile 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 → AVAILABLEThis is much cleaner.
But there is a problem.
Suppose a reservation expires at:
12:00and our cron job runs at:
12:09The 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 secondsAfter 600 seconds, Redis automatically removes the key.
So:
User selects A10
↓
Redis locks A10
↓
TTL = 10 minutes
↓
User pays
↓
PostgreSQL → BOOKEDOr:
User selects A10
↓
Redis locks A10
↓
User leaves
↓
10 minutes pass
↓
Redis automatically removes lock
↓
A10 becomes availableNo 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 4If 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 3This 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 DelhiThe search index can maintain relationships roughly like:
"Ed Sheeran"
↓
Event 1
Event 3
"Mumbai"
↓
Event 1
Event 2Now a search for:
Ed Sheeran + Mumbaican 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 IndexThe 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 IndexBut 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 IndexWhenever 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 resultsA 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 BOOKEDBut one second later, somebody else books A2.
The user’s screen still says:
A2 → AVAILABLEThey 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 A10 → RESERVED
↓
Server
↓
SSE connection
↓
Browser
↓
A10 becomes unavailableSSE is useful when communication mainly needs to go:
Server → ClientWebSockets 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 seatsYou 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 Servicewe introduce:
1,000,000 users
↓
Virtual Waiting Queue
↓
Small batch of users
↓
Booking SystemThe queue acts as a choke point.
For example:
1,000,000 users waiting
↓
Allow 1,000 users
↓
They book
↓
Allow next 1,000
↓
ContinueRedis 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 3We 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:
eventIdbecause 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
Performerwhile 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 + LockThis 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
│
↓
OpenSearchAnd for massive events:
Users
↓
Virtual Waiting Queue
↓
Allowed Users
↓
Booking Service
↓
Redis Lock
↓
PostgreSQLThis 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 RequirementsAnd 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