Design Ticketmaster
Patterns, Technologies, and Concepts
Key Takeaways
Requirements
You demonstrated a strong grasp of non-functional requirements for a ticketing system, correctly identifying consistency, low latency, and burst traffic handling as the key challenges.
-
In a ticketing system, apply strong consistency (not eventual consistency) to the booking flow to prevent double-selling seats. Use optimistic locking or database transactions to enforce this. Apply high availability with eventual consistency to the search/browse flow since a slightly stale seat count is acceptable and availability matters more there.
-
Ticketmaster-style systems face extreme burst traffic when popular events go on sale. A common pattern is to use a virtual waiting queue in front of the checkout service so that instead of hammering your backend with 1 million concurrent requests, you meter users into the system at a controlled rate your infrastructure can handle.
Core Entities
You demonstrated a strong grasp of core data modeling for a ticketing system, correctly identifying all the key entities and their relationships on your first attempt.
-
In a ticketing system, the three core entities are Event, Ticket, and User. Event holds details like date and venue, Ticket links a User to an Event and tracks seat or availability status, and User stores account and payment info. These three form the foundation everything else builds on.
-
Supporting entities like Venue, Performer, and Booking add important domain context. Venue lets you model capacity and seating layouts, Performer lets you associate acts with events, and Booking acts as a transaction record that ties a User purchase to one or more Tickets. Including these shows you understand real-world ticketing workflows.
API
You started with a solid API design and improved it quickly after feedback, showing good instincts overall with only minor gaps around REST resource naming and request body conventions.
-
When designing a booking endpoint, use a path like POST /bookings rather than POST /events/:eventId/tickets. The URL should name the resource being created, and a booking is its own resource, not a ticket. Using /tickets implies you are creating new tickets, which confuses the intent.
-
Pass identifiers in request bodies, not full objects. For example, send ticketIds as an array of strings rather than full Ticket objects. The server already has the ticket data, so sending the full object is redundant and adds unnecessary payload size.
-
Include a payment reference like a Stripe paymentIntentId in your booking request body. This lets the server confirm payment and create the booking in a single call, rather than requiring a separate payment step and making the flow more complex.
-
Be careful about returning unbounded arrays in a single endpoint response. For example, returning all available tickets inside a GET /events/:eventId response could return thousands of items for a popular event. Consider paginating that data or fetching it from a separate endpoint like GET /events/:eventId/tickets.
High Level Design
You demonstrated solid system design fundamentals across all three attempts, with consistently clean flows and good service separation, though you left some small but important details unspoken that would make your designs feel more complete and trustworthy.
-
When describing a read path that pulls from multiple tables like event, venue, and performer, explicitly say whether you are using a SQL JOIN or separate queries. A JOIN is the default choice here because it fetches all related data in one round trip, which is simpler and faster for a details page that always needs all three pieces together.
-
Search endpoints should return lightweight result cards, not full object payloads. For example, a search result for an event should return only id, name, date, and a venue summary so the client can render a list quickly. Full event details are only fetched when the user clicks into a specific event.
-
When a seat booking involves checking availability and then updating a ticket status, those two steps must happen inside a single database transaction. Without a transaction, two users could both pass the availability check at the same time and both get charged for the same seat. The transaction makes the check and the update atomic so only one can succeed.
-
Each physical seat should be its own row in a Ticket table with a unique seat identifier tied to a specific event. This lets the backend map a user’s seat map selection directly to one row, check its status, and update it atomically. Using a single inventory count instead would make it impossible to reason about which exact seat a user is buying.
Deep Dives
You demonstrated strong instincts throughout this section and improved your answers with each iteration, with the one clear gap being how SSE fan-out actually works across multiple horizontally scaled server instances.
-
When you horizontally scale a service that holds SSE connections, each instance only knows about its own connected clients. To fan out a message to all clients across all instances, each instance must subscribe to a shared Redis Pub/Sub channel. When a message is published to that channel, every subscribed instance receives it and pushes it down its own open SSE connections. Without this pattern, clients connected to instance B will never see updates published by a booking event handled by instance A.
-
SELECT FOR UPDATE acquires a row-level lock in the database before you read and update a row. This means if two users try to reserve the same seat at the same time, the second transaction blocks until the first one commits. Once the first commits and marks the seat as reserved, the second transaction unblocks, reads the updated status, and fails gracefully. Always wrap this in a short transaction that commits immediately after the status update so the lock is held for as little time as possible.
-
Redis sorted sets store members with a numeric score and keep them ordered by that score automatically. For a waiting room queue, you use arrival timestamp as the score so you get fairness for free and can look up a user’s rank in O(log N) time. A simple Redis list gives you FIFO order too, but a sorted set lets you do explicit rank lookups and timestamp-based ordering, which is why it is the better choice when you need to show users their queue position.
-
SSE is a one-way server-to-client push protocol over a regular HTTP connection, while WebSockets are bidirectional. For a live seat map, you only need the server to push updates to the client, so SSE is the right fit. It is simpler to implement, works over HTTP/2, and avoids the overhead of maintaining a full duplex channel when you never need the client to send data back on that same connection.