System design interview guide
Booking Waitlist System Design
A sold-out concert has a huge waitlist. When one ticket is cancelled, the system must offer it fairly, give one user a time-limited window, handle notification delay, and prevent two waitlisted users from accepting the same spot.

Problem statement
Design a waitlist for a sold-out booking product. Users join a queue, cancellations release capacity, the system offers the spot to the next eligible user, the offer expires if ignored, and accepting the offer must safely create one booking without skipping people unfairly.
Introduction
A concert sells out in minutes. Thousands of people join the waitlist and watch their place move slowly. Three days later, one ticket is cancelled. The system sends an offer to the next person, but the SMS arrives late. Meanwhile, the user after them opens the app and sees an offer too. If both can accept the same ticket, the waitlist has failed at the only thing users care about: fairness.
A booking waitlist is not just a queue. It is a promise about order, time, and inventory. The product must decide who gets the next chance. The system must prove that decision later. The notification layer must tell the user quickly, but the offer must also exist somewhere durable because SMS, email, and push are not guaranteed to arrive on time.
The safest mental model is: waitlist owns chances; booking owns inventory. The waitlist can say, "You may try to claim this released seat until 10:15." The booking service still decides whether the seat is really available when the user accepts. This boundary keeps the design honest when workers retry, messages lag, or two users race during a hot release.
How to approach
Start by agreeing what fairness means. Do not draw Kafka or Redis before you know whether the queue is strict FIFO, priority-based, lottery-based, or a mix. The data model, support story, and abuse controls all depend on that policy.
Clarifying Q&A
You: "Is the default policy strict first come, first served, or do some users have priority?"
Interviewer: "Start with FIFO. You can mention priority as a policy variant."
That answer means your ordering score can begin with joined_at, plus a tie-breaker. It also means support should be able to explain queue order plainly.
You: "When capacity returns, do we offer one released spot to one user, or notify several users and let the first accept win?"
Interviewer: "One spot to one user for the main design."
Now the design should create one active offer per returned unit. If you notify many users for one seat, you must call it a different product policy.
You: "What is the offer window, and does it start when the offer is created or when a notification is delivered?"
Interviewer: "Use a fixed window from offer creation, but account for notification lag."
That tells you the in-app offer is the source of truth and notification delay is an operational metric, not a reason to lose the offer row.
You: "Does accepting the offer include payment and booking, or only a claim that later goes through checkout?"
Interviewer: "Accept should create or hold the booking through the booking service."
Now you need idempotency on accept and a clear handoff to the booking service.
You: "Do users see exact queue position?"
Interviewer: "You choose, but explain support and abuse tradeoffs."
This is your chance to say exact position improves trust but can help bots. The backend should still store exact order even if the UI shows bands.
Minute pacing + In the room
| Minutes | Focus |
|---|---|
| 0-5 | Clarify fairness, offer policy, offer window, accept/payment scope, and visible position. |
| 5-12 | Capacity, HLD, and state machine. |
| 12-24 | Join, promote, notify, accept, expire, and audit in detailed design. |
| 24-34 | Architectural dig on one released seat and two racing users. |
| 34-45 | Scaling hot resources, notification lag, abuse, observability, and interview pushes. |
In the room: "I will define the queue policy first, then walk one cancelled seat. A promotion worker creates exactly one active offer for the next eligible waiter, stores it with an expiry, and writes an outbox event for notification. The in-app offer is primary; SMS and email are hints. When the user accepts, the waitlist calls the booking service with an idempotency key, because booking owns inventory truth. If the offer expires or accept fails with a real conflict, the system promotes the next user and writes an audit trail."
Out of scope
Building the whole ticketing or reservation platform. The waitlist depends on booking inventory, but this interview focuses on the queue, offer, accept, and expiry flow.
Payment provider internals. If accept includes payment, call a booking or payment service with idempotency. Do not design card networks unless asked.
A complete anti-bot vendor. You should mention rate limits, verified accounts, and duplicate-account checks, but the main design is fair promotion and safe accept.
Manual customer support tools. Support needs an audit read path. A full case-management product can stay out of scope.
Capacity estimation
Waitlists are usually read-light and write-important. Users may poll status, but the dangerous moments happen when capacity returns and offers are created.
| Dimension | Example scale | What it means |
|---|---|---|
| Waiters per hot resource | 200K or more | Index by resource and ordering score; avoid scanning the full list. |
| Capacity release bursts | Hundreds or thousands at once | Promote in controlled batches and respect notification provider limits. |
| Offer window | 5-30 minutes | Expiry workers must be reliable and visible in metrics. |
| Notification latency | Seconds to minutes by channel | In-app offer must be primary; channel delay is measured. |
| Accept traffic | Spiky around notification send | Accept must be idempotent and protected against double tap. |
| Audit retention | Months or years | Store compact transition events, not only current state. |
The main design implication is that each hot resource needs ordered promotion without turning into a global lock. You want one serialized decision stream per concert, flight, clinic slot pool, or room type, while letting different resources process independently.
High-level architecture
The high-level design has four parts: a waitlist API, a durable waitlist store, promotion workers, and notification workers. The booking service sits beside it and remains the final inventory authority.
Users join through the waitlist API, which creates a durable entry in WAITING. The booking service emits CapacityReleased when someone cancels or inventory is added. A promotion worker consumes that event for the resource, locks the next eligible waiter, creates an OFFERED record with expires_at, and writes an outbox event in the same transaction. The notification service sends push, SMS, or email from the outbox. The user accepts through the waitlist API, which calls the booking service with an idempotency key and moves the offer to a terminal state.
[ User app ] ---> [ Waitlist API ] ---> [ Waitlist DB ]
| ^ |
| | | outbox
| | v
| [ Notification workers ] <--- offer events
|
+-- accept offer --> [ Waitlist API ] ---> [ Booking service ] ---> [ Inventory DB ]
[ Booking cancellation ] ---> event bus ---> [ Promotion worker per resource ]
In the room: Make the boundary explicit. The waitlist chooses the next user and creates an offer. The booking service confirms the actual seat or slot. That prevents the waitlist from becoming a second, weaker inventory system.
Core design approaches
The first decision is policy, not storage.
| Policy | How it behaves | Good when | Watch out |
|---|---|---|---|
| Strict FIFO | Earliest valid join gets the next offer | Public fairness promise | Users may wait long; bots optimize join time. |
| Priority bands | VIP, accessibility, loyalty, or policy groups go first, FIFO inside each band | Business rules matter | Must be honest and auditable or users feel skipped. |
| Lottery from eligible waiters | Random draw for very scarce drops | Demand is far above supply | Harder to explain individual position. |
| Offer waves | Notify several users for several seats | Many cancellations arrive together | Needs careful matching so waves do not oversell. |
For the main interview, strict FIFO per resource is easiest to defend. Store an ordering key such as (joined_at, sequence_id) so ties are deterministic. If priority is required, make it a versioned policy that changes the score, not a hidden manual override.
The second decision is promotion style. A single worker per hot resource is simple and fair but can be slow if one resource has huge release bursts. Partitioning by resource_id in a stream gives you the same ordering property while different resources run in parallel. Database locks such as FOR UPDATE SKIP LOCKED can work, but you must explain how you avoid two workers creating offers for the same unit.
Data model
The data model needs current state for the product and transition history for trust.
| Record | Important fields | Why it exists |
|---|---|---|
waitlist_entry | entry_id, resource_id, user_id, state, joined_at, score, policy_version | The user's place in the queue. |
offer | offer_id, entry_id, resource_id, quantity, expires_at, state, token_hash | The temporary right to accept. |
capacity_event | event_id, resource_id, quantity, reason, processed_at | Idempotent input from booking or admin. |
booking_attempt | offer_id, idempotency_key, booking_id, result | Ties accept retries to one booking result. |
transition_audit | from_state, to_state, reason, actor, timestamp | Explains "why was I skipped?" later. |
outbox_event | event type, offer id, channel payload, status | Reliable handoff to notification workers. |
abuse_signal | user, device, IP, reason, action | Detects duplicate joins and bot activity. |
Use uniqueness rules to protect the obvious mistakes: one active waitlist entry per user per resource, one active offer per released unit, and one accepted booking attempt per offer. The exact constraint depends on the product, but the intent should be visible in the schema.
Detailed design
A waitlist journey has five important states: WAITING, OFFERED, ACCEPTED, DECLINED, and EXPIRED. The design is easier to explain if every user-visible action moves the entry through one of those states and writes an audit record. That audit record is not optional polish. It is how support answers the painful question, "Why did someone after me get a spot?"
When a user joins, the API verifies the resource is eligible for waitlisting and the user is allowed to join. It then inserts a waitlist_entry if one does not already exist for that user and resource. The ordering score is created at insert time. For FIFO, it is the join timestamp plus a monotonic sequence id so two users who join in the same millisecond still have a stable order. The API can return an exact position, an estimated band, or simply "you are on the list," depending on product choice. The backend still stores exact order and policy version.
When capacity returns, the booking service emits a CapacityReleased event with a unique event id, resource id, quantity, and reason. The promotion worker processes that event idempotently. If the same event is delivered twice, the worker sees it was already processed and does not create duplicate offers. For each returned unit, the worker opens a transaction, selects the next WAITING entry for that resource according to policy, moves it to OFFERED, creates an offer row with expires_at, writes an outbox notification event, writes an audit transition, and commits. Notification happens after commit so a slow SMS provider cannot leave the database in a half-offered state.
The user may learn about the offer from push, SMS, email, or the app. The app's in-product offer is the source of truth because external channels are delayed and sometimes lost. The offer token should be signed or random enough that users cannot guess someone else's offer, but the server still checks the database state on accept. The UI should show the expiry time based on the offer record, not based on when the SMS arrived.
When the user accepts, the API requires an idempotency key. It verifies the offer is still OFFERED, not expired, and belongs to that user. Then it calls the booking service to create or hold the reservation. The booking call carries the offer id and idempotency key so a retry returns the same booking result. If booking succeeds, the waitlist marks the offer ACCEPTED and the entry terminal. If booking returns a real conflict because inventory disappeared, the waitlist closes the offer with a reason and promotes the next eligible user. If booking times out, the system should retry or reconcile before telling the user to try a brand-new accept.
Expiry is another promotion input. A scheduler or delayed queue finds offers where expires_at has passed and state is still OFFERED. It moves the offer to EXPIRED, writes an audit transition, and emits a new capacity-available signal for the same resource. The next promotion should happen through the same path as a cancellation so there is one code path for "capacity can be offered."
Decline and leave are similar. If a user declines an active offer, the offer closes and the capacity moves to the next user. If a user leaves while waiting, the entry moves to LEFT. If a user tries to leave while offered, choose a policy: treat it as decline, or require explicit decline first. Either choice is fine if the state transition is clear and audited.
In the room recap: "Join creates a durable ordered entry. Capacity release creates one expiring offer for the next eligible waiter and an outbox notification in the same transaction. Notifications are hints; the in-app offer is primary. Accept is idempotent and calls booking, which owns final inventory. Expiry, decline, and booking conflict all feed back into the same promotion path for the next user."
Architectural dig
One released seat, two racing users
The central race starts when a cancellation returns one seat. A weak design pops a user from Redis, sends a message, and assumes the job is done. If the worker crashes after popping but before writing the offer, the user disappears. If two workers process the same cancellation, two users may receive offers for one seat. If the notification arrives late, users may race at accept time with stale assumptions.
A safer design makes offer creation a database transition. The worker processes CapacityReleased(event_id) once, locks the next WAITING row for the resource, creates an OFFERED row, and writes the notification outbox before commit. If the worker crashes after commit, the outbox relay still sends the message. If it crashes before commit, no offer exists and the event can be retried.
Bad design
cancel event -> worker pops user from volatile queue
-> sends SMS
-> crashes before durable offer
Result: user may be skipped, and support cannot explain why
Great design
BEGIN
record capacity_event if new
select next WAITING entry for resource in order
create OFFERED with expires_at
write transition_audit and outbox event
COMMIT
notification worker sends from outbox

The second race is accept. Suppose the user double taps, or the app sends the same accept twice after a timeout. Both requests must map to the same booking attempt. Store the idempotency key with the offer and call booking with a stable key such as offer_id + accept_attempt_id. One request may finish first, but the second should return the same booking id or the same terminal error.
The final detail is fairness proof. If priority is added later, do not silently rewrite queue order. Store policy version and reason codes on promotion. A user who says "I was skipped" should get an answer based on records, not a guess from logs.
Key challenges
| Challenge | What users feel | Strong answer |
|---|---|---|
| Fair order | "Someone after me got the spot" | Pick a policy and audit every promotion. |
| Duplicate offers | Two users think they can claim one seat | Serialize promotion per resource and create durable offer rows. |
| Offer expiry | User clicks a stale link | Server checks expires_at; expired offers promote the next user. |
| Notification lag | SMS arrives after the clock has almost run out | In-app offer is primary; measure delivery latency by channel. |
| Idempotent accept | Double tap creates duplicate booking or charge | Stable idempotency key and one booking attempt per offer. |
| Queue gaming | Bots or duplicate accounts crowd real users | Verified account, join rate limits, duplicate detection, and queue caps. |
Scaling the system
Partition work by resource_id so one concert's ordering does not block every other concert. In Kafka-like systems, use resource_id as the partition key to keep events for a single resource ordered. In a database-driven system, use row locks or advisory locks per resource and keep promotion transactions short.
Large waitlists need efficient indexes: (resource_id, state, score) for selecting the next waiting user and (user_id, resource_id) for preventing duplicate joins. Archive terminal entries over time so a hot resource does not carry every old transition in its active table. Keep the audit, but move old read-heavy history to cheaper storage.
Notification fan-out should respect provider limits. If 500 seats return at once, creating 500 offers may be correct, but sending 500 SMS messages through a provider with a strict rate cap needs backpressure. The product can handle this by waves, channel budgets, or in-app-first delivery with external channels catching up.
Failure handling
| Failure | User experience | What to build |
|---|---|---|
| Promotion worker crashes before commit | No visible offer yet | Retry event; no durable state changed. |
| Worker crashes after commit | Offer exists but SMS may not send | Outbox relay sends later; in-app offer visible. |
| Notification provider fails | User may not get SMS or email | Retry with backoff, use in-app inbox as primary, alert on delivery lag. |
| Accept call times out | User sees processing | Idempotent retry returns same booking result after reconciliation. |
| Booking service returns 409 | Offer could not become booking | Close offer with reason and promote next user. |
| Expiry job delayed | Capacity sits in stale offers | Alert on expired active offers and promotion lag. |
Do not let poison messages block a hot resource forever. Bad templates, invalid phone numbers, or provider failures should go to a dead-letter path with alerting, while the offer state remains visible in the app.
API design
| Endpoint | Role |
|---|---|
POST /v1/resources/{resource_id}/waitlist:join | Join the queue for a sold-out resource. |
DELETE /v1/waitlist/{entry_id} | Leave the waitlist or decline if policy allows. |
GET /v1/waitlist/{entry_id} | Read current state, visible position band, and active offer. |
POST /v1/offers/{offer_id}:accept | Accept an active offer with an idempotency key. |
POST /v1/offers/{offer_id}:decline | Decline and release the chance to the next user. |
POST /v1/internal/capacity-released | Internal event from booking or admin inventory. |
POST /v1/offers/{offer_id}:accept should include an Idempotency-Key header. The body may include payment method or checkout details if accept completes booking. The server validates offer ownership and expiry before calling booking.
cancel booking -> CapacityReleased(resource)
-> promotion worker -> OFFERED + outbox
-> user sees in-app offer / receives notification
-> POST accept with Idempotency-Key
-> booking service creates reservation
-> waitlist marks ACCEPTED
Important errors: 409 when the offer is no longer active, 410 when the offer expired, 403 when the user does not own the offer, 429 for abusive join or accept patterns, and 503 when booking is temporarily unavailable and the system cannot safely complete accept.
Observability
| Signal | Why it matters |
|---|---|
| Promotion lag from capacity release to offer created | Shows whether returned inventory reaches users quickly. |
| Offer creation to notification delivered by channel | Shows whether the offer window is fair for real users. |
| Active offers past expiry | Finds broken expiry workers. |
| Accept success, conflict, and timeout rates | Shows whether booking integration is healthy. |
| Duplicate join attempts and suspicious devices | Detects queue gaming. |
| Queue position complaints by resource | Finds fairness or communication problems. |
| Audit write failures | A waitlist without audit cannot defend itself. |
A good dashboard answers three questions: who should have been next, when did they get the offer, and what happened when they accepted or expired?
Security and abuse
Require authenticated users for joining and accepting. Use rate limits by user, device, IP, and payment instrument on hot resources. Prevent duplicate entries for the same user-resource pair, and consider stronger identity checks for high-value events where bots create many accounts. Signed offer tokens should be unguessable and tied to user identity; never let possession of a link alone accept someone else's offer.
Protect the fairness story from insider abuse too. Admin moves, priority overrides, and manual promotions need reason codes and audit. If the product supports priority, publish or at least clearly document the rule. Hidden manual reordering creates the same trust damage as a technical bug.
Cost awareness
Every notification costs money, especially SMS. Offering one seat to one user at a time is fair but may lower conversion if many users ignore messages. Offering in waves improves speed but changes the fairness promise. Choose the policy with product and cost in mind.
Long offer windows reduce complaints about late notifications but hide inventory. Short windows increase churn and notification spend because the same seat may be offered many times. Exact queue positions increase support cost when they move. Audit storage costs money, but it is cheaper than having no proof when users claim they were skipped.
Production scenes
Scene 1: "I was skipped"
The moment is a support escalation with screenshots. A user joined at 10:01, says their friend joined at 10:05, and the friend got an offer first. The system may be technically right because of priority bands or duplicate-account removal, but if support cannot explain it, users read it as unfair.
The trap is using a queue structure without a transition audit. A current WAITING row does not explain what happened yesterday. What to build: transition records with policy version, score, reason code, and actor. Support should reconstruct the promotion path without asking engineers to search raw logs.
Scene 2: Offer expires before notification arrives
A push notification is delayed by the mobile OS, SMS provider throttles, and email lands in spam. The offer window started at creation time, so the user opens the link with one minute left. The product looks unfair even if the backend followed the rule.
What to build: an in-app inbox that shows active offers immediately, channel-latency metrics, and an offer TTL sized from p99 delivery plus human reaction time. If the product wants the clock to start at delivery, that is possible, but it needs provider delivery receipts and a more complex fairness rule.
Scene 3: Promotion backlog grows while inventory is free
A storm cancels hundreds of bookings. Inventory is technically available, but waitlisted users do not receive offers for an hour. CPU looks normal because the bottleneck is a hot resource lock or provider rate cap.
What to build: promotion lag alerts, resource-level queue depth, wave promotion controls, and provider-aware notification budgets. Free inventory that is not offered is lost conversion and lost trust.
Scene 4: Double accept during a flaky mobile session
The user taps accept, the app times out, and they tap again. Without idempotency, two booking attempts or two payment attempts may start. With idempotency, the second call returns the same booking result and the UI settles.
In the interview: Use this scene to show that waitlist fairness and booking correctness meet at accept. The waitlist can offer the chance, but booking must still confirm one real reservation.
Bottlenecks and tradeoffs
Strict fairness vs fast conversion
Offering one spot to one person is fair and easy to explain. It can be slow if many people ignore offers. Waves improve conversion but turn the design into a competition among notified users. Say which product you are building.
Exact position vs abuse
Exact position helps trust but gives bots a target. Opaque bands reduce gaming but can feel less transparent. The backend should keep exact audit either way.
Notification speed vs provider limits
You can create offers faster than SMS providers can send. If the clock starts at offer creation, provider lag eats the user's window. In-app offers and channel-specific budgets keep the promise honest.
Waitlist ownership vs booking truth
If the waitlist tries to own inventory, it can drift from booking. If booking owns everything, the waitlist becomes a thin notification layer with no fairness record. The useful split is waitlist owns order and offers; booking owns final reservation.
Interview tips
Fairness is a policy before it is a data structure
You might say: "I will put users in a Redis sorted set."
Interview Push: "Sorted by what, and how do users know the order is fair?"
Land here: Name the policy first. For strict FIFO, score by join time plus a deterministic tie-breaker. For priority, store the priority rule version and FIFO inside each band. The storage choice supports the policy; it does not define fairness by itself.
One released seat should create one durable offer
You might say: "When a ticket is cancelled, we pop the next user and send an SMS."
Interview Push: "What if the worker crashes after the pop but before the SMS or database write?"
Land here: Create an offer row and notification outbox in the same transaction. If the worker crashes after commit, the offer still exists and the outbox can retry. If it crashes before commit, the event can retry and choose the same next user. Do not let a volatile queue pop become the only proof that someone was offered a spot.
Accept must be idempotent
You might say: "The accept endpoint calls booking and creates the reservation."
Interview Push: "The user double taps accept during a timeout. Do they book twice?"
Land here: Require Idempotency-Key on accept and bind it to the offer. The booking service receives a stable key such as offer_id + accept_attempt. A retry returns the same booking id or the same terminal failure. This protects both inventory and payment.
Notifications are hints, not the source of truth
You might say: "We notify the user, and then their offer window starts."
Interview Push: "How do you know SMS was delivered, and what if push is delayed?"
Land here: The in-app offer state is primary. Email, SMS, and push help the user notice it, but they may be delayed. The offer window should be sized from real delivery latency, and the UI should always read the server-side expires_at.
Audit is part of the product
You might say: "If users complain, we can check logs."
Interview Push: "A user says they were skipped two weeks ago. Logs expired. What now?"
Land here: Store compact transition audit as product data: previous state, next state, policy version, score, reason, actor, and timestamp. Logs are for debugging. Audit is how support explains fairness.
What should stick
- Policy first. FIFO, priority, lottery, or waves must be named before storage.
- One offer per unit. A released seat creates a durable offer with an expiry and audit.
- Notifications are async. The in-app offer is primary; SMS and email may lag.
- Accept is idempotent. Double taps and retries return the same booking result.
- Booking owns inventory. The waitlist gives a user the chance; booking confirms the seat.
Tell it in the room: "Cancellation emits capacity released. A promotion worker for that resource creates one expiring offer for the next FIFO entry and writes an outbox notification. The user accepts with an idempotency key. Booking confirms the real seat. Expiry, decline, or conflict promotes the next user with an audit record."
Key Takeaways
- Do not say "use a queue" until you define fairness.
- Store exact order and transition audit even if the UI shows only a position band.
- Offer creation and notification outbox belong in the same transaction.
- Size offer windows with notification lag and human reaction time in mind.
- Treat late SMS as expected; treat missing in-app offer state as a bug.
- Make accept idempotent and keep final inventory in the booking service.
- Use abuse controls on join and accept for hot resources.
Related Topics
- Airbnb Reservation System Design - Holds, confirmation, and date-range inventory.
- Vaccine Booking System Design - Last-slot contention and fair admission.
- Notification System Design - Channel delay, retries, and outbox delivery.
- Ticketmaster System Design - High-demand ticketing and queue pressure.
- Rate Limiter System Design - Bot and abuse controls around hot endpoints.
Quick revision notes
Clarify fairness. Ask whether the queue is FIFO, priority, lottery, or wave-based. Say what users can see and what support can prove.
Draw the state machine. WAITING -> OFFERED -> ACCEPTED / DECLINED / EXPIRED is the backbone. Every transition should be durable and auditable.
Promote through one path. Cancellation, expiry, and decline all create capacity that should flow through the same promotion logic.
Keep notification honest. The offer exists in the app first. External messages are helpful but delayed, so measure delivery time and do not rely on them as the only source of truth.
Protect accept. Idempotency on accept prevents duplicate booking and duplicate payment when the app retries.
Practice next: Open Booking Waitlist System Design on Practice System Design and run a timed attempt using one cancelled seat and one expiring offer as your main story.
What interviewers expect
A strong answer names the ordering policy, stores waitlist state durably, serializes promotion per resource, creates expiring offers, uses outbox notifications, makes accept idempotent, and keeps the booking service as the final inventory authority.
Interview workflow (template)
- Clarify requirements. Confirm functional scope, users, consistency needs, and which non-functional goals matter most (latency, availability, cost).
- Rough capacity. Estimate QPS, storage, and bandwidth so your data model and partitioning story are grounded.
- APIs and core flows. Define a minimal API and walk 1–2 critical read/write paths end to end.
- Data model and storage. Choose stores for each access pattern; call out hot keys, indexes, and retention.
- Scale and failure. Add caching, sharding, replication, queues, or fan-out as needed; say what breaks in failure modes.
- Tradeoffs. Name alternatives you rejected and why (e.g. strong vs eventual consistency, sync vs async).
Frequently asked follow-ups
- How do you keep the waitlist fair?
- What happens when an offer expires?
- How do you prevent two users from accepting the same released seat?
- What if SMS arrives after the offer window is almost over?
- How do you stop bots or duplicate accounts from gaming the queue?
Deep-dive questions and strong answer outlines
How do users join the waitlist?
Store a durable waitlist entry with resource id, user id, state, joined time, and ordering score. Use a uniqueness rule so the same user cannot join the same resource many times from retries.
What happens when capacity is released?
A promotion worker processes the release for that resource in order, locks the next waiting entry, creates an offer with an expiry, and writes an outbox event in the same transaction. Notification is async, but the offer exists in the in-app inbox immediately.
How do you avoid double sale on accept?
Accept validates the offer is active and unexpired, then calls the booking service with an idempotency key. Booking owns final inventory truth. If the booking service returns conflict, the offer closes and the system promotes the next person.
How do you handle notification lag?
Treat email, SMS, and push as hints. The in-app offer state is primary. The offer window should account for p99 notification delay plus a reasonable user reaction time, and metrics should track time from offer creation to delivered by channel.
How do you support priority users without breaking fairness?
Make the policy explicit: separate queues, score bands, or priority then FIFO within each band. Store policy version and reason codes on every promotion so support can explain why a user moved or was skipped.
AI feedback on your design
After a practice session, InterviewCrafted summarizes strengths, gaps, and interviewer-style expectations—similar to a written debrief. See a static example report, then practice this problem to get feedback on your own answer.
FAQs
Q: Is the waitlist the same as the booking system?
A: No. The waitlist decides who gets the next chance. The booking system still owns final inventory and payment. Keeping that boundary clear prevents the waitlist from selling a seat the booking system no longer has.
Q: Should users see exact position?
A: It depends on the product. Exact position builds trust but invites gaming and creates support tickets when priority rules move people. Many systems show a band or estimated wait instead, while keeping exact order in an audit log.
Q: Can we notify several users for one seat to improve conversion?
A: You can, but then it becomes a race or lottery policy, not strict FIFO. If strict fairness matters, offer one seat to one user at a time or clearly define waves with a booking lock that only one user can win.
Q: What if the user gets the SMS late?
A: The in-app offer should be the source of truth, and the window should be sized using delivery latency data. If the message arrives late, support should see when the offer was created, when each channel delivered, and why it expired.
Practice interactively
Open the practice session to use the canvas and stages, then review AI feedback.