45-minute system design interview format - Full-Stack Engineer Position
"Thanks for this challenge. I'll be designing an event ticketing platform like Ticketmaster, covering both the backend systems for handling traffic spikes and preventing overselling, and the frontend experience for seat selection and checkout. The key is designing the end-to-end flow where fast Redis locks enable responsive seat selection while PostgreSQL transactions ensure no double-selling."
- Event Browsing - Search and discover events with filtering
- Interactive Seat Selection - Visual seat map with real-time availability
- Virtual Waiting Room - Fair queue system for high-demand events
- Ticket Purchase - Reserve seats, checkout with countdown, payment processing
- Order Management - View tickets and order history
- Scalability: Handle 100x traffic spikes during on-sales
- Consistency: Zero overselling - each seat sold exactly once
- Latency: Seat reservation < 100ms, seat map load < 200ms
- Availability: 99.9% uptime, no downtime during high-profile events
- Shared TypeScript types between frontend and backend
- End-to-end seat reservation flow with optimistic UI
- Queue position polling with automatic admission
- Checkout flow with synchronized timer
- Real-time availability synchronization
┌──────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (React + TypeScript) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Event │ │ Seat Map │ │ Waiting │ │ Checkout │ │
│ │ Discovery │ │ (Canvas) │ │ Room │ │ Timer │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └─────┬───────┘ │
└─────────┼─────────────────┼─────────────────┼────────────────┼──────────┘
│ │ │ │
└─────────────────┴─────────────────┴────────────────┘
│
┌───────▼───────┐
│ API Gateway │
│ (Express) │
└───────┬───────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Redis │ │ PostgreSQL │ │ RabbitMQ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Seat Locks│ │ │ │ Events │ │ │ │ Notifs │ │
│ │ Sessions │ │ │ │ Seats │ │ │ │ Cleanup │ │
│ │ Queue │ │ │ │ Orders │ │ │ │ Tasks │ │
│ │ Cache │ │ │ │ Users │ │ │ └───────────┘ │
│ └───────────┘ │ │ └───────────┘ │ └─────────────────┘
└─────────────────┘ └─────────────────┘
The system uses shared TypeScript types between frontend and backend to ensure consistency:
┌─────────────────────────────────────────────────────────────────────────┐
│ SHARED TYPES (types.ts) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Venue │ │ Event │ │ Seat │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ id: string │ │ id: string │ │ id: string │ │
│ │ name: string │◀───│ venueId: string │ │ eventId: string │ │
│ │ capacity: number│ │ eventDate: Date │ │ section: string │ │
│ │ sectionConfig[] │ │ onSaleDate: Date│ │ row: string │ │
│ │ │ │ status: enum │ │ price: number │ │
│ │ │ │ highDemand: bool│ │ status: enum │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Reservation │ │ QueuePosition │ │ Order │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ sessionId │ │ position: number│ │ id: string │ │
│ │ eventId │ │ estimatedWait │ │ userId: string │ │
│ │ seatIds[] │ │ status: enum │ │ status: enum │ │
│ │ totalAmount │ │ - queued │ │ totalAmount │ │
│ │ expiresAt: Date │ │ - active │ │ paymentId │ │
│ │ status: enum │ │ - not_in_queue │ │ seats[] │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ SeatStatus: 'available' | 'held' | 'sold' │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ ReserveSeatsRequest │ │ ReserveSeatsResponse │
├──────────────────────────────┤ ├──────────────────────────────┤
│ eventId: string │───▶│ reservation: Reservation │
│ seatIds: string[] │ │ unavailableSeats: string[] │
└──────────────────────────────┘ └──────────────────────────────┘
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ CheckoutRequest │ │ ApiResponse<T> │
├──────────────────────────────┤ ├──────────────────────────────┤
│ idempotencyKey: string │ │ success: boolean │
│ paymentMethod: CardDetails │ │ data?: T │
└──────────────────────────────┘ │ error?: string │
└──────────────────────────────┘
┌────────┐ ┌─────────┐ ┌───────┐ ┌────────────┐
│Frontend│ │ Express │ │ Redis │ │ PostgreSQL │
└───┬────┘ └────┬────┘ └───┬───┘ └─────┬──────┘
│ │ │ │
│ 1. Select seats (optimistic UI) │
├───────────────▶ │ │
│ │ │ │
│ 2. POST /reserve │ │
├──────────────▶│ │ │
│ │ │ │
│ │ 3. SET NX (lock) │
│ ├─────────────▶│ │
│ │◀─────────────┤ │
│ │ │ │
│ │ 4. BEGIN transaction │
│ ├─────────────────────────────▶│
│ │ │ │
│ │ 5. SELECT FOR UPDATE NOWAIT │
│ ├─────────────────────────────▶│
│ │◀─────────────────────────────┤
│ │ │ │
│ │ 6. UPDATE seats SET held │
│ ├─────────────────────────────▶│
│ │ │ │
│ │ 7. COMMIT │
│ ├─────────────────────────────▶│
│ │ │ │
│ │ 8. SETEX reservation │
│ ├─────────────▶│ │
│ │ │ │
│◀──────────────┤ │ │
│ 9. Reservation + unavailable │ │
│ │ │ │
│ 10. Update UI (remove unavailable) │
▼ ▼ ▼ ▼
"I'm using Zustand for the seat selection store because it provides optimistic updates with minimal boilerplate. When a user selects a seat, we immediately update the UI, then reconcile with the server response."
┌─────────────────────────────────────────────────────────────────────────┐
│ SeatSelectionStore (Zustand) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ State: Actions: │
│ ┌─────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ eventId: string │ │ addSeat(seatId) │ │
│ │ selectedSeats: [] │ │ - Check max 6 seats │ │
│ │ reservation: null │ │ - Add to selectedSeats │ │
│ │ isReserving: false │ │ │ │
│ └─────────────────────┘ │ removeSeat(seatId) │ │
│ │ - Filter from selectedSeats │ │
│ │ │ │
│ Reservation Flow: │ reserveSeats() │ │
│ ┌───────────────────┐ │ 1. Set isReserving = true │ │
│ │ User selects seat │──────────│ 2. POST /api/seats/reserve │ │
│ └─────────┬─────────┘ │ 3. Handle unavailable seats │ │
│ ▼ │ 4. Set reservation │ │
│ ┌───────────────────┐ │ 5. Show toast if conflicts │ │
│ │ Optimistic update │ └─────────────────────────────────────┘ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ Server response │ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ Reconcile state │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
"The backend uses two-phase locking: first acquire Redis locks for speed (sub-millisecond), then confirm with PostgreSQL transaction for durability. This gives us both performance and ACID guarantees."
┌─────────────────────────────────────────────────────────────────────────┐
│ Two-Phase Reservation Process │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PHASE 1: Redis Locks (Speed) PHASE 2: PostgreSQL (Durability) │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ │ │ │ │
│ │ For each seat: │ │ BEGIN TRANSACTION │ │
│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ SET lock:seat:{id} │ │ │ │ SELECT ... FOR UPDATE │ │ │
│ │ │ NX EX 600 │ │──────▶│ │ NOWAIT │ │ │
│ │ │ (10 min TTL) │ │ │ └───────────────────────┘ │ │
│ │ └───────────────────────┘ │ │ │ │ │
│ │ │ │ │ ▼ │ │
│ │ ▼ │ │ ┌───────────────────────┐ │ │
│ │ ┌───────────────────────┐ │ │ │ Verify status = │ │ │
│ │ │ Track acquired/failed │ │ │ │ 'available' │ │ │
│ │ └───────────────────────┘ │ │ └───────────────────────┘ │ │
│ │ │ │ │ │ │
│ └─────────────────────────────┘ │ ▼ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ UPDATE seats SET │ │ │
│ │ │ status = 'held' │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ COMMIT │ │
│ └─────────────────────────────┘ │
│ │
│ On Error: ROLLBACK + Release Redis locks │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ ReservationTimer Component │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Props: expiresAt: Date │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Calculate │────▶│ Display │────▶│ When expired │ │
│ │ remaining time │ │ mm:ss │ │ - Toast error │ │
│ │ every 1 second │ │ │ │ - Clear select │ │
│ └─────────────────┘ └─────────────────┘ │ - Navigate home │ │
│ └─────────────────┘ │
│ │
│ Visual States: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ timeRemaining >= 120s │ Blue background, normal text │ │
│ ├────────────────────────┼────────────────────────────────────────┤ │
│ │ timeRemaining < 120s │ Red background, animate-pulse │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Virtual Waiting Room │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Redis Data Structures: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ queue:{eventId} │ Sorted Set (ZADD with timestamp) │ │
│ │ active:{eventId} │ Set of active session IDs │ │
│ │ active_session:{event}:{sid} │ Key with TTL (15 min shopping) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Constants: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MAX_CONCURRENT_SHOPPERS = 5000 │ │
│ │ SHOPPING_WINDOW = 900 seconds (15 minutes) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌────────┐ ┌─────────┐ ┌───────┐
│ User │ │ Backend │ │ Redis │
└───┬────┘ └────┬────┘ └───┬───┘
│ │ │
│ POST /queue/{eventId}/join │ │
├─────────────────────────────▶│ │
│ │ │
│ │ EXISTS active_session? │
│ ├────────────────────────────▶│
│ │◀────────────────────────────┤
│ │ │
│ │ [If not active] │
│ │ ZRANK queue:{eventId} │
│ ├────────────────────────────▶│
│ │◀────────────────────────────┤
│ │ │
│ │ [If not in queue] │
│ │ ZADD queue:{eventId} │
│ ├────────────────────────────▶│
│ │ │
│◀─────────────────────────────┤ │
│ { position, estimatedWait } │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ admitFromQueue Worker │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Every N seconds: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. activeCount = SCARD active:{eventId} │ │
│ │ 2. slotsAvailable = MAX_CONCURRENT - activeCount │ │
│ │ 3. nextUsers = ZRANGE queue:{eventId} 0 (slotsAvailable-1) │ │
│ │ 4. For each user: │ │
│ │ - SADD active:{eventId} sessionId │ │
│ │ - SETEX active_session:{eventId}:{sessionId} 900 "1" │ │
│ │ 5. ZREM queue:{eventId} ...nextUsers │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ estimateWait(position) = ceil(position / 500) * 60 seconds │
│ (Assumes ~500 users processed per minute) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ useQueuePolling Hook │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐ │
│ │ Join queue │───▶│ Poll every │───▶│ When status='active' │ │
│ │ on mount │ │ 3 seconds │ │ - Clear interval │ │
│ └───────────────┘ └───────────────┘ │ - Toast success │ │
│ │ - Navigate to event │ │
│ └───────────────────────┘ │
│ │
│ QueuePage UI: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ You're in the Queue │ │ │
│ │ │ │ │ │
│ │ │ [ 12,345 ] │ │ │
│ │ │ people ahead of you │ │ │
│ │ │ │ │ │
│ │ │ Estimated wait: ~25 minutes │ │ │
│ │ │ │ │ │
│ │ │ Don't refresh - we'll redirect you automatically │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Idempotent Checkout Process │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 1. Check Redis: GET idem:{idempotencyKey} │ │
│ │ └─▶ If exists, return cached result │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 2. Check PostgreSQL: SELECT * FROM orders WHERE idempotency_key │ │
│ │ └─▶ If exists, cache in Redis and return │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 3. Get reservation from Redis │ │
│ │ └─▶ Check not expired │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 4. Process payment (with circuit breaker) │ │
│ │ └─▶ On failure: return 402, don't continue │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 5. PostgreSQL Transaction: │ │
│ │ - INSERT INTO orders (with idempotency_key) │ │
│ │ - UPDATE seats SET status = 'sold' │ │
│ │ - INSERT INTO order_items │ │
│ │ - COMMIT │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 6. Cleanup: │ │
│ │ - DEL reservation:{sessionId} │ │
│ │ - DEL lock:seat:{eventId}:{seatId} (for each seat) │ │
│ │ - DEL availability:{eventId} (invalidate cache) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 7. SETEX idem:{idempotencyKey} 86400 response │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
"The frontend generates a unique idempotency key when the checkout page mounts (via useRef), and reuses it for retries. This prevents double-charges even if the user clicks 'Pay' multiple times."
┌─────────────────────────────────────────────────────────────────────────┐
│ CheckoutPage Component │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Initialization: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ idempotencyKeyRef = useRef(crypto.randomUUID()) │ │
│ │ (Generated once, reused for retries) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Layout: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ ReservationTimer [ 8:45 ] │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ Order Summary │ │ Payment Form │ │ │
│ │ │ ─────────────────── │ │ ─────────────────────────── │ │ │
│ │ │ Section A, Row 12 │ │ Card Number: [____________] │ │ │
│ │ │ Seat 5 $125.00 │ │ Expiry: [____] [____] │ │ │
│ │ │ Seat 6 $125.00 │ │ CVC: [___] │ │ │
│ │ │ ─────────────────── │ │ │ │ │
│ │ │ Subtotal: $250.00 │ │ [ Pay $262.50 ] │ │ │
│ │ │ Fees: $12.50 │ │ │ │ │
│ │ │ Total: $262.50 │ └─────────────────────────────────┘ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Error Handling: │
│ - 402: "Payment declined. Please try a different card." │
│ - Other: "Checkout failed. Please try again." │
│ - Same idempotency key for retry (safe to re-submit) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ GET /events/{eventId}/seats │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Check Redis │────▶│ Cache Hit? │─Yes─▶│ Return cached │ │
│ │ availability: │ └────────┬────────┘ │ response │ │
│ │ {eventId} │ │ No └─────────────────┘ │
│ └─────────────────┘ ▼ │
│ ┌─────────────────────┐ │
│ │ Query PostgreSQL │ │
│ │ SELECT id, section, │ │
│ │ row, seat_number, │ │
│ │ price, status │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Build response: │ │
│ │ { seats[], avail } │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Cache with TTL: │ │
│ │ on_sale: 5s │ │
│ │ otherwise: 30s │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ useAvailabilityPolling Hook │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Poll Interval: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ isOnSale = true ──▶ 5 seconds (high frequency) │ │
│ │ isOnSale = false ──▶ 30 seconds (low frequency) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Conflict Detection: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ For each selectedSeat: │ │
│ │ if (prev[seatId] === 'available' && │ │
│ │ current[seatId] !== 'available') { │ │
│ │ removeSeat(seatId); │ │
│ │ toast.warning('A selected seat was just taken'); │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Returns: { seats, availability } │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ cleanupExpiredHolds Worker │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Runs every 60 seconds: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. UPDATE seats │ │
│ │ SET status = 'available', │ │
│ │ held_by_session = NULL, │ │
│ │ held_until = NULL │ │
│ │ WHERE status = 'held' AND held_until < NOW() │ │
│ │ RETURNING id, event_id, held_by_session │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 2. For each expired seat: │ │
│ │ DEL lock:seat:{event_id}:{seat_id} │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 3. For each affected event: │ │
│ │ DEL availability:{event_id} │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 4. Log: { count: expired.length, events: eventIds.length } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| Decision | Chosen Approach | Alternative | Rationale |
|---|---|---|---|
| Seat Locking | Redis SET NX + PostgreSQL FOR UPDATE | Database-only locks | Redis gives sub-ms speed; PostgreSQL provides ACID |
| Hold Duration | 10 minutes | 5 min / 15 min | Balance between completion time and inventory release |
| Queue Implementation | Redis Sorted Set | Database polling | O(log N) operations, sub-ms latency |
| Cache TTL | Dynamic (5s on-sale, 30s otherwise) | Fixed TTL | Fresher data when it matters most |
| Idempotency | Redis cache + PostgreSQL column | Redis-only | Permanent record for auditing |
| Checkout | Synchronous payment | Async with webhooks | Simpler; immediate feedback to user |
"I've designed a full-stack event ticketing platform with:
- Shared TypeScript types ensuring consistency between frontend and backend for seats, reservations, and orders
- Two-phase seat reservation with Redis locks (1ms) and PostgreSQL transactions (ACID), with optimistic UI updates on the frontend
- Virtual waiting room with Redis sorted sets for fair queue management and frontend polling with auto-redirect
- Idempotent checkout preventing double-charges through idempotency keys cached in both Redis and PostgreSQL
- Real-time availability sync with dynamic cache TTLs (5s during sales, 30s otherwise) and conflict detection for selected seats
The key full-stack insight is that the frontend optimistically updates seat selections while the backend uses two-phase locking to guarantee consistency - when they diverge, the frontend gracefully handles the conflict by removing unavailable seats and notifying the user."