System Design Cases
Hotel Booking (Booking.com)
Hotel Booking (Booking.com style) — system design case. Search → Booking Saga (hold → pay → confirm) → Payment → Notifications. Demonstrates no-double-booking via hybrid pessimistic-lock-on-hold + saga compensation, Elasticsearch search vs Postgres FTS trade-offs, dynamic pricing, cancellation/refund flow, and concurrent reservation race resolution. Five FlowBuilder scenarios + 2 ADRs (inventory consistency, search engine choice) + capacity hints on every node.
Hotel Booking (Booking.com)
Зачем нужно знать
Hotel booking хорошо проверяет, понимает ли инженер разницу между поиском и бронированием. Поиск должен быть быстрым, дешевым и допускающим небольшую устарелость. Бронирование должно быть строгим: нельзя продать последнюю комнату двум гостям, нельзя списать деньги без подтвержденного hold, нельзя оставить номер заблокированным навсегда после падения orchestrator-а.
Целевой учебный масштаб: 1.5M hotels × 50 room types/units × 730 nights дает верхнюю оценку 54.75B inventory-night rows до учета агрегации room types, sparse horizon и partitioning. 500M searches/day — примерно 5.8K RPS average; modeled peak 30K. 1M bookings/day — около 11.6 RPS average, но contention концентрируется на популярных hotel/date combinations. Эта асимметрия определяет архитектуру.
Кейс тренирует [CONCEPT]database-internals-overview, [CONCEPT]idempotency, [CONCEPT]consistency-models и Saga Orchestration. Он особенно полезен после базового [CONCEPT]acid-vs-base: availability в поисковом индексе может быть approximate, но hold всех stay nights должен быть атомарным.
Mental model
Думайте о hotel booking как о двух системах с разной правдой. Read path отвечает на вопрос: какие отели, вероятно, подходят пользователю по городу, датам, цене, amenities и рейтингу. Write path отвечает на другой вопрос: можно ли прямо сейчас удержать конкретный room type на конкретные dates и потом подтвердить его после оплаты. Ошибка возникает, когда search index начинают считать source of truth.
Второй mental model: бронирование — state machine hold -> authorize -> inventory commit -> capture -> confirm. Hold одной transaction резервирует каждую ночь [check_in, check_out). Payment сначала только авторизуется. Пока hold активен, confirm переводит все ночи held -> booked; лишь затем выполняется idempotent capture. Capture timeout оставляет capture_pending для reconciliation, а не позволяет reaper продать inventory снова.
Третий mental model: inventory обычно работает на уровне room type per date, а не физической комнаты. Для отеля важнее не “комната 503 свободна”, а “двухместный номер DBL свободен на 12-14 июня”. Физическую комнату можно назначить позже. Это уменьшает cardinality и делает overbooking контроль проще, но усложняет edge cases с connected rooms, accessible rooms и specific room requests.
Что показывает диаграмма
Диаграмма разделяет Edge/API Gateway, Search read path, Booking write path и Async fan-out. Client идет через CDN/API Gateway. GET /search направляется в Search Service, который сначала проверяет Redis cache, затем Elasticsearch с geo-фильтрами, price filters, amenities и текстовым ranking. Elasticsearch хранит денормализованный hotel document и availability hint, обновляемый через CDC или индексатор.
POST /bookings идет в Booking Orchestrator. Postgres хранит inventory_night(total,booked,held), booking_hold(hold_id,request_id,status,expires_at) и hold_night. Hold transaction блокирует все нужные nights в одинаковом date order, проверяет, что найдено ровно ожидаемое число nights, проверяет capacity, обновляет все counters и вставляет hold items. Row-local constraint booked + held <= total остается последней защитой, но не заменяет проверку полноты multi-night range.
Outbox rows вставляются в той же Postgres transaction, которая меняет booking/hold state, а publisher доставляет их асинхронно и может повторить event после сбоя. Поэтому notifications, analytics и search indexer deduplicate по event id. Письмо гостю и обновление поискового availability не выполняются внутри critical inventory transaction.
Сценарии и что они учат
Search + book + confirm показывает нормальный путь. Search возвращает stale-tolerant candidates; перед hold система получает versioned quote и транзакционно резервирует каждую ночь. PSP authorization не равен capture. Inventory становится booked только пока hold еще pending/unexpired, затем payment capture выполняется с устойчивым operation id, и клиент получает confirmed response после известного capture.
Race scenario показывает двух пользователей, которые одновременно пытаются взять последний room type на две nights. Transactions блокируют все одинаковые inventory rows в одном порядке, что уменьшает deadlock risk. Первый hold обновляет обе nights; второй после ожидания видит нулевую capacity и не пишет half booking. При SERIALIZABLE/deadlock errors application повторяет всю transaction, а не только последний SQL statement.
Authorization decline показывает compensation: transaction блокирует конкретный booking_hold, меняет только pending -> failed и освобождает все связанные hold_night. Reaper делает pending -> expired тем же способом, поэтому он не может повторно release уже confirmed/capture_pending hold. Это пример Saga Orchestration без долгой distributed transaction.
Hold timeout учит проектировать abandoned checkout. expires_at находится на отдельном hold aggregate, поэтому independent holds одного inventory row освобождаются независимо. Reaper выбирает expired pending holds, блокирует каждый aggregate и уменьшает counters по его hold items. capture_pending не expires: reconciler сначала выясняет исход PSP capture, затем либо confirms, либо cancels и releases.
Trade-offs
Первый trade-off: Elasticsearch против Postgres full-text/PostGIS. Оба варианта требуют measurement на реальном workload; специализированный index упрощает независимое масштабирование geo/text/facets/ranking, но приносит index lag. Поэтому ES отвечает только за discovery hints, а Postgres — за финальную availability.
Второй trade-off: pessimistic lock на весь booking flow против коротких inventory transactions плюс saga. Row locks не удерживаются во время PSP calls. Цена — explicit hold items, expiration, payment authorization/capture states, reaper, reconciliation и compensation. Выигрыш — concurrency invariant остается в Postgres, а external latency не занимает locks.
Третий trade-off: inventory по room type против inventory по конкретной комнате. Room type проще масштабируется и лучше подходит OTA, но не решает specific room assignment. Concrete room model нужен для небольших PMS или luxury hotels с точным выбором номера. Часто используют гибрид: booking держит room type, property management system позже назначает physical room.
Четвертый trade-off: cache freshness против search latency. Агрессивный cache дает 70-85% hit ratio, но может показывать уже занятый номер. Хороший UX обязан объяснять это: “room just taken”, suggestions, re-search. Плохой UX берет оплату и потом сообщает, что номера нет.
Реальные системы
Booking.com, Expedia и Airbnb разделяют search/discovery и transactional booking. У них разные доменные нюансы: hotel inventory часто идет от channel managers и PMS, Airbnb ближе к уникальным listings, а корпоративные travel systems добавляют policy approvals и negotiated rates. Но паттерн один: denormalized read model для поиска, строгая transaction для reservation.
В отельном мире есть внешние источники правды: property management systems, channel managers, GDS, direct hotel APIs. Они могут прислать обновление availability с задержкой или отклонить booking после предварительного hold. Поэтому production OTA часто держит partner-specific adapters, reconciliation с поставщиками и manual ops tools.
Платежи обычно делегируются отдельному payment case. Для hotel booking важно не смешивать payment ledger и reservation inventory. Booking знает payment_id, payment знает состояние денег, а refund/cancellation policy связывает их через saga.
Anti-patterns и типичные ошибки
Главный anti-pattern: считать Elasticsearch availability финальным. Если search index сказал “1 room left”, это только hint. Без transaction в inventory source of truth будет double-booking. Второй anti-pattern: long DB transaction на время payment. Это создает lock wait, connection pool starvation и cascading failures.
Третья ошибка: хранить один hold_expires_at на aggregate inventory counter. Несколько клиентов держат independent holds, поэтому expiry должен принадлежать booking_hold, а hold items определяют точный decrement. Четвертая: не делать durable idempotency на POST /bookings; duplicate submit обязан вернуть тот же hold/booking, а не создать второй.
Пятая ошибка: хранить цену только в search result. Versioned quote с taxes, fees, currency и expiry подтверждается перед hold/payment. Шестая: capture до authoritative inventory commit. Если hold истек, captured payment переживет потерянный inventory; безопаснее authorize, atomically commit inventory while unexpired, then capture and reconcile unknown outcomes.
Когда НЕ использовать
Не нужна такая архитектура для маленького сайта одного отеля с десятками номеров. Там можно начать с Postgres, простого transaction lock, server-rendered search и hosted payment checkout. Elasticsearch, Kafka, sharding и saga orchestrator будут дороже пользы.
Не стоит использовать OTA-style room type inventory, если продукт продает строго назначенные места или ресурсы: театр, самолет, парковочное место, врачебный slot. Там модель ближе к ticket booking или appointment scheduling. Также не нужен 15-minute hold для instant booking без payment step, если поставщик гарантирует availability и оплата идет позже по invoice.
Если partner API является единственным source of truth и не дает hold/confirm semantics, нельзя обещать пользователю жесткую гарантию “номер ваш” до подтверждения от партнера. Архитектура должна явно показывать pending state.
Дальше читать
- [CONCEPT]database-internals-overview про row locks, indexes, isolation и constraints.
- [CONCEPT]idempotency для безопасных retries booking/payment endpoints.
- Saga Orchestration для hold, payment, confirm, cancellation и refund.
- [CONCEPT]consistency-models для разделения stale read model и strict write model.
- Transactional Outbox для событий booking lifecycle.
- Материалы Booking.com/Expedia/Airbnb engineering о search ranking, availability cache, partner integrations и marketplace payments.