Ticket booking system case study (Ticketmaster, BookMyShow): browse events, select seats with TTL hold, pay, confirm. Handles flash sales (1M concurrent, 100K seats). Components: CDN, WAF/CAPTCHA with ML bot scoring, virtual waiting room (Redis sorted set), API gateway, event catalog (Cassandra), Redis SETNX hold service with 10-min TTL, sharded Postgres inventory, payment service to PSP (Stripe/Adyen), seat-map WebSocket, outbox to Kafka, notifications. Five scenarios: virtual queue under flash sale, atomic seat hold conflict, checkout to PSP and PG transaction, hold TTL expiry, anti-bot scalper detection. Two ADRs: virtual waiting room vs FCFS, Redis SETNX TTL vs Postgres row-lock.
Ticket booking похож на hotel booking только на поверхности. В гостиницах contention обычно распределен по отелям, датам и room types. В ticketing вся нагрузка может ударить в один event в одну минуту: миллионы пользователей хотят 80K мест, и большинство неизбежно проиграет. Поэтому задача системы не “продать всем”, а честно, предсказуемо и без oversell распределить ограниченный inventory под экстремальным burst.
Целевой масштаб: 100K active events, около 500M seats inventory, обычный browse QPS около 5K и flash-sale browse до 2M RPS. Booking attempts могут достигать 500K/sec, но successful bookings ограничены количеством seats и скоростью checkout. В hot event 99%+ пользователей не получат билет, и качество UX, fairness, bot mitigation и стабильность важнее, чем иллюзия бесконечного throughput.
Кейс тренирует ::concept{slug="rate-limiting"}, ::concept{slug="cdn-design"}, ::concept{slug="idempotency"} и ::concept{slug="postgres-internals"}. Если hotel booking учит короткому hold на inventory, ticket booking добавляет edge-side queue, антибот защиту и real-time seat map.
Думайте о ticket booking как о funnel с intentional shedding. CDN обслуживает кэшируемые event pages. WAF/CAPTCHA и bot scoring отсеивают автоматизацию. Virtual Waiting Room допускает в core только столько пользователей, сколько hold/payment/inventory pipeline может обработать. API Gateway принимает только signed queue tokens. Hold Service резервирует seat на короткое время. Inventory Postgres фиксирует окончательный booked state.
Второй mental model: seat hold и booked seat это разные уровни истины. Redis SETNX с TTL хорошо подходит для “этот пользователь сейчас имеет право оплатить F12”. Postgres остается source of truth для “F12 окончательно продан”. Если Redis падает, можно потерять временные holds, но нельзя потерять booked tickets. Если Postgres падает, продажи останавливаются, потому что финальная истина недоступна.
Третий mental model: fairness начинается до БД. Если пустить всех напрямую к Postgres row locks, победят не честные пользователи, а те, у кого быстрее bot, ближе регион и больше parallel connections. Queue token, admission rate и bot score превращают uncontrolled thundering herd в управляемый поток.
Диаграмма начинается с User и Scalper Bot, которые идут через CDN. Static event page, images и базовый catalog должны обслуживаться с edge cache, иначе core умрет до начала продаж. Dynamic queue join проходит через WAF/CAPTCHA/Bot Scoring. WAF записывает пользователя в Virtual Queue на Redis ZSET, где score может учитывать время входа, lottery seed, verified fan статус и risk score.
После admission пользователь получает signed token с коротким TTL и идет в API Gateway. Gateway проверяет токен, обращается к Event Catalog в Cassandra или другой read-optimized storage, затем к Hold Service. Hold Service делает SETNX hold:event:{event_id}:{seat_id} = user_id EX 600. Успех означает, что пользователь может идти в checkout. Conflict означает, что место уже held/booked и надо выбрать другое.
Payment Service обращается к PSP, а Inventory PG, sharded by event_id, в transaction переводит seats в booked и создает booking/ticket rows. Outbox публикует booking.confirmed в Kafka, Notifications отправляет e-ticket, Seat-map WebSocket рассылает изменения статуса мест. Важно, что edge queue, hold, payment и final commit не смешаны в один монолитный lock.
Virtual queue under burst показывает Taylor-Swift-tier drop. Пользователь открывает cached event page, проходит CAPTCHA, получает позицию и ETA, затем polling или SSE обновляет статус. Queue admits batches с rate, который соответствует capacity booking core. Урок: ожидание лучше, чем падение. Очередь также дает точку контроля для bot mitigation и регионального throttling.
Atomic seat hold показывает двух пользователей, которые кликают F12 почти одновременно. Первый SETNX создает ключ с TTL, второй получает conflict. WebSocket broadcast красит F12 как held для всех viewers. Урок: hold должен быть атомарным и дешевым, а UI должен быстро показывать stale seat map как обновившуюся реальность.
Checkout scenario показывает проверку hold owner, payment auth/capture, Postgres transaction held -> booked, outbox event и удаление Redis hold. Здесь важны idempotency key на checkout и повторная проверка владения hold внутри transaction. Если client retry-ит после timeout, он должен получить тот же booking result, а не второй charge.
Hold expiry показывает abandoned checkout. Redis TTL auto-delete через 10 минут освобождает seat without app code in hot path. Но production системе все равно нужен reconciler: если Redis key исчез, а Postgres seat остался held, надо привести state к available или booked. Anti-bot scenario показывает headless browser, rotated IP, invalid queue token и direct API bypass. Gateway не должен доверять только frontend route.
Первый trade-off: virtual waiting room против first-come-first-served. FCFS проще объяснить, но при 500K attempts/sec он уничтожает core и поощряет bots. Waiting room добавляет задержку и недовольство пользователей, зато стабилизирует систему, дает fairness policy и делает capacity планируемой.
Второй trade-off: Redis SETNX TTL против Postgres row-lock на весь checkout. Redis hold быстрый, self-expiring и выдерживает burst. Цена: temporary state живет вне source-of-truth, поэтому нужен reconciliation. Postgres long lock проще с точки зрения строгой атомарности, но под flash-sale превращается в lock storm и idle transactions.
Третий trade-off: assigned seats против general admission. Assigned seats требуют per-seat state, seat map, conflicts и WebSocket updates. General admission можно моделировать counter-ом capacity, но там все равно нужен atomic decrement and hold. Четвертый trade-off: lottery queue против strict timestamp order. Lottery снижает преимущество bots и network proximity, но может восприниматься как менее прозрачная.
Пятый trade-off: CAPTCHA и bot scoring против conversion. Сильная защита режет bots, но увеличивает friction для реальных fans. Хорошая система использует risk-based challenge: trusted users проходят быстрее, подозрительные получают больше проверок.
Ticketmaster, BookMyShow, Eventbrite и AXS решают похожий набор задач: cached event discovery, waiting room, seat hold, payment, e-ticket delivery и fraud/bot control. Cloudflare Waiting Room и Akamai edge controls часто используются как внешний слой, но core все равно обязан проверять signed tokens and rate limits.
В спортивных лигах и концертах добавляются sponsor presale, verified fan, promo codes, VIP packages, resale marketplace, transfer tickets and QR rotation. Эти функции не отменяют базовую модель, а добавляют policy вокруг admission, pricing and ticket ownership.
Платежный слой похож на payment-system, но ticketing имеет важный доменный нюанс: successful payment без successful seat commit недопустим. Если PSP charge succeeded, а inventory commit failed, нужна немедленная компенсация/refund или manual recovery queue.
Главный anti-pattern: пускать flash-sale traffic напрямую в booking API. Даже если Postgres и Redis выдержат секунды, bots будут выигрывать за счет parallelism. Второй: считать CAPTCHA достаточной защитой. Современные scalpers используют solved CAPTCHA services, residential proxies и browser fingerprint spoofing.
Третья ошибка: показывать seat map как source of truth. Seat map всегда slightly stale. Финальная проверка идет через hold. Четвертая: не проверять queue token на каждом mutating endpoint. Direct API bypass должен получать 401/403, иначе queue становится декоративной.
Пятая ошибка: удалять hold после payment до Postgres commit. Если commit fails, seat может вернуться в продажу при уже списанных деньгах. Шестая: отсутствие idempotency на checkout. Retry после PSP timeout без idempotency вызывает double charge или duplicate booking.
Седьмая ошибка: не разделять temporary hold expiry и final booked state. Redis TTL не должен отменять уже подтвержденный ticket. Postgres booked state должен побеждать временные cache inconsistencies.
Не нужна waiting room и bot scoring для малого театра с сотнями посетителей и равномерными продажами. Там достаточно Postgres transaction, simple rate limit, hosted payment и email tickets. Redis hold может быть полезен, но CDN/queue/WAF stack будет избыточен.
Не подходит assigned-seat модель для products без уникальных мест: цифровые товары, unlimited webinars, обычный checkout с большим складом. Там лучше inventory counter или order reservation. Также не стоит использовать Redis SETNX as final truth для legally binding tickets: при споре нужен durable record in Postgres/ledger.
Если event organizer требует ручное подтверждение каждого заказа, real-time hold pipeline можно заменить pending approval workflow. Пользователю надо честно показывать, что это request, а не confirmed ticket.
::concept{slug="rate-limiting"} для admission control, edge throttling и per-token limits.::concept{slug="cdn-design"} для кэширования event pages и защиты origin.::concept{slug="idempotency"} для checkout retries и PSP callbacks.::concept{slug="postgres-internals"} для row-level locks, unique constraints и transaction isolation.::concept{slug="saga-orchestration"} для payment + inventory commit + refund compensation.