<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sumedh Bala</title>
    <description>The latest articles on DEV Community by Sumedh Bala (@sumedhbala).</description>
    <link>https://dev.to/sumedhbala</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3580553%2F1432be4b-7147-4437-8eb9-b66d129f92e2.jpg</url>
      <title>DEV Community: Sumedh Bala</title>
      <link>https://dev.to/sumedhbala</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sumedhbala"/>
    <language>en</language>
    <item>
      <title>Hotel Booking: Schema Design Comparison</title>
      <dc:creator>Sumedh Bala</dc:creator>
      <pubDate>Wed, 19 Nov 2025 20:07:02 +0000</pubDate>
      <link>https://dev.to/sumedhbala/hotel-booking-schema-design-comparison-g3h</link>
      <guid>https://dev.to/sumedhbala/hotel-booking-schema-design-comparison-g3h</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Hotel booking systems face a fundamental design decision: how to model availability and reservations in the database. This document compares two primary approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema 1: Row-Per-Day for Fungible Room Types&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One row per (hotel_id, room_type_id, date)&lt;/li&gt;
&lt;li&gt;Rooms of the same type are fungible (interchangeable)&lt;/li&gt;
&lt;li&gt;Uses B-tree indexes and optimistic locking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Schema 2: GiST with Individual Rooms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One row per physical room&lt;/li&gt;
&lt;li&gt;Rooms are non-fungible (each room tracked separately)&lt;/li&gt;
&lt;li&gt;Uses GiST indexes and exclusion constraints to prevent overlapping bookings (see Section 3.2 for detailed explanation of how GiST works)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both approaches solve the same problem (preventing double-booking) but with different trade-offs in complexity, performance, and flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite Reading (Shared Building Blocks)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This document extends the event-ticketing series by &lt;a class="mentioned-user" href="https://dev.to/sumedhbala"&gt;@sumedhbala&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/designing-a-large-scale-ticketing-system-2h1c"&gt;Designing a Large-Scale Ticketing System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-2-event-discovery-32o5"&gt;Part 2 – Event Discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-3-seat-management-nn2"&gt;Part 3 – Seat Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h"&gt;Part 4 – Payments &amp;amp; Ticket Issuance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We reuse the same foundation: PostgreSQL as system of record, transactional outbox + Debezium CDC into Kafka, Elasticsearch for discovery, Redis for low-latency availability, and Stripe-style payment flows. Familiarity with those components is assumed so we can focus on hotel-specific schema and availability trade-offs here.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Technical Topics Covered (Interview Highlights)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GiST indexes + exclusion constraints&lt;/strong&gt; (Section 3): Prevent overlapping range bookings with PostgreSQL GiST/exclusion constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic vs. pessimistic locking&lt;/strong&gt; (Sections 2.4 &amp;amp; 3.6): Version columns + &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; to avoid overbooking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fungible vs. non-fungible inventory modeling&lt;/strong&gt; (Sections 2 &amp;amp; 3): When to use row-per-day counts vs. per-room range bookings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-room-type pricing&lt;/strong&gt; (Section 2.1): &lt;code&gt;reservation_room_nights&lt;/code&gt; to support multiple room types in one reservation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid design&lt;/strong&gt; (Section 2.6): Schema 1 inventory + GiST room assignments to avoid room switching after check-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling patterns&lt;/strong&gt; (Section 7): Redis/search caches, read replicas, sharding by &lt;code&gt;hotel_id&lt;/code&gt;, regional deployments, streaming events.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  2. Schema 1: Row-Per-Day for Fungible Room Types
&lt;/h2&gt;
&lt;h3&gt;
  
  
  2.1 Schema Design
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Room types (fungible - Room 101 and 102 of same type are interchangeable)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;room_types&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;type_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- e.g., "Deluxe", "Suite"&lt;/span&gt;
    &lt;span class="n"&gt;max_capacity&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amenities&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Inventory: One row per room type per night&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Specific night date&lt;/span&gt;
    &lt;span class="n"&gt;total_rooms&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reserved_rooms&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- For optimistic locking&lt;/span&gt;

    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;room_types&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="c1"&gt;-- Database constraint ensures: total_rooms = available_rooms + reserved_rooms&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;chk_inventory_balance&lt;/span&gt; 
        &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;reserved_rooms&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="c1"&gt;-- Additional constraints for data integrity&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;chk_inventory_non_negative&lt;/span&gt; 
        &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;reserved_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;total_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- B-tree indexes for fast lookups&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: The PRIMARY KEY already creates an index on (hotel_id, room_type_id, date),&lt;/span&gt;
&lt;span class="c1"&gt;-- so idx_inventory_lookup is technically redundant but kept for clarity/explicit naming.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_inventory_lookup&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Partial index: Only includes rows where available_rooms &amp;gt; 0&lt;/span&gt;
&lt;span class="c1"&gt;-- This is smaller and faster for availability queries (the most common operation).&lt;/span&gt;
&lt;span class="c1"&gt;-- PostgreSQL will automatically use this index when querying for available rooms.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_inventory_available&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Reservations: Header table (one reservation can have multiple room types)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;check_in_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;check_out_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_guests&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_out'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'expired'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_amount_minor_units&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;hotels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Reservation rooms: One row per room type in the reservation&lt;/span&gt;
&lt;span class="c1"&gt;-- Allows booking multiple room types in a single reservation&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reservation_rooms&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;reservation_room_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Needed for composite foreign key&lt;/span&gt;
    &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_rooms&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Number of rooms of this type in the reservation&lt;/span&gt;

    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;room_types&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;chk_num_rooms_positive&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- One row per room type per reservation&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Reservation room nights: One row per room type per night for pricing&lt;/span&gt;
&lt;span class="c1"&gt;-- This allows different room types in the same reservation to have different nightly rates&lt;/span&gt;
&lt;span class="c1"&gt;-- (e.g., Deluxe room at $150/night, Suite at $300/night)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reservation_room_nights&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;reservation_room_night_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;reservation_room_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;night_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nightly_rate_minor_units&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Rate per room of this type for this night&lt;/span&gt;

    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_room_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;reservation_rooms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_room_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_room_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;night_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2.2 Key Characteristics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fungible Inventory&lt;/strong&gt;: Room 101 and Room 102 of type "Deluxe" are treated as identical&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Room Types&lt;/strong&gt;: A single reservation can include multiple room types (e.g., 2 Deluxe + 1 Suite) via the &lt;code&gt;reservation_rooms&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-Per-Day&lt;/strong&gt;: Each night gets its own inventory row&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count-Based&lt;/strong&gt;: Tracks &lt;code&gt;available_rooms&lt;/code&gt; count, not specific room assignments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B-tree Indexes&lt;/strong&gt;: Standard indexes for point queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic Locking&lt;/strong&gt;: Uses &lt;code&gt;version&lt;/code&gt; column to detect concurrent modifications&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2.3 Availability Check Query
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check availability for June 15-17 (3 nights) for 1 room&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'deluxe_001'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Check for at least 1 room&lt;/span&gt;

&lt;span class="c1"&gt;-- Check availability for multiple rooms (e.g., 3 rooms)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'deluxe_001'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Check for at least 3 rooms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2.4 Booking Creation (With Optimistic Locking)
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Option 1: Single Room Type (Simplified)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema1_single_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Create a booking for one or more rooms of the same type.

    Args:
        num_rooms: Number of rooms to book (default: 1)
        num_guests: Total number of guests across all rooms
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Use the multi-type function with single room type
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; 
        &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;num_guests&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Option 2: Multiple Room Types (Full Implementation)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Example Usage&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Book 1 room of single type
&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hotel_123&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deluxe_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-15&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-17&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Book multiple rooms of same type
&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hotel_123&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deluxe_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-15&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-17&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Book multiple room types in one reservation
# Example: 2 Deluxe rooms + 1 Suite for a family
&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hotel_123&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deluxe_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;suite_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-15&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-17&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.5 Pros and Cons
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;br&gt;
✅ &lt;strong&gt;Simple and intuitive&lt;/strong&gt;: Easy to understand and maintain&lt;br&gt;
✅ &lt;strong&gt;Efficient point queries&lt;/strong&gt;: Direct date lookups are O(log n) with B-tree index&lt;br&gt;
✅ &lt;strong&gt;Easy updates&lt;/strong&gt;: Update individual nights independently&lt;br&gt;
✅ &lt;strong&gt;Supports per-night pricing&lt;/strong&gt;: Each row can have different pricing/rates&lt;br&gt;
✅ &lt;strong&gt;Handles overlapping bookings&lt;/strong&gt;: User books June 15-17, another books June 16-18&lt;br&gt;
✅ &lt;strong&gt;Flexible room assignment&lt;/strong&gt;: Any available room of the type can be assigned&lt;br&gt;
✅ &lt;strong&gt;Works with standard indexes&lt;/strong&gt;: No special index types needed&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;br&gt;
❌ &lt;strong&gt;More rows&lt;/strong&gt;: 365-day window = 365 rows per room type (scales linearly with booking window)&lt;br&gt;
❌ &lt;strong&gt;Optimistic locking complexity&lt;/strong&gt;: Requires retry logic on conflicts&lt;br&gt;
❌ &lt;strong&gt;No specific room assignment at booking&lt;/strong&gt;: Don't know which exact room guest will get until check-in&lt;br&gt;
❌ &lt;strong&gt;Count-based tracking&lt;/strong&gt;: Must update all three fields together (total, available, reserved) - enforced by database CHECK constraint&lt;/p&gt;
&lt;h3&gt;
  
  
  2.6 Room Assignment Guarantee After Check-In
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Question&lt;/strong&gt;: Can Schema 1 guarantee that guests won't have to switch rooms once they've checked in?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer&lt;/strong&gt;: &lt;strong&gt;Schema 1 alone cannot guarantee this&lt;/strong&gt; because it only tracks room types, not specific room assignments. However, you can add a &lt;strong&gt;room assignment table&lt;/strong&gt; that locks in the specific room at check-in time.&lt;/p&gt;
&lt;h4&gt;
  
  
  Option 1: Add Room Assignment at Check-In (Recommended)
&lt;/h4&gt;

&lt;p&gt;Add a table to track specific room assignments after check-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Physical rooms table (needed for room assignments)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- e.g., "101", "102"&lt;/span&gt;
    &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;room_types&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Room assignments: Lock in specific room at check-in&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;room_assignments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;assignment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="n"&gt;DATERANGE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- [check_in_date, check_out_date)&lt;/span&gt;
    &lt;span class="n"&gt;assigned_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="c1"&gt;-- GiST exclusion constraint prevents overlapping assignments for same room&lt;/span&gt;
    &lt;span class="c1"&gt;-- GiST is explained in section 3.3&lt;/span&gt;
    &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How This Works&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;At Booking Time&lt;/strong&gt;: Reservation is created with multiple room types via &lt;code&gt;reservation_rooms&lt;/code&gt; table (no specific rooms assigned yet)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Check-In&lt;/strong&gt;: Hotel staff assigns specific rooms (one &lt;code&gt;room_assignments&lt;/code&gt; entry per room) matching the room types and counts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GiST Constraint&lt;/strong&gt;: Prevents any other reservation from being assigned to the same room for overlapping dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarantee&lt;/strong&gt;: Once assigned, each room cannot be assigned to anyone else during the guest's stay&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Check-In Flow&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_in_guest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_assignments&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Assign specific rooms to a reservation at check-in.

    Args:
        reservation_id: The reservation ID
        room_assignments: Dict mapping room_type_id to list of room numbers
                         Example: {&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deluxe_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;101&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;102&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;], &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;suite_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;201&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]}
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;reservation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_reservation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidStateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reservation must be confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Get reservation_rooms to validate assignments
&lt;/span&gt;    &lt;span class="n"&gt;reservation_rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT room_type_id, num_rooms
        FROM reservation_rooms
        WHERE reservation_id = ?
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Validate room assignments match reservation
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res_room&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reservation_rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;assigned_rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;room_assignments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res_room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assigned_rooms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;res_room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidRoomCountError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Room type &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;res_room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: Reservation requires &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;res_room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rooms, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;but &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assigned_rooms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; provided&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Assign each room (GiST constraint prevents overlaps)
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_numbers&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;room_assignments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;room_numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                    INSERT INTO room_assignments 
                    (reservation_id, hotel_id, room_number, stay_dates)
                    VALUES (?, ?, ?, daterange(?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;))
                &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                     &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Update reservation status
&lt;/span&gt;        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            UPDATE reservations
            SET status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checked_in&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, checked_in_at = NOW()
            WHERE reservation_id = ?
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ExclusionViolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# One or more rooms already assigned to another guest for these dates
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RoomAlreadyAssignedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One or more rooms are not available&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multiple Room Types Example&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Reservation for 2 Deluxe + 1 Suite
&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hotel_123&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deluxe_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;suite_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-15&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-06-17&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# At check-in, assign specific rooms for each type
&lt;/span&gt;&lt;span class="nf"&gt;check_in_guest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deluxe_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;101&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;102&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# 2 Deluxe rooms
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;suite_001&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;201&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="c1"&gt;# 1 Suite
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# Creates 3 room_assignments entries total
# GiST constraint ensures no overlaps for any of the 3 rooms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Guarantees no room switching&lt;/strong&gt;: Once assigned, GiST constraint prevents conflicts&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Flexibility before check-in&lt;/strong&gt;: Can reassign rooms if needed before guest arrives&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Best of both worlds&lt;/strong&gt;: Fungible inventory for booking, specific assignment at check-in&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Maintenance handling&lt;/strong&gt;: If room needs maintenance, can reassign before check-in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires additional &lt;code&gt;rooms&lt;/code&gt; and &lt;code&gt;room_assignments&lt;/code&gt; tables&lt;/li&gt;
&lt;li&gt;More complex than pure Schema 1, but simpler than full Schema 2&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Option 2: Pure Schema 1 (No Guarantee)
&lt;/h4&gt;

&lt;p&gt;If you don't add room assignments, Schema 1 &lt;strong&gt;cannot guarantee&lt;/strong&gt; that guests won't switch rooms because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No specific room is tracked&lt;/li&gt;
&lt;li&gt;Hotel could theoretically reassign rooms (though this would be bad practice)&lt;/li&gt;
&lt;li&gt;No database constraint prevents it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When This Is Acceptable&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard hotels where room switching is rare and handled operationally&lt;/li&gt;
&lt;li&gt;Hotels that prioritize flexibility over guarantees&lt;/li&gt;
&lt;li&gt;Systems where room assignment happens outside the booking system&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Recommendation
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;For production systems, add room assignments at check-in&lt;/strong&gt; (Option 1) to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Guarantee no room switching after check-in&lt;/li&gt;
&lt;li&gt;Maintain flexibility before check-in&lt;/li&gt;
&lt;li&gt;Provide audit trail of room assignments&lt;/li&gt;
&lt;li&gt;Enable room-level tracking for maintenance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This hybrid approach gives you the benefits of Schema 1 (fungible inventory, simple booking) with the guarantees of Schema 2 (specific room assignment, no conflicts).&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Schema 2: GiST with Individual Rooms
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Tables: Reused from Schema 1 vs. New Tables
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tables Reused from Schema 1:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;room_types&lt;/code&gt; - Room type metadata (Deluxe, Suite, etc.)&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;reservations&lt;/code&gt; - Reservation header table (check-in/out dates, guest info, status)&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;reservation_room_nights&lt;/code&gt; - Pricing per room type per night (optional, for pricing calculations)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tables NOT Used from Schema 1:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;code&gt;inventory&lt;/code&gt; - Not needed (availability calculated from &lt;code&gt;room_bookings&lt;/code&gt; overlaps)&lt;/li&gt;
&lt;li&gt;❌ &lt;code&gt;reservation_rooms&lt;/code&gt; - Not needed (bookings are per-room, not per-room-type)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;New Tables for Schema 2:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🆕 &lt;code&gt;rooms&lt;/code&gt; - Physical room inventory (hotel_id, room_number, room_type_id)&lt;/li&gt;
&lt;li&gt;🆕 &lt;code&gt;room_bookings&lt;/code&gt; - Individual room bookings with date ranges (uses GiST index with partial exclusion constraint to prevent overlaps for pending/confirmed/checked_in)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Difference:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema 1: Tracks availability by room type (fungible: "2 Deluxe rooms available")&lt;/li&gt;
&lt;li&gt;Schema 2: Tracks availability by specific room (non-fungible: "Room 101 available, Room 102 booked")&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Schema Design
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Physical rooms (non-fungible - each room tracked separately)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- e.g., "101", "102", "Suite-A"&lt;/span&gt;
    &lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Links to room_types for metadata&lt;/span&gt;
    &lt;span class="n"&gt;floor_number&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;room_types&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Room bookings: One row per booking with date range&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;booking_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Specific physical room&lt;/span&gt;
    &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="n"&gt;DATERANGE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- [check_in_date, check_out_date)&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_out'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- GiST index for fast range queries and overlap detection&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_room_bookings_dates&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stay_dates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Exclusion constraint prevents overlapping bookings for same room&lt;/span&gt;
&lt;span class="c1"&gt;-- Option 1: Partial exclusion constraint (RECOMMENDED - most idiomatic)&lt;/span&gt;
&lt;span class="c1"&gt;-- This constraint ONLY applies to "active" bookings (pending, confirmed, checked_in)&lt;/span&gt;
&lt;span class="c1"&gt;-- Cancelled/checked_out bookings don't block new bookings&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_in'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Option 2: Use trigger (more flexible, but less idiomatic)&lt;/span&gt;
&lt;span class="c1"&gt;-- Triggers are more flexible because you can add custom logic beyond just checking overlaps.&lt;/span&gt;
&lt;span class="c1"&gt;-- For example, you could log failed booking attempts, allow VIP users to override conflicts,&lt;/span&gt;
&lt;span class="c1"&gt;-- or enforce additional business rules like "no bookings within 1 hour of each other".&lt;/span&gt;
&lt;span class="c1"&gt;-- However, we don't need this flexibility here - the simple overlap check is sufficient.&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: We prevent overlaps for 'pending', 'confirmed', and 'checked_in' to avoid&lt;/span&gt;
&lt;span class="c1"&gt;-- double-booking scenarios where multiple pending reservations could later be confirmed&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;check_room_overlap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stay_dates&lt;/span&gt;  &lt;span class="c1"&gt;-- Overlap operator&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_in'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;booking_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;booking_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Room already booked for these dates'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;trigger_check_overlap&lt;/span&gt;
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_in'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; 
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;check_room_overlap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.3 How GiST Works
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;GiST (Generalized Search Tree)&lt;/strong&gt; is a PostgreSQL index type optimized for complex data types like ranges, geometric data, and full-text search. Unlike B-tree indexes (which work with simple comparisons like &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;), GiST indexes support spatial and range operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  What is GiST?
&lt;/h4&gt;

&lt;p&gt;GiST is an &lt;strong&gt;extensible indexing framework&lt;/strong&gt; that allows you to define custom index methods for complex data types. For date ranges, PostgreSQL provides built-in GiST support that understands range operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  How GiST Indexes Range Data
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Range Representation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- A date range is stored as: [start_date, end_date)&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Includes both endpoints&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Standard: includes start, excludes end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. GiST Index Structure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GiST builds a tree where each node contains &lt;strong&gt;bounding boxes&lt;/strong&gt; (the smallest range that contains all child ranges)&lt;/li&gt;
&lt;li&gt;Leaf nodes contain actual date ranges&lt;/li&gt;
&lt;li&gt;Internal nodes contain bounding boxes of their children&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example GiST Tree Structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Root Node
              [2024-01-01, 2024-12-31]  (bounding box)
                    /              \
         Node A                    Node B
    [2024-01-01, 2024-06-30]  [2024-07-01, 2024-12-31]
         /        \                  /        \
    Leaf 1      Leaf 2          Leaf 3      Leaf 4
[2024-06-15,  [2024-06-20,   [2024-07-10,  [2024-08-01,
 2024-06-17]   2024-06-22]    2024-07-15]   2024-08-05]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Range Operations GiST Supports
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Overlap Operator (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if two ranges overlap&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-18'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: TRUE (they overlap on June 16)&lt;/span&gt;

&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-18'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-20'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: FALSE (no overlap)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How GiST Evaluates Overlap:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start at root node&lt;/li&gt;
&lt;li&gt;Check if query range overlaps with node's bounding box&lt;/li&gt;
&lt;li&gt;If yes, descend into that branch&lt;/li&gt;
&lt;li&gt;If no, skip entire branch (pruning)&lt;/li&gt;
&lt;li&gt;Continue until leaf nodes&lt;/li&gt;
&lt;li&gt;Check actual ranges for overlap&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;2. Contains Operator (&lt;code&gt;@&amp;gt;&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if range contains a date&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-16'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: TRUE&lt;/span&gt;

&lt;span class="c1"&gt;-- Check if range contains another range&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-20'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: TRUE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Contained By Operator (&lt;code&gt;&amp;lt;@&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if range is contained by another range&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-20'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: TRUE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Adjacent Operator (&lt;code&gt;-|-&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if ranges are adjacent (touch but don't overlap)&lt;/span&gt;
&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-|-&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-19'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: TRUE (they touch at June 17)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  How Exclusion Constraints Use GiST
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Exclusion Constraint Syntax:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- Equality operator for hotel_id&lt;/span&gt;
    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- Equality operator for room_number&lt;/span&gt;
    &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;      &lt;span class="c1"&gt;-- Overlap operator for date ranges&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What This Means:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the same &lt;code&gt;(hotel_id, room_number)&lt;/code&gt; combination&lt;/li&gt;
&lt;li&gt;Prevent any two rows where &lt;code&gt;stay_dates&lt;/code&gt; overlap (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The constraint is enforced at the &lt;strong&gt;database level&lt;/strong&gt; (before insert/update)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How It Works Internally:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When inserting a new booking, PostgreSQL checks the GiST index&lt;/li&gt;
&lt;li&gt;Finds all existing bookings for the same &lt;code&gt;(hotel_id, room_number)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For each existing booking, checks if &lt;code&gt;stay_dates &amp;amp;&amp;amp; new_stay_dates&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If any overlap is found, the insert &lt;strong&gt;fails&lt;/strong&gt; with an exclusion violation error&lt;/li&gt;
&lt;li&gt;This happens &lt;strong&gt;atomically&lt;/strong&gt; - no race conditions possible&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Booking 1: Room 101, June 15-17&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'booking_1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reservation_1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
 &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- SUCCESS&lt;/span&gt;

&lt;span class="c1"&gt;-- Booking 2: Room 101, June 16-18 (OVERLAPS with Booking 1!)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'booking_2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reservation_2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
 &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-18'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: conflicting key value violates exclusion constraint "room_bookings_excl"&lt;/span&gt;
&lt;span class="c1"&gt;-- The GiST index found that Booking 1's range [2024-06-15, 2024-06-17] &lt;/span&gt;
&lt;span class="c1"&gt;-- overlaps with Booking 2's range [2024-06-16, 2024-06-18]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why GiST is Efficient for Range Queries
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Pruning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GiST can skip entire branches of the tree if their bounding boxes don't overlap&lt;/li&gt;
&lt;li&gt;Example: If query range is &lt;code&gt;[2024-06-15, 2024-06-17]&lt;/code&gt; and a node's bounding box is &lt;code&gt;[2024-12-01, 2024-12-31]&lt;/code&gt;, that entire branch is skipped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Index-Only Scans:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For overlap checks, GiST can often answer queries using only the index (without accessing table data)&lt;/li&gt;
&lt;li&gt;This is much faster than scanning all rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Spatial Locality:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ranges that are close in time are stored near each other in the index&lt;/li&gt;
&lt;li&gt;Queries for nearby dates are very efficient&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Performance Comparison: GiST vs B-tree for Ranges
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;B-tree with Date Ranges (Inefficient):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Without GiST, you'd need to check every booking&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'101'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_in_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_in_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_out_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Must scan many rows, complex WHERE clause&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GiST with Date Ranges (Efficient):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- With GiST, single overlap operator&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'101'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Uses GiST index, very fast, simple query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  GiST Index Maintenance
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Insert Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inserting a new range requires updating the GiST tree&lt;/li&gt;
&lt;li&gt;May need to split nodes if bounding boxes become too large&lt;/li&gt;
&lt;li&gt;Generally O(log n) but can be slower than B-tree for simple inserts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Update Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updating a range may require rebalancing the tree&lt;/li&gt;
&lt;li&gt;More expensive than B-tree updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Query Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overlap queries: &lt;strong&gt;Excellent&lt;/strong&gt; (O(log n) with pruning)&lt;/li&gt;
&lt;li&gt;Point queries: Good (but B-tree is better)&lt;/li&gt;
&lt;li&gt;Range containment: Excellent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.4 Key Characteristics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-Fungible Inventory&lt;/strong&gt;: Each physical room (Room 101, Room 102) is tracked separately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date Ranges&lt;/strong&gt;: Uses &lt;code&gt;DATERANGE&lt;/code&gt; to store booking periods&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GiST Indexes&lt;/strong&gt;: Optimized for range queries and overlap detection (see section 3.3 for details)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database-Enforced Overlaps&lt;/strong&gt;: Exclusion constraints or triggers prevent double-booking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific Room Assignment&lt;/strong&gt;: Know exactly which room guest will get&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.5 Availability Check Query
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find available rooms of a specific type for June 15-17&lt;/span&gt;
&lt;span class="c1"&gt;-- Exclude rooms with pending, confirmed, or checked_in bookings&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hotel_123'&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'deluxe_001'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;room_bookings&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stay_dates&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-17'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Overlap check&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked_in'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.6 Booking Creation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Single Room Booking
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema2_single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Create a booking for a single room.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Find an available room of this type
&lt;/span&gt;    &lt;span class="c1"&gt;# Exclude rooms with pending, confirmed, or checked_in bookings for these dates
&lt;/span&gt;    &lt;span class="n"&gt;available_room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT r.room_number
        FROM rooms r
        WHERE r.hotel_id = ? AND r.room_type_id = ?
          AND NOT EXISTS (
              SELECT 1 FROM room_bookings b
              WHERE b.hotel_id = r.hotel_id
                AND b.room_number = r.room_number
                AND b.stay_dates &amp;amp;&amp;amp; daterange(?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                AND b.status IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checked_in&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)  -- Include pending
          )
        LIMIT 1
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;available_room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientAvailabilityError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;room_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;available_room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Create reservation
&lt;/span&gt;    &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO reservations 
        (hotel_id, check_in_date, check_out_date, num_guests, status, total_amount_minor_units)
        VALUES (?, ?, ?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending_payment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 0)
        RETURNING reservation_id
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 3: Create room booking (exclusion constraint prevents overlap)
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO room_bookings 
            (hotel_id, room_number, reservation_id, stay_dates, status)
            VALUES (?, ?, ?, daterange(?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ExclusionViolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Another booking took this room - retry with different room
&lt;/span&gt;        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema2_single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Multiple Rooms Booking (Complex - Requires Retry Logic)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_booking_schema2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Create a booking for multiple rooms of the same type.

    CRITICAL: This is significantly more complex than Schema 1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s atomic decrement.
    We must find and book N different rooms atomically, which requires retry logic
    if any room becomes unavailable during the transaction.

    Args:
        num_rooms: Number of rooms to book (e.g., 3 Deluxe rooms)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="c1"&gt;# Step 1: Find N available rooms of this type
&lt;/span&gt;                &lt;span class="c1"&gt;# Use FOR UPDATE SKIP LOCKED to lock available rooms atomically
&lt;/span&gt;                &lt;span class="c1"&gt;# 
&lt;/span&gt;                &lt;span class="c1"&gt;# FOR UPDATE SKIP LOCKED Explanation (VERY USEFUL FOR INTERVIEWS):
&lt;/span&gt;                &lt;span class="c1"&gt;# This PostgreSQL feature is crucial for preventing race conditions in concurrent systems.
&lt;/span&gt;                &lt;span class="c1"&gt;# 
&lt;/span&gt;                &lt;span class="c1"&gt;# How it works:
&lt;/span&gt;                &lt;span class="c1"&gt;# 1. FOR UPDATE: Locks the selected rows for this transaction (prevents other transactions
&lt;/span&gt;                &lt;span class="c1"&gt;#    from modifying them until we commit/rollback)
&lt;/span&gt;                &lt;span class="c1"&gt;# 2. SKIP LOCKED: If a row is already locked by another transaction, skip it and continue
&lt;/span&gt;                &lt;span class="c1"&gt;#    to the next available row (instead of waiting/blocking)
&lt;/span&gt;                &lt;span class="c1"&gt;# 
&lt;/span&gt;                &lt;span class="c1"&gt;# Why it's useful:
&lt;/span&gt;                &lt;span class="c1"&gt;# - Prevents "lost update" problems: Multiple transactions can't grab the same room
&lt;/span&gt;                &lt;span class="c1"&gt;# - High concurrency: Transactions don't block each other - they just skip locked rows
&lt;/span&gt;                &lt;span class="c1"&gt;# - Perfect for work queues, booking systems, inventory allocation
&lt;/span&gt;                &lt;span class="c1"&gt;# - Common interview question: "How do you prevent two workers from processing the same job?"
&lt;/span&gt;                &lt;span class="c1"&gt;# 
&lt;/span&gt;                &lt;span class="c1"&gt;# Example scenario:
&lt;/span&gt;                &lt;span class="c1"&gt;#   T1: SELECT ... FOR UPDATE SKIP LOCKED LIMIT 3 → locks rooms 101, 102, 103
&lt;/span&gt;                &lt;span class="c1"&gt;#   T2: SELECT ... FOR UPDATE SKIP LOCKED LIMIT 3 → skips 101,102,103, locks 104, 105, 106
&lt;/span&gt;                &lt;span class="c1"&gt;#   Both transactions proceed without blocking each other!
&lt;/span&gt;                &lt;span class="c1"&gt;# 
&lt;/span&gt;                &lt;span class="c1"&gt;# Alternative (FOR UPDATE without SKIP LOCKED):
&lt;/span&gt;                &lt;span class="c1"&gt;#   T1: SELECT ... FOR UPDATE LIMIT 3 → locks rooms 101, 102, 103
&lt;/span&gt;                &lt;span class="c1"&gt;#   T2: SELECT ... FOR UPDATE LIMIT 3 → WAITS for T1 to commit (blocks, reduces concurrency)
&lt;/span&gt;                &lt;span class="n"&gt;available_rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                    SELECT r.room_number
                    FROM rooms r
                    WHERE r.hotel_id = ? AND r.room_type_id = ?
                      AND NOT EXISTS (
                          SELECT 1 FROM room_bookings b
                          WHERE b.hotel_id = r.hotel_id
                            AND b.room_number = r.room_number
                            AND b.stay_dates &amp;amp;&amp;amp; daterange(?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                            AND b.status IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checked_in&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                      )
                    ORDER BY r.room_number
                    LIMIT ?
                    FOR UPDATE SKIP LOCKED  -- Lock rows, skip if already locked
                &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available_rooms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientAvailabilityError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available_rooms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rooms available, need &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Step 2: Create reservation header
&lt;/span&gt;                &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                    INSERT INTO reservations 
                    (hotel_id, check_in_date, check_out_date, num_guests, status, total_amount_minor_units)
                    VALUES (?, ?, ?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending_payment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 0)
                    RETURNING reservation_id
                &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_guests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Step 3: Create room bookings for all N rooms
&lt;/span&gt;                &lt;span class="c1"&gt;# The exclusion constraint will prevent overlaps if another transaction
&lt;/span&gt;                &lt;span class="c1"&gt;# grabbed a room between our SELECT and INSERT
&lt;/span&gt;                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;available_rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                            INSERT INTO room_bookings 
                            (hotel_id, room_number, reservation_id, stay_dates, status)
                            VALUES (?, ?, ?, daterange(?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ExclusionViolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="c1"&gt;# Room was taken by another transaction - abort entire booking
&lt;/span&gt;                        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentModificationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Room &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; was booked by another transaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;

        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConcurrentModificationError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ExclusionViolation&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientAvailabilityError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not book &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rooms after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# Exponential backoff before retry
&lt;/span&gt;            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientAvailabilityError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Booking failed after retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Challenges with Multi-Room Bookings in Schema 2&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency Risk&lt;/strong&gt;: Even with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;, there's a small window between SELECT and INSERT where another transaction could grab a room. Important: &lt;code&gt;FOR UPDATE&lt;/code&gt; does NOT prevent other transactions from reading the rows - it only prevents them from:

&lt;ul&gt;
&lt;li&gt;Taking a &lt;code&gt;FOR UPDATE&lt;/code&gt; lock on the same rows (they'd wait or skip with &lt;code&gt;SKIP LOCKED&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Modifying the rows (UPDATE/DELETE)&lt;/li&gt;
&lt;li&gt;Other transactions can still read the rows with regular SELECT (no lock required)&lt;/li&gt;
&lt;li&gt;This means another transaction could check availability (read) and insert into &lt;code&gt;room_bookings&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The exclusion constraint on &lt;code&gt;room_bookings&lt;/code&gt; is the final protection (will raise &lt;code&gt;ExclusionViolation&lt;/code&gt; if overlap detected)&lt;/li&gt;
&lt;li&gt;This is why we catch &lt;code&gt;ExclusionViolation&lt;/code&gt; and retry the entire booking&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All-or-Nothing for Same Room Type&lt;/strong&gt;: If booking 3 rooms of the same type and we can only find 2 available, the entire booking fails. This is different from Schema 1, where booking 3 rooms of the same type is a single atomic &lt;code&gt;UPDATE inventory SET available_rooms = available_rooms - 3&lt;/code&gt; operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry Complexity&lt;/strong&gt;: Requires retry logic with exponential backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; helps prevent conflicts but adds locking overhead (see detailed explanation above)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Comparison with Schema 1&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema 1 (same room type)&lt;/strong&gt;: &lt;code&gt;UPDATE inventory SET available_rooms = available_rooms - 3&lt;/code&gt; (single atomic operation for N rooms of same type)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema 1 (multiple room types)&lt;/strong&gt;: Also all-or-nothing - if any room type lacks availability, entire booking fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema 2&lt;/strong&gt;: Must find N different physical rooms, lock them, and insert N rows (multiple operations, higher failure risk even for same room type)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.7 Pros and Cons
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;br&gt;
✅ &lt;strong&gt;Automatic overlap prevention&lt;/strong&gt;: Database enforces no double-booking at constraint level (for single rooms)&lt;br&gt;
✅ &lt;strong&gt;Exact room assignment&lt;/strong&gt;: Know exactly which room guest will get (Room 101, not "any Deluxe room")&lt;br&gt;
✅ &lt;strong&gt;Room-level tracking&lt;/strong&gt;: Can track maintenance, room-specific issues, preferences&lt;br&gt;
✅ &lt;strong&gt;Efficient range queries&lt;/strong&gt;: GiST index optimized for date range operations&lt;br&gt;
✅ &lt;strong&gt;No optimistic locking needed&lt;/strong&gt;: Exclusion constraint handles single-room concurrency&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;br&gt;
❌ &lt;strong&gt;Multi-room booking complexity&lt;/strong&gt;: Booking N rooms requires finding and locking N different rooms, with retry logic if any room becomes unavailable. Much more complex than Schema 1's atomic &lt;code&gt;UPDATE inventory SET available_rooms = available_rooms - N&lt;/code&gt;&lt;br&gt;
❌ &lt;strong&gt;Concurrency challenges for multi-room&lt;/strong&gt;: Between finding available rooms and inserting bookings, another transaction may grab one, requiring full retry&lt;br&gt;
❌ &lt;strong&gt;Less flexible&lt;/strong&gt;: Can't reassign rooms easily (guest booked Room 101, but it needs maintenance)&lt;br&gt;
❌ &lt;strong&gt;More complex queries&lt;/strong&gt;: Need to find available rooms by checking non-overlapping ranges&lt;br&gt;
❌ &lt;strong&gt;Room assignment logic&lt;/strong&gt;: Must decide which available room to assign (first available? best view?)&lt;br&gt;
❌ &lt;strong&gt;More dynamic booking rows&lt;/strong&gt;: For a booking of 3 rooms, creates 3 rows in &lt;code&gt;room_bookings&lt;/code&gt; (vs. Schema 1's 1 row in &lt;code&gt;reservation_rooms&lt;/code&gt;)&lt;br&gt;
❌ &lt;strong&gt;GiST index overhead&lt;/strong&gt;: Larger index size, more complex query planning&lt;br&gt;
❌ &lt;strong&gt;Not fungible&lt;/strong&gt;: Room 101 and 102 are different, even if same type (like event seats)&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Comparative Analysis
&lt;/h2&gt;
&lt;h3&gt;
  
  
  4.1 Schema Structure
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Schema 1: Row-Per-Day (Fungible)&lt;/th&gt;
&lt;th&gt;Schema 2: GiST (Individual Rooms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inventory Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fungible (Room 101 = Room 102)&lt;/td&gt;
&lt;td&gt;Non-fungible (Room 101 ≠ Room 102)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inventory Table&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inventory(hotel_id, room_type_id, date)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rooms(hotel_id, room_number)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Booking Table&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reservations&lt;/code&gt; + &lt;code&gt;reservation_rooms(room_type_id, num_rooms)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;room_bookings(room_number, stay_dates)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Date Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One row per night (DATE)&lt;/td&gt;
&lt;td&gt;Date range per booking (DATERANGE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Index Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;B-tree&lt;/td&gt;
&lt;td&gt;GiST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rows per Booking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 reservation + N nights&lt;/td&gt;
&lt;td&gt;1 room_booking (with date range)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  4.2 Query Performance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Schema 1&lt;/th&gt;
&lt;th&gt;Schema 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Check Availability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT ... WHERE date IN (...)&lt;/code&gt; - O(log n) per date&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT ... WHERE NOT EXISTS (overlap check)&lt;/code&gt; - O(log n) with GiST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Point Query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent (direct date lookup)&lt;/td&gt;
&lt;td&gt;Good (range overlap check)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Range Query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Good (multiple point queries)&lt;/td&gt;
&lt;td&gt;Excellent (single range query)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Find Available Room&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Count-based (simple)&lt;/td&gt;
&lt;td&gt;Must query all rooms, check overlaps (more complex)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  4.3 Concurrency Control
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Schema 1&lt;/th&gt;
&lt;th&gt;Schema 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optimistic locking (version numbers)&lt;/td&gt;
&lt;td&gt;Database constraints (GiST exclusion)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conflict Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check version in WHERE clause&lt;/td&gt;
&lt;td&gt;Constraint violation on overlap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required (on version mismatch)&lt;/td&gt;
&lt;td&gt;Required for multi-room (must find N rooms atomically)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deadlocks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (no long-held locks)&lt;/td&gt;
&lt;td&gt;Possible (if using FOR UPDATE SKIP LOCKED)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium (retry logic for version)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High for multi-room&lt;/strong&gt; (find N rooms, lock, insert N rows, handle failures)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single Room&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Atomic &lt;code&gt;UPDATE ... SET available_rooms = available_rooms - 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Simple: exclusion constraint handles it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-Room&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Atomic &lt;code&gt;UPDATE ... SET available_rooms = available_rooms - N&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Complex&lt;/strong&gt;: Must find N different rooms, lock them, insert N rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  4.4 Update Operations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Schema 1&lt;/th&gt;
&lt;th&gt;Schema 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Create Booking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UPDATE inventory (decrement count)&lt;/td&gt;
&lt;td&gt;INSERT room_booking (with date range)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cancel Booking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UPDATE inventory (increment count)&lt;/td&gt;
&lt;td&gt;DELETE room_booking or update status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Modify Dates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UPDATE multiple inventory rows&lt;/td&gt;
&lt;td&gt;DELETE + INSERT new date range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Room Reassignment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No change needed (fungible)&lt;/td&gt;
&lt;td&gt;DELETE + INSERT (different room)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  4.5 Storage and Scalability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important Distinction&lt;/strong&gt;: We must separate &lt;strong&gt;static inventory rows&lt;/strong&gt; from &lt;strong&gt;dynamic booking rows&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Schema 1&lt;/th&gt;
&lt;th&gt;Schema 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static Inventory Rows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;365 rows per room type per hotel (scales with booking window)&lt;/td&gt;
&lt;td&gt;1 row per physical room (fixed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Note on "365"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;This represents a 1-year (365-day) booking window (common in hotels). The number scales linearly: 90-day window = 90 rows, 365-day window = 365 rows. This is configurable based on business needs.&lt;/td&gt;
&lt;td&gt;Fixed regardless of booking window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rows per Single-Room Booking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 reservation + 1 reservation_rooms + N reservation_room_nights&lt;/td&gt;
&lt;td&gt;1 reservation + 1 room_booking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rows per Multi-Room Booking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 reservation + M reservation_rooms + (M × N) reservation_room_nights&lt;br&gt;(M = room types, N = nights)&lt;/td&gt;
&lt;td&gt;1 reservation + &lt;strong&gt;N room_bookings&lt;/strong&gt; (one per room)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Index Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2MB (B-tree, 100 hotels × 3 types × 365 days)&lt;/td&gt;
&lt;td&gt;~200KB (GiST, but more complex)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static rows scale with booking window&lt;/td&gt;
&lt;td&gt;Static rows fixed; dynamic rows scale with bookings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Example Calculation&lt;/strong&gt; (100 hotels, 3 room types, 10 rooms per type, 365-day window):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema 1 Static&lt;/strong&gt;: 100 × 3 × 365 = &lt;strong&gt;109,500 inventory rows&lt;/strong&gt; (large, but simple queries)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema 2 Static&lt;/strong&gt;: 100 × 3 × 10 = &lt;strong&gt;3,000 room rows&lt;/strong&gt; (smaller, fixed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why 365 days?&lt;/strong&gt; This is a typical booking window for hotels (1 year ahead). The number is configurable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;90-day window&lt;/strong&gt;: 90 rows per room type (shorter horizon, less storage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;180-day window&lt;/strong&gt;: 180 rows per room type (6 months ahead)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;365-day window&lt;/strong&gt;: 365 rows per room type (1 year ahead, most common)&lt;/li&gt;
&lt;li&gt;The number scales linearly: &lt;code&gt;rows = booking_window_days&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Booking 3 Deluxe Rooms for 2 Nights&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema 1 Dynamic&lt;/strong&gt;: 1 reservation + 1 reservation_rooms + 2 reservation_room_nights = &lt;strong&gt;4 rows&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema 2 Dynamic&lt;/strong&gt;: 1 reservation + &lt;strong&gt;3 room_bookings&lt;/strong&gt; = &lt;strong&gt;4 rows&lt;/strong&gt; (same count, but different structure)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Booking 2 Deluxe + 1 Suite for 2 Nights&lt;/strong&gt; (multiple room types):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema 1 Dynamic&lt;/strong&gt;: 1 reservation + 2 reservation_rooms + (2 × 2) reservation_room_nights = &lt;strong&gt;7 rows&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;1 reservation header&lt;/li&gt;
&lt;li&gt;2 reservation_rooms (one for Deluxe, one for Suite)&lt;/li&gt;
&lt;li&gt;4 reservation_room_nights (2 nights × 2 room types, each with potentially different rates)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema 2 Dynamic&lt;/strong&gt;: 1 reservation + &lt;strong&gt;3 room_bookings&lt;/strong&gt; = &lt;strong&gt;4 rows&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema 1 has &lt;strong&gt;more static rows&lt;/strong&gt; (inventory table) but &lt;strong&gt;fewer dynamic rows per multi-room booking&lt;/strong&gt; (1 reservation_rooms row regardless of N)&lt;/li&gt;
&lt;li&gt;Schema 2 has &lt;strong&gt;fewer static rows&lt;/strong&gt; (rooms table) but &lt;strong&gt;more dynamic rows per multi-room booking&lt;/strong&gt; (N room_bookings rows for N rooms)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4.6 Flexibility and Use Cases
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Schema 1&lt;/th&gt;
&lt;th&gt;Schema 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Room Reassignment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easy (any room of type works)&lt;/td&gt;
&lt;td&gt;Hard (must change room_number)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance Scheduling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flexible (remove from inventory)&lt;/td&gt;
&lt;td&gt;Must track per-room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Room Preferences&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Supported (per-room metadata)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exact Room Assignment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (assigned at check-in)&lt;/td&gt;
&lt;td&gt;Yes (assigned at booking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boutique Hotels&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Less suitable&lt;/td&gt;
&lt;td&gt;More suitable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard Hotels&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;More suitable&lt;/td&gt;
&lt;td&gt;Less suitable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  5. Detailed Code Examples
&lt;/h2&gt;
&lt;h3&gt;
  
  
  5.1 Availability Check: Schema 1
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_availability_schema1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Check if enough rooms are available for all nights.

    Args:
        num_rooms: Number of rooms needed (default: 1)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;nights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_nights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [2024-06-15, 2024-06-16]
&lt;/span&gt;
    &lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT date, available_rooms
        FROM inventory
        WHERE hotel_id = ? 
          AND room_type_id = ?
          AND date IN (?)
          AND available_rooms &amp;gt;= ?
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if all nights have enough availability
&lt;/span&gt;    &lt;span class="n"&gt;available_nights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;available_nights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;available_nights&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientAvailabilityError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not enough rooms available on: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Need &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rooms.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  5.2 Availability Check: Schema 2
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_availability_schema2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Check if enough rooms are available for the date range.

    Args:
        num_rooms: Number of rooms needed (default: 1)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;available_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT COUNT(*) as count
        FROM rooms r
        WHERE r.hotel_id = ? 
          AND r.room_type_id = ?
          AND NOT EXISTS (
              SELECT 1 FROM room_bookings b
              WHERE b.hotel_id = r.hotel_id
                AND b.room_number = r.room_number
                AND b.stay_dates &amp;amp;&amp;amp; daterange(?, ?, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                AND b.status IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checked_in&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)  -- Include pending
          )
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hotel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;available_count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientAvailabilityError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;available_count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rooms available, need &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_rooms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  5.3 Booking Creation: Schema 2 (Full Example)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: See Section 3.6 for the complete implementation with both single-room and multi-room booking examples. The key difference is that Schema 2 requires significantly more complex logic for multi-room bookings compared to Schema 1's atomic &lt;code&gt;UPDATE inventory SET available_rooms = available_rooms - N&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Points&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single room booking: Relatively straightforward with exclusion constraint&lt;/li&gt;
&lt;li&gt;Multi-room booking: Requires finding and locking N rooms, with retry logic if any room becomes unavailable&lt;/li&gt;
&lt;li&gt;See Section 3.6 for the full &lt;code&gt;create_booking_schema2()&lt;/code&gt; implementation with &lt;code&gt;num_rooms&lt;/code&gt; parameter&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  6. When to Use Each Schema
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Use Schema 1 (Row-Per-Day, Fungible) When:
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Standard hotel operations&lt;/strong&gt;: Rooms of the same type are truly interchangeable&lt;br&gt;
✅ &lt;strong&gt;Flexibility needed&lt;/strong&gt;: Want to reassign rooms easily (maintenance, upgrades)&lt;br&gt;
✅ &lt;strong&gt;Simplicity priority&lt;/strong&gt;: Prefer straightforward queries and standard indexes&lt;br&gt;
✅ &lt;strong&gt;High booking volume&lt;/strong&gt;: Need to handle many concurrent bookings efficiently&lt;br&gt;
✅ &lt;strong&gt;Per-night pricing&lt;/strong&gt;: Different rates per night (weekend vs weekday)&lt;br&gt;
✅ &lt;strong&gt;Most hotels&lt;/strong&gt;: Matches how most hotels actually operate&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Use Cases&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large chain hotels (Marriott, Hilton)&lt;/li&gt;
&lt;li&gt;Standard hotel rooms (not unique suites)&lt;/li&gt;
&lt;li&gt;High-volume booking systems&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Use Schema 2 (GiST, Individual Rooms) When:
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Unique rooms&lt;/strong&gt;: Rooms are non-fungible (boutique hotels, unique suites)&lt;br&gt;
✅ &lt;strong&gt;Exact room assignment&lt;/strong&gt;: Need to know which specific room guest will get&lt;br&gt;
✅ &lt;strong&gt;Room-level tracking&lt;/strong&gt;: Need to track maintenance, preferences per room&lt;br&gt;
✅ &lt;strong&gt;Database-enforced integrity&lt;/strong&gt;: Want strongest guarantees against double-booking&lt;br&gt;
✅ &lt;strong&gt;Event venues&lt;/strong&gt;: Similar to fixed-seat event booking (non-fungible)&lt;br&gt;
✅ &lt;strong&gt;Lower booking volume&lt;/strong&gt;: Can handle more complex queries&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Use Cases&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boutique hotels with unique rooms&lt;/li&gt;
&lt;li&gt;Luxury resorts with distinct suites&lt;/li&gt;
&lt;li&gt;Event venues (similar pattern)&lt;/li&gt;
&lt;li&gt;Hotels where guests request specific rooms&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Interview Tidbit: When to Use GiST Indexes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use GiST when a row-per-entry approach is not scalable or not possible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classic Example: Calendar Meeting Booking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine building a calendar system where users can book meetings. You need to check for time slot overlaps (e.g., "Is 2:00 PM - 3:00 PM available?").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;❌ Row-Per-Minute Approach (Not Scalable):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Bad: One row per minute for every possible time slot&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;time_slots&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;minute&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- 0 to 1439 (minutes in a day)&lt;/span&gt;
    &lt;span class="n"&gt;is_booked&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Problem: 1,440 rows per day × 365 days = 525,600 rows per year per resource&lt;/span&gt;
&lt;span class="c1"&gt;-- For 1000 resources: 525 million rows per year!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;✅ GiST Range Approach (Scalable):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Good: One row per booking with date range&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;meetings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;meeting_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resource_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;meeting_time&lt;/span&gt; &lt;span class="n"&gt;TSRANGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Time range: [start_time, end_time)&lt;/span&gt;
    &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meeting_time&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- One row per actual booking - scales with usage, not time granularity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row-per-entry&lt;/strong&gt;: Scales with time granularity (minutes, seconds) × duration × resources → exponential growth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GiST ranges&lt;/strong&gt;: Scales with actual bookings → linear growth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Other Use Cases for GiST:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource booking&lt;/strong&gt;: Conference rooms, equipment, vehicles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP address ranges&lt;/strong&gt;: Network allocation, geolocation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geographic data&lt;/strong&gt;: Spatial queries, map boundaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-series overlaps&lt;/strong&gt;: Scheduling, availability windows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Recommendation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For most hotel booking systems, Schema 1 (Row-Per-Day, Fungible) is recommended&lt;/strong&gt; because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Matches real-world operations&lt;/strong&gt;: Most hotels treat rooms of the same type as interchangeable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler queries&lt;/strong&gt;: Direct date lookups are easier to understand and optimize&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better performance&lt;/strong&gt;: B-tree indexes are well-understood and perform excellently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More flexible&lt;/strong&gt;: Can reassign rooms without database changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proven pattern&lt;/strong&gt;: Widely used in production systems&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Use Schema 2 (GiST, Individual Rooms) only if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have unique, non-fungible rooms&lt;/li&gt;
&lt;li&gt;You need exact room assignment at booking time&lt;/li&gt;
&lt;li&gt;You're building an event venue system (where this design excels)&lt;/li&gt;
&lt;li&gt;Room-level tracking is critical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you need Schema 1's fungible inventory but also want to guarantee no room switching after check-in, see Section 2.6 for a hybrid approach that adds a room assignment table with GiST constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling Beyond a Single Database
&lt;/h3&gt;

&lt;p&gt;If a single database instance cannot sustain throughput/latency requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vertical scale + read replicas&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;reservations&lt;/code&gt;, &lt;code&gt;reservation_rooms&lt;/code&gt;, and &lt;code&gt;reservation_room_nights&lt;/code&gt; on the primary for strong consistency.
&lt;/li&gt;
&lt;li&gt;Serve availability/search queries from read replicas (or a Redis cache) to offload the primary.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Logical partitioning (sharding)&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shard by &lt;code&gt;hotel_id&lt;/code&gt; (most queries are scoped to a hotel).
&lt;/li&gt;
&lt;li&gt;Each shard owns its own &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;reservations&lt;/code&gt;, &lt;code&gt;room_assignments&lt;/code&gt;, etc.
&lt;/li&gt;
&lt;li&gt;Global services (payments, user accounts) remain shared; booking services route to the correct shard via a metadata service.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Regional replication&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy per-region databases (still sharded by hotel) with asynchronous replication for cross-region disaster recovery.
&lt;/li&gt;
&lt;li&gt;Keep writes region-local; replicate summary events to a central analytics store.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Streaming + caches&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publish inventory delta events to Kafka/PubSub.
&lt;/li&gt;
&lt;li&gt;Materialize availability views in Redis/Elastic for search so that the OLTP database handles only booking-critical writes.
&lt;/li&gt;
&lt;li&gt;Ensure cache invalidation by listening to the same event stream.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Background reconciliation&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Periodically reconcile per-shard inventory totals against master room counts (or via nightly jobs) to catch drift.
&lt;/li&gt;
&lt;li&gt;Use idempotent booking operations so retries across shards are safe.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These patterns let you grow from a single-node deployment to multi-shard, multi-region topologies without rewriting the core schemas.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision Factor&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simplicity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 1 (Row-Per-Day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 1 (Row-Per-Day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database-Enforced Integrity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 2 (GiST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exact Room Assignment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 2 (GiST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tie (depends on query pattern)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Control (Single Room)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 2 (GiST) - simpler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Control (Multi-Room)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 1 (atomic decrement) - much simpler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static Rows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 2 (fewer static rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamic Rows (Multi-Room)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 1 (1 row per booking vs N rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-Room Booking Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 1 (atomic decrement vs find/lock N rooms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Most Hotels&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema 1 (Row-Per-Day)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Final Recommendation&lt;/strong&gt;: Start with &lt;strong&gt;Schema 1 (Row-Per-Day, Fungible)&lt;/strong&gt; for most hotel booking systems. Consider &lt;strong&gt;Schema 2 (GiST, Individual Rooms)&lt;/strong&gt; only if you have specific requirements for exact room assignment or unique, non-fungible rooms.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>booking</category>
      <category>postgres</category>
      <category>indexes</category>
    </item>
    <item>
      <title>Hotel Search System Design</title>
      <dc:creator>Sumedh Bala</dc:creator>
      <pubDate>Wed, 19 Nov 2025 20:05:19 +0000</pubDate>
      <link>https://dev.to/sumedhbala/hotel-search-system-design-5ea4</link>
      <guid>https://dev.to/sumedhbala/hotel-search-system-design-5ea4</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Technical Topics Covered (Interview Highlights)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid OLAP/OLTP architecture&lt;/strong&gt; (Sections 1 &amp;amp; 2): Elasticsearch for multi-dimensional search + Redis for real-time availability (and why neither alone is sufficient).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactional Outbox + CDC&lt;/strong&gt; (Section 2.2): Keeping Elasticsearch in sync with the primary DB without dual writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch modeling&lt;/strong&gt; (Sections 3 &amp;amp; 4): Denormalized hotel/room documents, geo fields, amenity facets, script scoring, and index aliasing for zero-downtime reindex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis availability design&lt;/strong&gt; (Section 6): Per-night bitmaps / sorted sets, multi-night intersections, TTL/eviction strategies, and consistency with bookings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query pipeline&lt;/strong&gt; (Section 7): Coordinated ES + Redis flow with fallbacks when availability or index data diverges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced filtering &amp;amp; ranking&lt;/strong&gt; (Section 5): Amenity faceting, price buckets, popularity boosts, personalization hooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling patterns&lt;/strong&gt; (Sections 2 &amp;amp; 7): Multi-region clusters, hot/warm node tiers, cache layers, snapshot/alias-based reindexing. &lt;em&gt;(Cross-region replication—e.g., keeping Redis regional while asynchronously syncing aggregates or using multi-region Elasticsearch—follows the same patterns but is out of scope for this doc.)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite Reading (Shared Components)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This hotel search design builds on the same platform described in &lt;a class="mentioned-user" href="https://dev.to/sumedhbala"&gt;@sumedhbala&lt;/a&gt;’s ticketing series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/designing-a-large-scale-ticketing-system-2h1c"&gt;Designing a Large-Scale Ticketing System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-2-event-discovery-32o5"&gt;Part 2 – Event Discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-3-seat-management-nn2"&gt;Part 3 – Seat Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h"&gt;Part 4 – Payments &amp;amp; Ticket Issuance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From those posts we inherit the core stack: PostgreSQL + transactional outbox, Debezium/Kafka pipelines, Elasticsearch clusters, Redis caches, and Stripe-style payment flows. This document focuses on how those building blocks are reconfigured for hotel-specific search, availability, and ranking concerns.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hotel Search Challenges
&lt;/h3&gt;

&lt;p&gt;Hotel search and booking introduce complexities that differ sharply from event search. Two fundamental differences from event search drive the architectural complexity:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Availability Integration in Search:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Availability Validation&lt;/strong&gt;: Availability must be checked during the search phase, not after results are displayed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date Range Queries&lt;/strong&gt;: Users search for stays spanning multiple nights (check-in to check-out), requiring multi-date availability validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring Availability&lt;/strong&gt;: Same rooms available daily, unlike one-time events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Updates&lt;/strong&gt;: Availability changes constantly as bookings are made, requiring sub-millisecond response times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Extensive Filter Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Amenity Filters&lt;/strong&gt;: Users expect to filter by numerous amenities (pool, spa, beachfront, gym, restaurant, concierge, parking, pet-friendly, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room Type Complexity&lt;/strong&gt;: Multiple room categories (Standard, Deluxe, Suite) with different capacities and room-level amenities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex Filter Combinations&lt;/strong&gt;: Location, amenities, price range, room type, guest count, and date combinations all working together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additional challenges include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fungible Inventory&lt;/strong&gt;: Room 101 and Room 102 of the same type are interchangeable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Pricing&lt;/strong&gt;: Prices vary by date, season, demand, and length of stay&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Key Differences from Event Search
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Event Search&lt;/th&gt;
&lt;th&gt;Hotel Search&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inventory Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed seats, non-fungible&lt;/td&gt;
&lt;td&gt;Room types, fungible inventory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-time events&lt;/td&gt;
&lt;td&gt;Recurring daily availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Date Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single event date&lt;/td&gt;
&lt;td&gt;Date ranges (check-in to check-out)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dynamic per seat/section (demand-based)&lt;/td&gt;
&lt;td&gt;Dynamic per room type and date (seasonal, demand-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple (venue, date, category)&lt;/td&gt;
&lt;td&gt;Complex (location, dates, room type, multiple amenity filters: pool, spa, beachfront, gym, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability in Search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Checked after displaying events&lt;/td&gt;
&lt;td&gt;Must be part of search criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Hybrid Architecture Overview: Mandatory OLAP/OLTP Separation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Terminology refresher&lt;/strong&gt;: &lt;em&gt;OLAP (Online Analytical Processing)&lt;/em&gt; handles read-heavy, multi-dimensional queries (e.g., Elasticsearch), while &lt;em&gt;OLTP (Online Transaction Processing)&lt;/em&gt; powers write-heavy, transactional workloads (e.g., Redis + primary DB). The rest of this document uses these terms frequently.&lt;/p&gt;

&lt;p&gt;Hotel search systems face two fundamentally contradictory workloads that cannot be solved by a single system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. OLAP Workload (Analytical Search):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex, read-intensive, multi-dimensional queries&lt;/li&gt;
&lt;li&gt;Example: "Find all 4-star hotels in New York with pool and free breakfast for 2 adults and 2 children from 2024-10-26 to 2024-10-30, sorted by review score"&lt;/li&gt;
&lt;li&gt;Optimized for: High-speed reads of large datasets, complex filtering, full-text search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technology&lt;/strong&gt;: Elasticsearch (built on Apache Lucene, designed for OLAP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. OLTP Workload (Transactional Availability):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-frequency, simple, isolated transactions&lt;/li&gt;
&lt;li&gt;Example: "Decrement room count for Hotel 123, room type 'King', on date 2024-10-26"&lt;/li&gt;
&lt;li&gt;Optimized for: High-throughput, low-latency updates, absolute data integrity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technology&lt;/strong&gt;: Redis (in-memory database designed for OLTP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Architectural Mandate:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dual-system architecture (Elasticsearch + Redis) is &lt;strong&gt;not optional&lt;/strong&gt;—it is &lt;strong&gt;mandatory&lt;/strong&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OLTP optimizes for write efficiency&lt;/strong&gt; (small, fast, atomic updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OLAP optimizes for read efficiency&lt;/strong&gt; (complex, large-scale, multi-dimensional queries)&lt;/li&gt;
&lt;li&gt;These optimization goals are &lt;strong&gt;directly contradictory&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attempting to use Elasticsearch for OLTP availability updates would fail catastrophically due to its "update penalty" (see Section 3.2). Conversely, using a relational database for OLAP search queries lacks the ability to efficiently perform complex, multi-dimensional, and full-text search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: The combination of OLAP (Elasticsearch) and OLTP (Redis) systems is not merely the "best" solution—it is the &lt;strong&gt;only viable one&lt;/strong&gt; for a business requiring both robust transaction processing and powerful data analysis.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Baseline Functional Solution
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Services
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search API Service&lt;/strong&gt; – Handles user queries, coordinates Elasticsearch and Redis operations, returns ranked results with availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Service&lt;/strong&gt; – Manages Redis availability data, handles booking updates, ensures data consistency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotel Management Service&lt;/strong&gt; – Source of truth for hotel metadata (writes to primary database)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Data synchronization from the primary database to Elasticsearch is handled by an event-driven pipeline (see Section 2.2: Data Synchronization Architecture)&lt;/p&gt;
&lt;h3&gt;
  
  
  Data Stores
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Cluster&lt;/strong&gt; – Search index for hotels, room types, amenities, location data (includes built-in query caching)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Cluster&lt;/strong&gt; (or AWS ElastiCache) – Real-time availability data exclusively&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary Database&lt;/strong&gt; (PostgreSQL/MySQL) – Hotel metadata, room configurations, pricing rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Redis is used exclusively for availability data. Search result caching is handled by Elasticsearch's built-in query cache or application-level caching mechanisms. (This is explained in detail in Section 6.)&lt;/p&gt;
&lt;h3&gt;
  
  
  High-Level Architecture
&lt;/h3&gt;

&lt;p&gt;The system follows a three-tier approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search Tier&lt;/strong&gt;: Elasticsearch handles complex queries (location, amenities, text search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Tier&lt;/strong&gt;: Redis provides real-time availability validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Tier&lt;/strong&gt;: Primary database maintains canonical hotel information&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Soundbite for interviews&lt;/strong&gt;: "Hotel search requires a hybrid architecture because Elasticsearch excels at complex filtering but struggles with real-time availability updates, while Redis provides sub-millisecond availability checks but lacks sophisticated search capabilities."&lt;/p&gt;
&lt;h3&gt;
  
  
  Data Synchronization Architecture: Transactional Outbox + Change Data Capture (CDC)
&lt;/h3&gt;

&lt;p&gt;At a glance, data flows &lt;strong&gt;DB → Outbox → CDC/Debezium → Kafka → Elasticsearch&lt;/strong&gt;. The subsections below break this down so readers can skim the summary and dive deeper only if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Dual-Write Anti-Pattern Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A naive approach would use an "Indexing Service" that attempts to write to both the database and Elasticsearch synchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write A: Update PostgreSQL (hotel name change)
Write B: Update Elasticsearch (hotel name change)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;strong&gt;well-known anti-pattern&lt;/strong&gt; because it cannot guarantee atomic execution across two independent systems. The risk of data inconsistency is not a risk—it is a &lt;strong&gt;guarantee&lt;/strong&gt; over time and at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Scenarios:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Partial Failure (Stale Search Index)&lt;/strong&gt;: Database update succeeds, Elasticsearch update fails → Search index permanently stale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial Failure (Phantom Data)&lt;/strong&gt;: Elasticsearch update succeeds, database update fails → Hotel searchable but doesn't exist in system of record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race Conditions&lt;/strong&gt;: Concurrent updates arrive out of order → Permanent data corruption&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Transactional Outbox Pattern + CDC&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The robust solution decouples writes using an asynchronous, event-driven architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Transactional Outbox Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an &lt;code&gt;Outbox_Events&lt;/code&gt; table in the primary PostgreSQL database&lt;/li&gt;
&lt;li&gt;When updating hotel data, perform &lt;strong&gt;one atomic database transaction&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;-- Business data write&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;Hotels&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'New Name'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;-- Event write (atomic within same transaction)&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;Outbox_Events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{"id": 123, "name": "New Name"}'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This guarantees atomicity: if UPDATE fails, INSERT is rolled back; if INSERT fails, UPDATE is rolled back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Change Data Capture (CDC):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a log-based CDC tool (e.g., Debezium) that reads PostgreSQL's Write-Ahead Log (WAL)&lt;/li&gt;
&lt;li&gt;Debezium sees the committed event in &lt;code&gt;Outbox_Events&lt;/code&gt; and publishes it to Apache Kafka&lt;/li&gt;
&lt;li&gt;This is non-intrusive and low-overhead (reads transaction log, doesn't query database)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Event-Driven Pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL (System of Record)
  ↓ (WAL)
Debezium (CDC Source Connector)
  ↓ (Kafka message)
Apache Kafka (Event Bus - provides resilience and buffering)
  ↓ (Kafka message)
Kafka Connect (Elasticsearch Sink Connector)
  ↓ (index operation)
Elasticsearch (Search Index)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Integrity&lt;/strong&gt;: Only 100% committed data is ever published&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience&lt;/strong&gt;: If Elasticsearch is down, events buffer in Kafka with no data loss&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling&lt;/strong&gt;: Database and Elasticsearch are fully independent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Each component can scale independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eventual Consistency&lt;/strong&gt;: Tunable lag (typically seconds to low minutes) is the correct trade-off for high-performance distributed systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt;: Enable logical replication (&lt;code&gt;wal_level = logical&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debezium&lt;/strong&gt;: Configure to read only from &lt;code&gt;Outbox_Events&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka&lt;/strong&gt;: Long retention period (e.g., 7 days) for disaster recovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Sink&lt;/strong&gt;: Configure for idempotency (&lt;code&gt;pk.mode: record_key&lt;/code&gt;) and delete handling (&lt;code&gt;behavior.on.null.values: delete&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead Letter Queue (DLQ)&lt;/strong&gt;: Route failed messages to DLQ to prevent pipeline halting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: The Transactional Outbox + CDC pattern eliminates the dual-write anti-pattern, providing a production-grade, resilient architecture for data synchronization that guarantees data integrity and system availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Elasticsearch Document Structure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Availability Data Structure Options
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Quick overview&lt;/strong&gt;: We evaluate four availability strategies:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep all availability in the primary database + Redis (Elasticsearch remains metadata-only).
&lt;/li&gt;
&lt;li&gt;Embed detailed availability inside Elasticsearch documents.
&lt;/li&gt;
&lt;li&gt;Hybrid flag in Elasticsearch plus full detail in Redis.
&lt;/li&gt;
&lt;li&gt;Maintain a separate availability-focused Elasticsearch index.
Use this summary to choose which option details to read.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Availability Only in Primary Database and Redis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch&lt;/strong&gt;: No availability data stored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt;: Real-time availability for all date ranges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary Database&lt;/strong&gt;: Source of truth for availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Source of Truth&lt;/strong&gt;: No data synchronization complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always Accurate&lt;/strong&gt;: No stale availability data in Elasticsearch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler Architecture&lt;/strong&gt;: Fewer moving parts, less to maintain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Guarantees&lt;/strong&gt;: Redis provides sub-millisecond updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Post-Filtering Required&lt;/strong&gt;: Must filter Elasticsearch results with Redis availability check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-Step Process&lt;/strong&gt;: Elasticsearch search → Redis availability check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slightly Higher Latency&lt;/strong&gt;: Additional Redis lookup after search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Nested Availability in Elasticsearch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Availability data embedded in room type documents&lt;/li&gt;
&lt;li&gt;Good for simple queries, limited scalability for date ranges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filtering in Search Phase&lt;/strong&gt;: Can filter by availability during Elasticsearch query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Query&lt;/strong&gt;: All filtering happens in one place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Synchronization&lt;/strong&gt;: Must keep Elasticsearch in sync with DB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale Data Risk&lt;/strong&gt;: Availability in Elasticsearch may be outdated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Complexity&lt;/strong&gt;: Every booking requires Elasticsearch update&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index Size&lt;/strong&gt;: Large index with date-based availability fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Hybrid Approach&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch&lt;/strong&gt;: Summary availability flag (e.g., "has_availability_next_30_days: true")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt;: Detailed real-time availability for specific dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary Database&lt;/strong&gt;: Source of truth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initial Filtering&lt;/strong&gt;: Elasticsearch can filter out hotels with no availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Accuracy&lt;/strong&gt;: Redis provides precise availability for date ranges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced Load&lt;/strong&gt;: Fewer hotels passed to Redis for availability check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best of Both Worlds&lt;/strong&gt;: Search performance + real-time accuracy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dual Maintenance&lt;/strong&gt;: Must update both Elasticsearch summary and Redis details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt;: More components to manage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 4: Separate Availability Index in Elasticsearch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dedicated index for availability data&lt;/li&gt;
&lt;li&gt;Better for complex date range queries, requires joins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex Queries&lt;/strong&gt;: Can handle sophisticated date range queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of Concerns&lt;/strong&gt;: Availability data separate from hotel metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Join Overhead&lt;/strong&gt;: Requires joining availability index with hotel index&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronization Complexity&lt;/strong&gt;: Multiple indexes to keep in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Joins are slower than single-index queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recommendation: Option 1 (Redis/DB Only) is Recommended&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Option 1 is the Default Choice:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Availability changes frequently&lt;/strong&gt; - Every booking updates availability in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis is already optimized&lt;/strong&gt; - Sub-millisecond lookups, perfect for real-time data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity wins&lt;/strong&gt; - No sync complexity, no stale data risk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance is acceptable&lt;/strong&gt; - Parallel execution (Elasticsearch + Redis) provides good performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single source of truth&lt;/strong&gt; - Database is the authority, Redis is a performance cache (see Section 6 for database authority details)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Elasticsearch Cannot Handle OLTP Availability Updates: The "Update Penalty"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Fundamental Problem: Elasticsearch's Immutable Segment Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Elasticsearch is built on Apache Lucene, which stores data in immutable segments on disk. This design has a critical consequence: &lt;strong&gt;Elasticsearch does not support in-place updates or deletes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How "Updates" Actually Work in Elasticsearch:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a document is "updated" (e.g., changing &lt;code&gt;num_available_rooms&lt;/code&gt; from 10 to 9), Elasticsearch performs two operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Soft Delete&lt;/strong&gt;: Marks the old document (with 10 rooms) as "deleted"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-index&lt;/strong&gt;: Indexes a brand new document (with 9 rooms) into a new, small segment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means a simple transactional counter change becomes a &lt;strong&gt;full document re-index&lt;/strong&gt;, which is CPU-intensive and requires the entire document to be processed and analyzed again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Compounding Cost: Segment Merging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;High-frequency updates create thousands of tiny segments and massive numbers of soft-deleted documents. This is catastrophic for search performance because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Elasticsearch must open and query every tiny segment&lt;/li&gt;
&lt;li&gt;CPU cycles are wasted filtering out soft-deleted documents&lt;/li&gt;
&lt;li&gt;To combat this, Lucene runs a background &lt;strong&gt;segment merging&lt;/strong&gt; process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Segment Merging Overhead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads small segments&lt;/li&gt;
&lt;li&gt;Filters out soft-deleted documents&lt;/li&gt;
&lt;li&gt;Merges remaining "live" documents into new, larger segments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extremely resource-intensive&lt;/strong&gt;: Consumes substantial CPU, disk I/O, and memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The "Update Penalty" Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Elasticsearch were used for the OLTP availability workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-frequency updates (thousands per minute) would trigger constant, aggressive segment merging&lt;/li&gt;
&lt;li&gt;Intense background CPU and I/O activity would compete directly with primary OLAP search queries&lt;/li&gt;
&lt;li&gt;Result: &lt;strong&gt;Increased query latency and system instability&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why using Elasticsearch for frequently updated, mutable data is a well-known architectural anti-pattern. The OLTP availability workload must be handled by a system designed for high-throughput, low-latency updates—Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to Consider Option 3 (Hybrid with Summary) - Edge Cases Only:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Very high query volume&lt;/strong&gt; - When you need to reduce Redis load by pre-filtering (rare)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large candidate sets&lt;/strong&gt; - When Elasticsearch returns 10,000+ hotels and you want to reduce Redis checks (unusual)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geographic filtering&lt;/strong&gt; - When initial geographic filter yields too many results (can be handled with better Elasticsearch queries)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: For most hotel search systems, Option 1 (Redis/DB Only) is sufficient and preferred due to its simplicity and real-time accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture Pattern (Option 1 - Recommended):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch&lt;/strong&gt;: Filters by location, amenities, room types, price (no availability)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt;: Validates availability for filtered hotels (date range check)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Only hotels with availability are returned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Performance Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch search&lt;/strong&gt;: 25ms (filters by location, amenities, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis availability check&lt;/strong&gt;: 10ms (checks availability for filtered hotels)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total&lt;/strong&gt;: 35ms with parallel execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: For most hotel search systems, keeping availability only in Redis and the primary database is the recommended approach. It provides simplicity, real-time accuracy, and acceptable performance. Only use Elasticsearch for availability if you have very high query volumes and need to reduce Redis load through pre-filtering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hotel Document Schema
&lt;/h3&gt;

&lt;p&gt;Hotel documents in Elasticsearch use nested structures to represent the complex relationships between hotels and their room types:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended Approach (Option 1 - Redis/DB Only):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hotel Document:
├── Basic Information (hotel_id, name, location, rating)
├── Amenities (pool, spa, gym, restaurant)
├── Location Data (geo_point, address, city, country)
└── Room Types (nested array)
    ├── Room Type 1 (room_type_id: "deluxe_001", type: "deluxe", capacity: 2, base_price: 299, amenities: ["wifi", "tv"])
    ├── Room Type 2 (room_type_id: "suite_garden_001", type: "suite", view: "garden", capacity: 4, base_price: 599, amenities: ["wifi", "tv", "balcony"])
    └── Room Type 3 (room_type_id: "suite_ocean_001", type: "suite", view: "ocean", capacity: 4, base_price: 799, amenities: ["wifi", "tv", "balcony", "ocean_view"])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Room types can have variations (e.g., "suite with garden view" vs "suite with ocean view"). Each variation has a unique &lt;code&gt;room_type_id&lt;/code&gt; used for availability tracking in Redis. (See "Important: Room Type ID in All Keys" in Section 6 for detailed explanation.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: No availability data in Elasticsearch. Availability is checked in Redis after Elasticsearch filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternative Approach (Option 3 - Hybrid with Summary):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hotel Document:
├── Basic Information (hotel_id, name, location, rating)
├── Amenities (pool, spa, gym, restaurant)
├── Location Data (geo_point, address, city, country)
└── Room Types (nested array)
    ├── Room Type 1 (room_type_id: "deluxe_001", type: "deluxe", capacity: 2, base_price: 299, amenities: ["wifi", "tv"])
    ├── Room Type 2 (room_type_id: "suite_garden_001", type: "suite", view: "garden", capacity: 4, base_price: 599, amenities: ["wifi", "tv", "balcony"])
    ├── Room Type 3 (room_type_id: "suite_ocean_001", type: "suite", view: "ocean", capacity: 4, base_price: 799, amenities: ["wifi", "tv", "balcony", "ocean_view"])
    └── Availability Summary
        ├── has_availability_next_30_days: true/false
        ├── price_range: {min: 199, max: 599}
        └── last_updated: timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What is Availability Summary? (Only for Option 3 - Hybrid Approach)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If using the hybrid approach, the Availability Summary contains aggregated availability information used for initial filtering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;has_availability_next_30_days&lt;/strong&gt;: Boolean flag indicating if hotel has any availability in the next 30 days&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Purpose: Quickly filter out hotels with no availability&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;true&lt;/code&gt; means hotel has at least one room available in next 30 days&lt;/li&gt;
&lt;li&gt;Updated: Periodically (e.g., every 15 minutes or when significant availability changes)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;price_range&lt;/strong&gt;: Minimum and maximum prices across all available rooms&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Purpose: Pre-filter by price range before detailed Redis check&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;{min: 199, max: 599}&lt;/code&gt; means cheapest room is $199, most expensive is $599&lt;/li&gt;
&lt;li&gt;Updated: When room prices change significantly&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;last_updated&lt;/strong&gt;: Timestamp of when summary was last refreshed&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Purpose: Track data freshness, implement cache invalidation&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;2024-06-15T10:30:00Z&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important Notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not Real-Time&lt;/strong&gt;: Summary is updated periodically, not on every booking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coarse Filtering&lt;/strong&gt;: Used to eliminate hotels with no availability, not for final availability check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Still Required&lt;/strong&gt;: Detailed availability check still happens in Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off&lt;/strong&gt;: Summary reduces Redis load but adds sync complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When Availability Summary is Updated:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Periodic refresh (every 15-30 minutes)&lt;/li&gt;
&lt;li&gt;When availability drops to zero (no rooms available)&lt;/li&gt;
&lt;li&gt;When availability becomes available after being zero&lt;/li&gt;
&lt;li&gt;Manual refresh triggered by significant booking events&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Understanding Nested Structures (Beginner-Friendly)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is a Nested Structure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you have a hotel document. Without nested structures, you might store room types like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hotel: "Luxury Hotel"
Room Types: ["deluxe", "suite", "standard"]
Room Prices: [299, 599, 199]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: If you search for hotels with "deluxe" rooms under $300, Elasticsearch might match this hotel incorrectly. Why? Because it sees "deluxe" (✓) and "$199" (✓) in the arrays, but doesn't know that "$199" belongs to "standard", not "deluxe". It treats all array items as independent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution - Nested Structures:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With nested structures, each room type (including variations) is stored as a separate "mini-document" inside the hotel document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hotel: "Luxury Hotel"
Room Types (nested):
  - Room Type 1: {room_type_id: "deluxe_001", type: "deluxe", price: 299, capacity: 2}
  - Room Type 2: {room_type_id: "suite_garden_001", type: "suite", view: "garden", price: 599, capacity: 4}
  - Room Type 3: {room_type_id: "suite_ocean_001", type: "suite", view: "ocean", price: 799, capacity: 4}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you search for "deluxe rooms under $300", Elasticsearch checks each room type as a complete unit. It finds "deluxe" with price "$299" as a matched pair, which is correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Room Type Variations:&lt;/strong&gt;&lt;br&gt;
The same room type category (e.g., "suite") can have multiple variations with different amenities or views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"suite with garden view" (room_type_id: "suite_garden_001")&lt;/li&gt;
&lt;li&gt;"suite with ocean view" (room_type_id: "suite_ocean_001")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each variation has its own unique &lt;code&gt;room_type_id&lt;/code&gt; used for availability tracking in Redis, allowing separate availability management for each variation. (See Section 6 for how &lt;code&gt;room_type_id&lt;/code&gt; is used in Redis keys.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-World Analogy:&lt;/strong&gt;&lt;br&gt;
Think of a hotel document as a filing cabinet. Without nesting, all room information is in one big drawer mixed together. With nesting, each room type has its own folder in the drawer, keeping related information together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Does It Help?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accurate Filtering&lt;/strong&gt;: When filtering by "deluxe rooms under $300", you get hotels that actually have deluxe rooms at that price, not hotels that have a deluxe room AND a cheaper room (but unrelated).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Independent Queries&lt;/strong&gt;: You can ask "show me hotels where the suite has ocean view but the standard room doesn't" - nested structures allow you to query each room type independently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better Performance&lt;/strong&gt;: Elasticsearch can optimize queries better because it knows which fields belong together, reducing false matches.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Why Nested Model Over Parent-Child: Optimizing for OLAP Reads
&lt;/h3&gt;

&lt;p&gt;Elasticsearch offers two options for modeling one-to-many relationships (hotel → room types): &lt;strong&gt;Nested&lt;/strong&gt; and &lt;strong&gt;Parent-Child&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested Model (Selected):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hotel and room types stored as a &lt;strong&gt;single document&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Parent (hotel) and children (rooms) are &lt;strong&gt;co-located in the same Lucene block&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Significantly faster queries, low memory overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: High update cost—updating any field forces re-indexing of the entire document (hotel + all rooms)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Parent-Child Model (Rejected):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hotel (parent) and rooms (children) indexed as &lt;strong&gt;separate documents&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Low update cost—updating one child only re-indexes that child&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Slower queries due to join overhead, higher memory overhead (requires in-memory "join list")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Decision: OLTP Workload Removed = No Compromise Needed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Nested vs Parent-Child debate is a microcosm of the entire OLAP/OLTP problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested model&lt;/strong&gt; optimizes for reads (OLAP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parent-Child model&lt;/strong&gt; compromises on read performance to gain update efficiency (a step towards OLTP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Because the high-frequency OLTP workload (availability) has been completely removed and placed in Redis&lt;/strong&gt;, the system is no longer forced to compromise. The correct choice is to &lt;strong&gt;optimize the search index for its primary, read-heavy workload&lt;/strong&gt;. Therefore, the &lt;strong&gt;Nested model is used&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rationale:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrequent updates to static hotel data (handled by asynchronous CDC pipeline) will incur the higher re-indexing cost&lt;/li&gt;
&lt;li&gt;This is a worthwhile trade-off for maximizing query performance for all users&lt;/li&gt;
&lt;li&gt;The update penalty only affects static data changes (hotel name, amenities), not high-frequency availability updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Index Refresh Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For this OLAP index, &lt;code&gt;index.refresh_interval&lt;/code&gt; can be set to a high value (e.g., 30s or 60s) or disabled during bulk loads&lt;/li&gt;
&lt;li&gt;A high-frequency OLTP workload would require a low interval (e.g., default 1s), triggering constant, costly refreshes&lt;/li&gt;
&lt;li&gt;With OLTP removed, the index can be tuned for maximum indexing performance and stability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: By moving the OLTP availability workload to Redis, Elasticsearch can be fully optimized for OLAP reads using the Nested model, without compromising on update performance for the primary search workload.&lt;/p&gt;
&lt;h3&gt;
  
  
  Field Mapping Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is Field Mapping?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Field mapping tells Elasticsearch how to store and index each field in your documents. Different field types enable different query capabilities and performance characteristics. Choosing the right field type is crucial for search performance and accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field Type Breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;hotel_id&lt;/strong&gt;: keyword (exact matching, aggregations)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Hotel IDs are unique identifiers that need exact matching only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: "Find hotel with ID 'hotel_123'" or "Count hotels by ID"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Very fast exact lookups, no text analysis overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;name&lt;/strong&gt;: text with keyword subfield (full-text search + exact matching)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Hotel names need both fuzzy text search AND exact matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text field&lt;/strong&gt;: Handles typos, partial matches, relevance scoring (e.g., "Luxury" matches "Luxurious")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;keyword subfield&lt;/strong&gt;: Exact matching for autocomplete, sorting, or filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Text search finds "Luxury Downtown Hotel" even if user types "Luxry Downtown", while keyword enables exact filtering&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;location&lt;/strong&gt;: geo_point (geospatial queries, distance calculations)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Geographic coordinates need special handling for distance queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: "Find hotels within 5km of coordinates [34.0522, -118.2437]"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Optimized spatial indexing for fast distance calculations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;amenities&lt;/strong&gt;: keyword array (filtering, faceted search)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Amenities are standardized discrete values (e.g., ["pool", "spa", "gym"]) that need exact matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Filter by "pool AND spa" - must match exact amenity values, not partial text matches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not text&lt;/strong&gt;: &lt;/li&gt;
&lt;li&gt;Text analysis would tokenize values, breaking exact matching needed for filtering&lt;/li&gt;
&lt;li&gt;Amenities are categorical data (not free-form text), so they should be matched exactly&lt;/li&gt;
&lt;li&gt;Example: If "pool table" appears in hotel description, &lt;code&gt;text&lt;/code&gt; type might match "pool" filter incorrectly in the wrong field (pool table = billiards, not swimming pool)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Fast exact matching for multiple amenities simultaneously&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;room_types&lt;/strong&gt;: nested object (complex room type queries)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Room types have their own attributes (price, capacity, amenities) that need independent filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: "Find hotels with deluxe rooms under $300" - must check room type AND price together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Enables filtering within nested documents without false matches&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;rating&lt;/strong&gt;: float (range queries, sorting)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Ratings are numeric values that need range queries and sorting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: "Find hotels with rating &amp;gt;= 4.0" or "Sort by rating descending"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Optimized for numeric comparisons and sorting operations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Right field type = Accurate results + Fast queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Each field type is optimized for specific query patterns. Text fields handle fuzzy matching, keyword fields handle exact matching, geo_point handles distance calculations, and nested objects handle complex relationships. Choosing the right type ensures both query accuracy and performance.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Elasticsearch Index Design
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Index Mapping Configuration
&lt;/h3&gt;

&lt;p&gt;The hotel index uses a carefully designed mapping to optimize for different query patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text Fields&lt;/strong&gt;: Standard analyzer for full-text search with stemming and stop words

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stemming&lt;/strong&gt;: Reduces words to their root form (e.g., "swimming" → "swim", "pools" → "pool") so that variations of the same word match&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop Words&lt;/strong&gt;: Removes common words like "the", "and", "is" that don't add search value&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword Fields&lt;/strong&gt;: Exact matching for filters and aggregations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo Fields&lt;/strong&gt;: Geo-point mapping for distance-based queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested Fields&lt;/strong&gt;: Specialized mapping for parent-child relationships&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Nested Document Configuration
&lt;/h3&gt;

&lt;p&gt;From an index design perspective, nested documents provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-level Filtering&lt;/strong&gt;: Enables filtering by both hotel amenities (pool, spa) and room-level amenities (wifi, minibar) simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Nested queries are optimized for parent-child relationships with efficient BitSet operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Easy to add new room types or room attributes without schema changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: The index design balances search performance with query flexibility, using appropriate field types for different use cases. For the recommended approach (Option 1), availability data is not stored in Elasticsearch - it's managed entirely in Redis and the primary database for real-time accuracy.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Query Patterns and Optimization
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Filter-First Execution Strategy
&lt;/h3&gt;

&lt;p&gt;Elasticsearch optimizes query performance by executing filters before expensive scoring operations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution Order:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Filter Context&lt;/strong&gt;: Execute all filters, create BitSets (fast, cached)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Context&lt;/strong&gt;: Execute text search, create BitSets (fast, cached)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BitSet Intersection&lt;/strong&gt;: Combine filters with AND operations (very fast)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring Phase&lt;/strong&gt;: Apply BM25 scoring only to filtered results (expensive but small set)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Performance Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Without Filter-First&lt;/strong&gt;: Score 50,000 documents, then filter to 1,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With Filter-First&lt;/strong&gt;: Filter to 1,000 documents, then score only those&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Improvement&lt;/strong&gt;: 25-50x faster execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Search for "Luxury hotels with pool and spa in Los Angeles"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Components:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text search: "Luxury" (query context - needs scoring)&lt;/li&gt;
&lt;li&gt;Location filter: "Los Angeles" (filter context - no scoring)&lt;/li&gt;
&lt;li&gt;Amenity filter: "pool AND spa" (filter context - no scoring)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Without Filter-First (Inefficient):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Text search finds 50,000 hotels with "luxury" in name/description&lt;/li&gt;
&lt;li&gt;BM25 scoring calculated for all 50,000 hotels (expensive!)&lt;/li&gt;
&lt;li&gt;Field boosting applied to all 50,000 hotels&lt;/li&gt;
&lt;li&gt;Results ranked: Hotel_A (score 0.95), Hotel_B (score 0.92), ...&lt;/li&gt;
&lt;li&gt;Location filter applied: 50,000 → 2,000 hotels in Los Angeles&lt;/li&gt;
&lt;li&gt;Amenity filter applied: 2,000 → 800 hotels with pool AND spa&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total cost&lt;/strong&gt;: 50,000 scoring operations + ranking + filtering&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;With Filter-First (Optimized):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Location filter: Create BitSet A of hotels in "Los Angeles" → 2,000 hotels (fast, cached)&lt;/li&gt;
&lt;li&gt;Amenity filter: Create BitSet B of hotels with "pool AND spa" → 5,000 hotels (fast, cached)&lt;/li&gt;
&lt;li&gt;Text search: Create BitSet C of hotels containing "luxury" → 50,000 hotels (fast - from inverted index)&lt;/li&gt;
&lt;li&gt;BitSet intersection: A ∩ B ∩ C = 600 hotels (very fast - bitwise AND operation)&lt;/li&gt;
&lt;li&gt;BM25 scoring: Calculate scores for only 600 hotels (expensive but small set)&lt;/li&gt;
&lt;li&gt;Field boosting: Apply to only 600 hotels&lt;/li&gt;
&lt;li&gt;Results ranked: Final 600 hotels with scores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total cost&lt;/strong&gt;: 600 scoring operations (25-50x fewer than without filter-first)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text search BitSet creation&lt;/strong&gt;: Still processes ALL documents (50,000) from inverted index - this is fast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter BitSet creation&lt;/strong&gt;: Processes ALL documents (fast, uses doc values)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BitSet intersection&lt;/strong&gt;: Very fast - just bitwise AND operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring optimization&lt;/strong&gt;: Only scores the 600 hotels in the intersection, not all 50,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The magic&lt;/strong&gt;: BitSet operations are fast even on large sets, but scoring is expensive - so we minimize scoring by intersecting BitSets first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why This Works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filters are fast&lt;/strong&gt;: BitSet operations are O(1) per document&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring is expensive&lt;/strong&gt;: BM25 calculation is O(term_frequency) per document&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce scoring set&lt;/strong&gt;: Filter first to minimize expensive operations
&lt;strong&gt;Interview Takeaway: BitSet Caching and Precomputation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching Strategy&lt;/strong&gt;: BitSets are cached for repeated queries (85-90% cache hit rate for popular filters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precomputation Opportunity&lt;/strong&gt;: Amenity-based BitSets can be precomputed since amenities don't change frequently

&lt;ul&gt;
&lt;li&gt;Common filters like "pool", "spa", "gym" can be precomputed and stored in memory&lt;/li&gt;
&lt;li&gt;Only updated when hotel amenities are added/removed (rare event)&lt;/li&gt;
&lt;li&gt;Provides instant filter execution without recalculating BitSets on every query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Insight&lt;/strong&gt;: Identify filters that change infrequently (amenities, location) vs. frequently (availability, price) - precompute the stable ones&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query latency&lt;/strong&gt;: 200ms → 30ms (6.7x faster)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU usage&lt;/strong&gt;: 50,000 scoring operations → 600 operations (83x reduction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: Cached BitSets reused for popular queries (85-90% cache hit rate)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Text Search with Multi-Match
&lt;/h3&gt;

&lt;p&gt;Multi-match queries handle complex text search across multiple fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field Boosting&lt;/strong&gt;: Hotel name gets higher weight than description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Types&lt;/strong&gt;: best_fields, most_fields, cross_fields for different matching strategies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy Matching&lt;/strong&gt;: Automatic typo tolerance and synonym expansion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrase Matching&lt;/strong&gt;: Exact phrase matching with proximity scoring&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Nested Queries for Room Type Filtering
&lt;/h3&gt;

&lt;p&gt;Nested queries enable complex filtering within room type documents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Room Type Filtering&lt;/strong&gt;: Find hotels with specific room types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity Filtering&lt;/strong&gt;: Filter by guest count requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amenity Filtering&lt;/strong&gt;: Room-level amenities (wifi, minibar, ocean_view)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price Range Filtering&lt;/strong&gt;: Filter by room type pricing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  How Filtering Works (Inverted Index vs Filtering)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Understanding the Difference:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text Search (Uses Inverted Index):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inverted Index&lt;/strong&gt;: Maps each word → list of documents containing that word&lt;/li&gt;
&lt;li&gt;Example: "deluxe" → [Hotel_A, Hotel_B, Hotel_C]&lt;/li&gt;
&lt;li&gt;Used for: Finding documents that contain specific words&lt;/li&gt;
&lt;li&gt;Fast for: "Find hotels with 'deluxe' in the name"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Filtering (Uses Doc Values):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Doc Values&lt;/strong&gt;: Columnar storage format - stores all values for a field together&lt;/li&gt;
&lt;li&gt;Example: All room prices stored in one column: [299, 599, 199, 450, ...]&lt;/li&gt;
&lt;li&gt;Used for: Exact matching, range queries, aggregations&lt;/li&gt;
&lt;li&gt;Fast for: "Find hotels with deluxe rooms under $300"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why Different Data Structures?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inverted indexes are optimized for text search (finding which documents contain terms), but they're inefficient for filtering operations like numeric comparisons or exact matches. Doc values store data in a columnar format that's optimized for filtering, sorting, and aggregations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison to Columnar Databases:&lt;/strong&gt;&lt;br&gt;
Elasticsearch's doc values use a similar storage strategy to columnar databases (e.g., Amazon Redshift, Google BigQuery, ClickHouse):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Columnar Storage&lt;/strong&gt;: All values for a field are stored together in a column (e.g., all prices: [299, 599, 199, 450])&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized for Analytics&lt;/strong&gt;: Fast filtering, sorting, and aggregations on numeric/categorical fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Difference&lt;/strong&gt;: Columnar databases store ALL data in columnar format, while Elasticsearch uses a hybrid approach:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inverted indexes&lt;/strong&gt; (row-oriented) for text search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doc values&lt;/strong&gt; (columnar) for filtering/aggregations&lt;/li&gt;
&lt;li&gt;This hybrid approach gives Elasticsearch both fast text search AND fast filtering in one system&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: "Deluxe Rooms Under $300" Query&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's trace through how Elasticsearch handles this query:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Understanding the Data Structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hotel Document:
  hotel_id: "hotel_123"
  name: "Luxury Hotel"
  room_types (nested):
    - {type: "deluxe", price: 299, capacity: 2}
    - {type: "suite", price: 599, capacity: 4}
    - {type: "standard", price: 199, capacity: 2}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pricing Reality (Interview Tip)&lt;/strong&gt;: In production the &lt;code&gt;price&lt;/code&gt; stored in Elasticsearch is usually a summary (&lt;code&gt;min_price_next_30_days&lt;/code&gt;, &lt;code&gt;max_price&lt;/code&gt;, or a bucketed price range). This lets Elasticsearch filter aggressively without forcing constant reindexing. The precise per-date price (and availability) comes from Redis/the pricing service during the availability step, ensuring tomorrow’s rate (e.g., $400) is accurate even if Elasticsearch still lists $300 as the minimum. The rule of thumb: &lt;strong&gt;use Elasticsearch for coarse filtering, Redis for rapidly-changing truth.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Nested Query Execution (Using BitSet Operations)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Access Nested Documents&lt;/strong&gt;: Elasticsearch treats each nested room type as a separate "mini-document"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Room Type 1: {type: "deluxe", price: 299}&lt;/li&gt;
&lt;li&gt;Room Type 2: {type: "suite", price: 599}&lt;/li&gt;
&lt;li&gt;Room Type 3: {type: "standard", price: 199}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Filter by Type (BitSet Creation)&lt;/strong&gt;: Using doc values, create BitSet of nested documents with type = "deluxe"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Room Type 1: type = "deluxe" ✓ → BitSet bit 1 = true&lt;/li&gt;
&lt;li&gt;Room Type 2: type = "suite" ✗ → BitSet bit 2 = false&lt;/li&gt;
&lt;li&gt;Room Type 3: type = "standard" ✗ → BitSet bit 3 = false&lt;/li&gt;
&lt;li&gt;Result: BitSet A = &lt;a href="https://dev.toonly%20Room%20Type%201%20matches"&gt;1, 0, 0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Filter by Price (BitSet Creation)&lt;/strong&gt;: Using doc values, create BitSet of nested documents with price &amp;lt; 300&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Room Type 1: price = 299 &amp;lt; 300 ✓ → BitSet bit 1 = true&lt;/li&gt;
&lt;li&gt;Room Type 2: price = 599 &amp;lt; 300 ✗ → BitSet bit 2 = false&lt;/li&gt;
&lt;li&gt;Room Type 3: price = 199 &amp;lt; 300 ✓ → BitSet bit 3 = true&lt;/li&gt;
&lt;li&gt;Result: BitSet B = &lt;a href="https://dev.toRoom%20Type%201%20and%203%20match"&gt;1, 0, 1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BitSet Intersection&lt;/strong&gt;: A ∩ B = [1, 0, 0] ∩ [1, 0, 1] = [1, 0, 0]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only Room Type 1 matches both conditions (bitwise AND operation)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Return Parent Document&lt;/strong&gt;: If any nested document matches, the parent hotel document is included&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Result: Hotel_123 is returned&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Doc Values&lt;/strong&gt;: Used to create BitSets for filtering operations (fast, cached)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BitSet Operations&lt;/strong&gt;: Filtering by type and price uses BitSet intersection (bitwise AND)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested Documents&lt;/strong&gt;: Each room type is stored separately, allowing independent BitSet creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: BitSet operations are O(1) per document, much faster than scoring all documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combination&lt;/strong&gt;: The query can use both - text search for hotel names AND BitSet filtering for room types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance Comparison:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Data Structure&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text search ("deluxe" in name)&lt;/td&gt;
&lt;td&gt;Inverted Index&lt;/td&gt;
&lt;td&gt;Very Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter (price &amp;lt; 300)&lt;/td&gt;
&lt;td&gt;Doc Values&lt;/td&gt;
&lt;td&gt;Very Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter (type = "deluxe")&lt;/td&gt;
&lt;td&gt;Doc Values&lt;/td&gt;
&lt;td&gt;Very Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested filter (deluxe AND price &amp;lt; 300)&lt;/td&gt;
&lt;td&gt;Doc Values + Nested&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inverted indexes are great for "find documents containing words"&lt;/li&gt;
&lt;li&gt;Doc values are great for "find documents matching criteria"&lt;/li&gt;
&lt;li&gt;Nested structures allow filtering on related data (room types) independently&lt;/li&gt;
&lt;li&gt;Combining both gives you powerful search + accurate filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Query Execution Order and Performance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Optimal Query Structure (Option 1 - Recommended):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Geographic Filters&lt;/strong&gt;: city, radius (most selective)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amenity Filters&lt;/strong&gt;: pool, spa, gym (moderately selective)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room Type Filters&lt;/strong&gt;: nested queries (selective)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text Search&lt;/strong&gt;: multi-match queries (expensive)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring&lt;/strong&gt;: BM25 calculation (expensive but small set)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Date range validation is handled by Redis after Elasticsearch filtering, not during the Elasticsearch query phase. This aligns with Option 1 (Redis/DB Only) where availability is not stored in Elasticsearch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Filter-first execution with BitSet caching provides 25-50x performance improvement by reducing expensive scoring operations to only the documents that pass all filters. Detailed BitSet mechanics and caching strategies are explained in the "Filter-First Execution Strategy" section above.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Redis Availability Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Architecture Note&lt;/strong&gt;: Redis (or AWS ElastiCache) is used exclusively for availability data. Search result caching and other data caching are handled by Elasticsearch's built-in caching mechanisms or application-level caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis Data Structures for Availability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important: Room Type ID in All Keys&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All Redis availability keys include &lt;code&gt;room_type_id&lt;/code&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hotels have multiple room types and variations (e.g., "suite with garden view" vs "suite with ocean view") with separate availability&lt;/li&gt;
&lt;li&gt;Each room type variation has a unique &lt;code&gt;room_type_id&lt;/code&gt; (e.g., "suite_garden_001", "suite_ocean_001")&lt;/li&gt;
&lt;li&gt;Elasticsearch filters by room type and returns &lt;code&gt;room_type_id&lt;/code&gt;, which is used to build Redis keys&lt;/li&gt;
&lt;li&gt;This allows separate availability tracking for each variation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis uses multiple data structures optimized for availability patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple Key-Value Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key Format&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id:check_in:check_out&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt;: "1" (at least one room available) or number of available rooms (e.g., "5" for 5 rooms available) or null (unavailable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Simple availability checks (not recommended due to update complexity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(1) lookup time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How the Key Gets Created:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Booking Event&lt;/strong&gt;: When a booking is made or cancelled for a specific room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room Type ID from Elasticsearch&lt;/strong&gt;: Elasticsearch nested query filters hotels by room type and returns &lt;code&gt;room_type_id&lt;/code&gt; (e.g., "suite_garden_001", "suite_ocean_001")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Generation&lt;/strong&gt;: Combine hotel_id, room_type_id, check_in date, and check_out date

&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;availability:hotel_123:suite_garden_001:2024-06-15:2024-06-17&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Check&lt;/strong&gt;: Query database for availability of that room type variation across all dates in range&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Creation&lt;/strong&gt;: If all dates are available for that room type variation, set key with value "1" (at least one available) or the number of available rooms (e.g., "5" for 5 rooms)

&lt;ul&gt;
&lt;li&gt;Redis command: &lt;code&gt;SET availability:hotel_123:suite_garden_001:2024-06-15:2024-06-17 "1" EX 14400&lt;/code&gt; (4 hour TTL) - for at least one room&lt;/li&gt;
&lt;li&gt;Redis command: &lt;code&gt;SET availability:hotel_123:suite_garden_001:2024-06-15:2024-06-17 "5" EX 14400&lt;/code&gt; (4 hour TTL) - for 5 available rooms&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Deletion&lt;/strong&gt;: If any date becomes unavailable for that room type variation, delete the key

&lt;ul&gt;
&lt;li&gt;Redis command: &lt;code&gt;DEL availability:hotel_123:suite_garden_001:2024-06-15:2024-06-17&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Trigger&lt;/strong&gt;: Keys are created/updated/deleted when:

&lt;ul&gt;
&lt;li&gt;Bookings are confirmed for specific room type variations&lt;/li&gt;
&lt;li&gt;Bookings are cancelled for specific room type variations&lt;/li&gt;
&lt;li&gt;New availability is added for specific room type variations&lt;/li&gt;
&lt;li&gt;Periodic sync from database&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Challenge: Finding Which Keys to Modify - Combinatorial Explosion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
When a booking is made for a specific date and room type variation (e.g., 2024-06-16, suite with ocean view), you need to invalidate all keys that include that date for that room type variation. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combinatorial Explosion Example:&lt;/strong&gt;&lt;br&gt;
For a 30-day booking window, there are (30 × 31) / 2 = 465 possible check-in/check-out combinations. A single booking on one day (e.g., June 16th) would require finding and invalidating every single one of these 465 keys that contains "June 16th". This creates a combinatorial explosion of keys that must be tracked and updated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Keys to Invalidate:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;availability:hotel_123:suite_ocean_001:2024-06-15:2024-06-17&lt;/code&gt; (includes 2024-06-16)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;availability:hotel_123:suite_ocean_001:2024-06-16:2024-06-18&lt;/code&gt; (includes 2024-06-16)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;availability:hotel_123:suite_ocean_001:2024-06-14:2024-06-17&lt;/code&gt; (includes 2024-06-16)&lt;/li&gt;
&lt;li&gt;... and 462 more keys for a 30-day window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But Redis doesn't provide an efficient way to find all keys matching a date pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution Options:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Pattern Matching (Inefficient)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;KEYS availability:hotel_123:*&lt;/code&gt; to find all keys for a hotel&lt;/li&gt;
&lt;li&gt;Filter keys that include the affected date&lt;/li&gt;
&lt;li&gt;Delete matching keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: &lt;code&gt;KEYS&lt;/code&gt; command is O(N) and blocks Redis, not suitable for production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Secondary Index (Recommended)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain a separate index tracking which keys exist for each date and room type variation&lt;/li&gt;
&lt;li&gt;Use a hash: &lt;code&gt;availability_index:hotel_123:suite_ocean_001:2024-06-16&lt;/code&gt; → Set of all keys containing this date for this room type variation&lt;/li&gt;
&lt;li&gt;When booking affects 2024-06-16 for suite with ocean view:

&lt;ol&gt;
&lt;li&gt;Get all keys from index: &lt;code&gt;SMEMBERS availability_index:hotel_123:suite_ocean_001:2024-06-16&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delete all those keys: &lt;code&gt;DEL key1 key2 key3...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove index entry: &lt;code&gt;DEL availability_index:hotel_123:suite_ocean_001:2024-06-16&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When creating a key&lt;/strong&gt;: Add key to index for each date in range and room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(M) where M = number of keys containing that date for that room type variation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Hash Structure (Better Alternative) - Solves Combinatorial Explosion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use hash structure instead of simple keys (see Hash Structure section)&lt;/li&gt;
&lt;li&gt;Key format: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hash fields: Individual dates (2024-06-15, 2024-06-16, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solves the Problem&lt;/strong&gt;: Instead of 465 keys for a 30-day window, you have only 30 hash fields (one per day)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When booking affects 2024-06-16 for suite with garden view&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;Simply update/delete the hash field: &lt;code&gt;HDEL availability:hotel_123:suite_garden_001 2024-06-16&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No need to find which keys to modify - directly update the specific date field&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(1) - directly access the date field for specific room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Linear growth (30 fields for 30 days) vs. quadratic growth (465 keys for 30 days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt;&lt;br&gt;
For simple availability checks, &lt;strong&gt;Hash Structure is preferred&lt;/strong&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need to track which keys to invalidate&lt;/li&gt;
&lt;li&gt;Direct date-based access (O(1))&lt;/li&gt;
&lt;li&gt;Easier to update individual dates&lt;/li&gt;
&lt;li&gt;More efficient for date range queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hash Structure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key Format&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Fields&lt;/strong&gt;: Individual dates representing nights (2024-06-15, 2024-06-16, 2024-06-17, etc.)

&lt;ul&gt;
&lt;li&gt;Each date field represents availability for that night&lt;/li&gt;
&lt;li&gt;Example: Field "2024-06-15" = availability for the night of June 15th&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Values&lt;/strong&gt;: JSON object with &lt;code&gt;room_count&lt;/code&gt; and &lt;code&gt;last_updated&lt;/code&gt; (price comes from Elasticsearch/database, not Redis)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Detailed availability per room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(1) field access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;availability:hotel_123:deluxe_001&lt;/code&gt; (for deluxe rooms)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;availability:hotel_123:suite_garden_001&lt;/code&gt; (for suite with garden view)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;availability:hotel_123:suite_ocean_001&lt;/code&gt; (for suite with ocean view)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantage: Direct Date-Based Updates&lt;/strong&gt;&lt;br&gt;
Unlike the simple key-value pattern, hash structure allows direct updates to specific dates without needing to find which keys to modify. When a booking affects 2024-06-16 for a specific room type variation (e.g., suite with garden view), you directly update that hash field - no pattern matching or secondary indexes needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the Hash Gets Created:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial Creation&lt;/strong&gt;: When hotel availability data is loaded into Redis for a specific room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Key&lt;/strong&gt;: Create hash with key &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;availability:hotel_123:suite_garden_001&lt;/code&gt; (for suite with garden view)&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;availability:hotel_123:suite_ocean_001&lt;/code&gt; (for suite with ocean view)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Field Population&lt;/strong&gt;: For each date with availability, add a field:

&lt;ul&gt;
&lt;li&gt;Redis command: &lt;code&gt;HSET availability:hotel_123:suite_garden_001 2024-06-15 '{"room_count":5,"last_updated":"2024-06-15T10:30:00Z"}'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Redis command: &lt;code&gt;HSET availability:hotel_123:suite_garden_001 2024-06-16 '{"room_count":3,"last_updated":"2024-06-15T10:30:00Z"}'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field Updates&lt;/strong&gt;: When availability changes for that room type variation:

&lt;ul&gt;
&lt;li&gt;Update specific date field: &lt;code&gt;HSET availability:hotel_123:suite_garden_001 2024-06-15 '{"room_count":4,"last_updated":"2024-06-15T14:20:00Z"}'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field Deletion&lt;/strong&gt;: When date becomes unavailable for that room type variation:

&lt;ul&gt;
&lt;li&gt;Remove field: &lt;code&gt;HDEL availability:hotel_123:suite_garden_001 2024-06-17&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL&lt;/strong&gt;: Set expiration on entire hash: &lt;code&gt;EXPIRE availability:hotel_123:suite_garden_001 14400&lt;/code&gt; (4 hours)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Triggers&lt;/strong&gt;: Hash is updated when:

&lt;ul&gt;
&lt;li&gt;Bookings change availability for specific room type variations and dates&lt;/li&gt;
&lt;li&gt;Periodic batch sync from database (availability data only)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Atomic Operations for OLTP Transactions: Redis Hash Design
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The OLTP Transaction Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The entire booking transaction (decrementing room count) is a &lt;strong&gt;single, atomic Redis command&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HINCRBY availability:hotel_123:suite_garden_001 2024-06-15 -1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why This Works for OLTP:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;O(1) Performance&lt;/strong&gt;: Constant time operation, designed for high-throughput scenarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic&lt;/strong&gt;: The increment/decrement operation is atomic—no race conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Operation&lt;/strong&gt;: No multi-step process, no re-indexing, no segment merging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conceptual Opposite of Elasticsearch&lt;/strong&gt;: This is the exact opposite of Elasticsearch's multi-stage, resource-intensive re-indexing "update"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hash Structure Design:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Key&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt; (e.g., &lt;code&gt;availability:hotel_123:suite_garden_001&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Fields&lt;/strong&gt;: &lt;code&gt;{date}&lt;/code&gt; (e.g., &lt;code&gt;2024-06-15&lt;/code&gt;, &lt;code&gt;2024-06-16&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Values&lt;/strong&gt;: Integer representing room count (e.g., &lt;code&gt;10&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Alternative: JSON Value with Room Count&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The document shows JSON values with &lt;code&gt;room_count&lt;/code&gt; and &lt;code&gt;last_updated&lt;/code&gt;. For pure OLTP counter operations, you can also use simple integer values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple Integer&lt;/strong&gt;: &lt;code&gt;HSET availability:hotel_123:suite_garden_001 2024-06-15 10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Decrement&lt;/strong&gt;: &lt;code&gt;HINCRBY availability:hotel_123:suite_garden_001 2024-06-15 -1&lt;/code&gt; → Result: &lt;code&gt;9&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Increment&lt;/strong&gt;: &lt;code&gt;HINCRBY availability:hotel_123:suite_garden_001 2024-06-15 1&lt;/code&gt; → Result: &lt;code&gt;10&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Date Range Check (OLAP Query Pattern):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Checking availability for a 5-night stay is a single, efficient command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HMGET availability:hotel_123:suite_garden_001 2024-06-15 2024-06-16 2024-06-17 2024-06-18 2024-06-19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;: O(N) where N = number of fields requested (i.e., number of nights), &lt;strong&gt;not&lt;/strong&gt; the total number of documents in the database. This is extremely fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Redis Hash structure with atomic HINCRBY operations provides the exact OLTP capabilities needed for high-frequency availability updates—simple, fast, atomic, and designed for this exact workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date Range Validation with Hash Structure (Room Type Variation Aware):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical: Hotel Bookings are for Nights&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guest checks in on check-in date (stays that night)&lt;/li&gt;
&lt;li&gt;Guest checks out on check-out date (stays the previous night, leaves on check-out date)&lt;/li&gt;
&lt;li&gt;Room is occupied on check-in date and (check-out - 1 day), but available again on check-out date&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date range to check&lt;/strong&gt;: &lt;a href="https://dev.toinclusive"&gt;check_in_date, check_out_date - 1 day&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Room Type ID from Elasticsearch&lt;/strong&gt;: Elasticsearch nested query filters hotels by room type and returns &lt;code&gt;room_type_id&lt;/code&gt; (e.g., "suite_garden_001", "suite_ocean_001")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate date range&lt;/strong&gt;: Dates from check-in to (check-out - 1 day) inclusive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Key&lt;/strong&gt;: Use &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt; (e.g., &lt;code&gt;availability:hotel_123:suite_garden_001&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch check&lt;/strong&gt;: &lt;code&gt;HMGET availability:hotel_id:room_type_id date1 date2...&lt;/code&gt; (all nights in range)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt;: If all dates return non-null values with room_count &amp;gt; 0, room type variation is available for entire stay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(N) where N = number of nights (check-out - check-in), not total available dates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Check-in 2024-06-15, check-out 2024-06-17 → Check dates [2024-06-15, 2024-06-16]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sorted Set Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key Format&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id:dates&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Members&lt;/strong&gt;: date strings (2024-06-15, 2024-06-16)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores&lt;/strong&gt;: availability status (1 = available, 0 = unavailable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Date range queries with ranking per room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(log N) for range queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;availability:hotel_123:suite_garden_001:dates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;availability:hotel_123:suite_ocean_001:dates&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How the Sorted Set Gets Created:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial Creation&lt;/strong&gt;: When hotel availability data is loaded for a specific room type variation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorted Set Key&lt;/strong&gt;: Create sorted set with key &lt;code&gt;availability:hotel_id:room_type_id:dates&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;availability:hotel_123:suite_garden_001:dates&lt;/code&gt; (for suite with garden view)&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;availability:hotel_123:suite_ocean_001:dates&lt;/code&gt; (for suite with ocean view)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Member Addition&lt;/strong&gt;: For each available date for that room type variation, add as member with score 1:

&lt;ul&gt;
&lt;li&gt;Redis command: &lt;code&gt;ZADD availability:hotel_123:suite_garden_001:dates 1 "2024-06-15"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Redis command: &lt;code&gt;ZADD availability:hotel_123:suite_garden_001:dates 1 "2024-06-16"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Redis command: &lt;code&gt;ZADD availability:hotel_123:suite_garden_001:dates 1 "2024-06-17"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Member Updates&lt;/strong&gt;: When availability changes for that room type variation:

&lt;ul&gt;
&lt;li&gt;Mark available: &lt;code&gt;ZADD availability:hotel_123:suite_garden_001:dates 1 "2024-06-17"&lt;/code&gt; (add date with score 1)&lt;/li&gt;
&lt;li&gt;Mark unavailable: &lt;code&gt;ZREM availability:hotel_123:suite_garden_001:dates "2024-06-17"&lt;/code&gt; (remove date from set)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean Set&lt;/strong&gt;: Only available dates are stored in the sorted set (no "unavailable" members with score 0)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range Queries&lt;/strong&gt;: Query all available dates for that room type variation:

&lt;ul&gt;
&lt;li&gt;Redis command: &lt;code&gt;ZRANGE availability:hotel_123:suite_garden_001:dates 0 -1&lt;/code&gt; (get all available dates, sorted)&lt;/li&gt;
&lt;li&gt;Filter dates in desired range (e.g., June 2024)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL&lt;/strong&gt;: Set expiration: &lt;code&gt;EXPIRE availability:hotel_123:suite_garden_001:dates 14400&lt;/code&gt; (4 hours)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Triggers&lt;/strong&gt;: Sorted set is updated when:

&lt;ul&gt;
&lt;li&gt;Bookings change availability&lt;/li&gt;
&lt;li&gt;New availability periods are added&lt;/li&gt;
&lt;li&gt;Periodic sync from database&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Hash Structure vs Sorted Set: When to Use Each
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hash Structure Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;O(1) field access&lt;/strong&gt;: Direct access to any date&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct updates&lt;/strong&gt;: Update specific dates without pattern matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple date range validation&lt;/strong&gt;: Check all dates in range using HMGET&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient storage&lt;/strong&gt;: Only store dates that have availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better for&lt;/strong&gt;: Checking if specific dates are available, updating individual dates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Use Case:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query: "Is hotel_123 available for check-in 2024-06-15 to check-out 2024-06-17?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date range to check&lt;/strong&gt;: &lt;a href="https://dev.tosee%20Section%206%20for%20explanation%20of%20nights%20vs%20days"&gt;2024-06-15, 2024-06-16&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hash: &lt;code&gt;HMGET availability:hotel_123:suite_garden_001 2024-06-15 2024-06-16&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check if all dates return non-null values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(N) where N = number of nights (check-out - check-in)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sorted Set Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Range queries&lt;/strong&gt;: Efficiently find all available dates in a date range&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorted order&lt;/strong&gt;: Dates are automatically sorted, making range queries fast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk operations&lt;/strong&gt;: Get all available dates in a range with one query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better for&lt;/strong&gt;: Finding all available dates in a future period, date range discovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Use Case:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query: "Find all available dates for hotel_123 in June 2024"&lt;/li&gt;
&lt;li&gt;Sorted Set: &lt;code&gt;ZRANGE availability:hotel_123:suite_garden_001:dates 0 -1&lt;/code&gt; (get all available dates, sorted)&lt;/li&gt;
&lt;li&gt;Filter dates in June 2024 range&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: O(log N + M) where N = total dates, M = dates in range&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advantage&lt;/strong&gt;: Clean set with only available dates, no need to filter out "unavailable" members&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Comparison Table:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Hash Structure&lt;/th&gt;
&lt;th&gt;Sorted Set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single Date Check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Date Range Check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(N) - check each date&lt;/td&gt;
&lt;td&gt;O(log N + M) - efficient range query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update Single Date&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(1) - direct update&lt;/td&gt;
&lt;td&gt;O(log N) - need to find member&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Find All Available Dates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(N) - scan all fields&lt;/td&gt;
&lt;td&gt;O(log N + M) - efficient range query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Efficiency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Efficient (only available dates)&lt;/td&gt;
&lt;td&gt;Efficient (only available dates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check specific date ranges&lt;/td&gt;
&lt;td&gt;Discover available dates in periods&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation for Hotel Search:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Hash Structure&lt;/strong&gt; for the recommended approach because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most queries check specific date ranges (check-in to check-out)&lt;/li&gt;
&lt;li&gt;Need to verify all dates in range are available&lt;/li&gt;
&lt;li&gt;Hash structure provides O(1) access to specific dates&lt;/li&gt;
&lt;li&gt;Simpler to update when bookings change&lt;/li&gt;
&lt;li&gt;More intuitive for date range validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Sorted Set&lt;/strong&gt; if you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To find all available dates in a future period (e.g., "show me all available dates in July")&lt;/li&gt;
&lt;li&gt;Efficient range queries for date discovery&lt;/li&gt;
&lt;li&gt;Automatic sorting of dates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For Most Hotel Search Systems&lt;/strong&gt;: Hash structure is the better choice because the primary use case is checking if a specific date range is available, not discovering all available dates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Naming Conventions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Redis Key Patterns (Availability Only):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hash Structure (Recommended)&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt; (hash fields are dates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Key-Value&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id:check_in:check_out&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorted Set&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id:dates&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TTL Strategies for Availability Data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Time-to-Live Configuration (Redis/ElastiCache):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Availability Keys&lt;/strong&gt;: 1-4 hours (matches booking timeout, ensures fresh availability data)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Atomic Operations for Concurrent Booking
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important: Database is the Final Authority&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The primary database is the source of truth for all availability data. Redis is a fast access layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Authoritative data, transactional consistency, ACID guarantees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt;: Performance optimization layer, real-time access, eventual consistency with database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronization&lt;/strong&gt;: Redis must be updated to reflect database state, not the other way around&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Resolution&lt;/strong&gt;: If Redis and database disagree, database state is correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Race Condition Prevention:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SET with NX&lt;/strong&gt;: Only set if key doesn't exist (prevent concurrent updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WATCH/MULTI/EXEC&lt;/strong&gt;: Transactional updates for complex operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lua Scripts&lt;/strong&gt;: Atomic multi-step operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Booking Flow (Database as Authority):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check Redis&lt;/strong&gt;: Fast availability check in Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve in Database&lt;/strong&gt;: Create booking record in database (authoritative)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Redis&lt;/strong&gt;: Update Redis to reflect database state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If Database Fails&lt;/strong&gt;: Booking is not confirmed, Redis remains unchanged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If Redis Fails&lt;/strong&gt;: Database state is correct, Redis will be synced later&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Real-Time Update Patterns
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Update Flow: Database → Redis (One-Way Sync)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All updates originate from the database. Redis is updated to reflect database state:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update Strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immediate Updates&lt;/strong&gt;: After database transaction commits, immediately update Redis to reflect new availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch Updates&lt;/strong&gt;: Periodic sync from database to Redis to catch any missed updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-Driven&lt;/strong&gt;: Database triggers or change data capture (CDC) events update Redis in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback Sync&lt;/strong&gt;: Periodic reconciliation with database to ensure Redis matches database state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Update Order (Critical):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database Transaction&lt;/strong&gt;: Make changes in database first (authoritative)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Commit&lt;/strong&gt;: Wait for database commit to succeed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Update&lt;/strong&gt;: Update Redis to match database state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If Redis Update Fails&lt;/strong&gt;: Database state is still correct, Redis will be synced later&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Integrity&lt;/strong&gt;: Database transactions ensure consistency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis is Cache&lt;/strong&gt;: Redis can be rebuilt from database if needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Data Loss&lt;/strong&gt;: If Redis fails, database has all the data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eventual Consistency&lt;/strong&gt;: Redis may be temporarily out of sync, but database is always correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Redis provides sub-millisecond availability checks through optimized data structures and atomic operations, enabling real-time booking systems that would be impossible with database-only approaches. The primary database remains the final authority (see "Important: Database is the Final Authority" above) - all updates flow from database to Redis, ensuring data integrity and consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Hybrid Search Execution Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sequential vs Parallel Execution
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sequential Execution (Simple but Slower):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Elasticsearch search (25ms) → Get candidate hotels&lt;/li&gt;
&lt;li&gt;Redis availability check (10ms) → Filter by availability&lt;/li&gt;
&lt;li&gt;Total: 35ms&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Parallel Execution (Complex but Faster):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start Elasticsearch search (25ms) and Redis availability check (15ms) simultaneously&lt;/li&gt;
&lt;li&gt;Wait for both to complete (max of both = 25ms)&lt;/li&gt;
&lt;li&gt;Filter Elasticsearch results by Redis availability&lt;/li&gt;
&lt;li&gt;Total: 25ms (28% improvement)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Elasticsearch Candidate Generation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Search Phase:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Geographic Filtering&lt;/strong&gt;: City, radius, location-based filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amenity Filtering&lt;/strong&gt;: Pool, spa, gym, restaurant availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text Search&lt;/strong&gt;: Hotel name, description, location text matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room Type Filtering&lt;/strong&gt;: Nested queries for room type requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price Range Filtering&lt;/strong&gt;: Room type pricing constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result Set:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Candidate Hotels&lt;/strong&gt;: 100-1000 hotels matching search criteria&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotel Metadata&lt;/strong&gt;: Name, location, amenities, room types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Scores&lt;/strong&gt;: Relevance ranking for result ordering&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Redis Availability Validation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How Elasticsearch and Redis Work Together:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Elasticsearch Filters by Room Type&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User search includes room type filter (e.g., "suite with ocean view")&lt;/li&gt;
&lt;li&gt;Elasticsearch nested query filters hotels that have matching room type variations (e.g., suite with ocean view)&lt;/li&gt;
&lt;li&gt;Elasticsearch returns: List of hotels with matching room types (hotel metadata + room type info including &lt;code&gt;room_type_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: We know which hotels have matching room types and which &lt;code&gt;room_type_id&lt;/code&gt; to check in Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Redis Checks Availability for Specific Room Type Variation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For each hotel from Elasticsearch, we get the &lt;code&gt;room_type_id&lt;/code&gt; (e.g., "suite_ocean_001", "deluxe_001")&lt;/li&gt;
&lt;li&gt;Use hash key: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt; (e.g., &lt;code&gt;availability:hotel_123:suite_ocean_001&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Check availability for the specific room type variation across date range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Availability Check Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract Room Type ID&lt;/strong&gt;: From Elasticsearch results, get &lt;code&gt;room_type_id&lt;/code&gt; (e.g., "suite_ocean_001", "deluxe_001")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate Date List&lt;/strong&gt;: Create list of dates from check-in to (check-out - 1 day) inclusive

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Critical&lt;/strong&gt;: Hotel bookings are for nights, not days (see Section 6 for detailed explanation)&lt;/li&gt;
&lt;li&gt;Dates to check: [check_in_date, check_out_date - 1 day]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Hash Key&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HMGET Check&lt;/strong&gt;: &lt;code&gt;HMGET availability:hotel_id:room_type_id date1 date2...&lt;/code&gt; (all nights in range)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate Results&lt;/strong&gt;: Check if all dates have non-null values with room_count &amp;gt; 0&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example Flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query: "Suite with ocean view in Los Angeles, check-in: 2024-06-15, check-out: 2024-06-17"

Elasticsearch:
  - Filters: city=Los Angeles, room_type="suite", view="ocean"
  - Returns: [hotel_123 (has suite_ocean_001), hotel_456 (has suite_ocean_001), hotel_789 (has suite_ocean_001)]
  - Each result includes: hotel_id, room_type_id="suite_ocean_001"

Redis Availability Check:
  - Dates to check: [2024-06-15, 2024-06-16] (see Section 6 for explanation of date range logic)
  - For hotel_123: HMGET availability:hotel_123:suite_ocean_001 "2024-06-15" "2024-06-16"
  - For hotel_456: HMGET availability:hotel_456:suite_ocean_001 "2024-06-15" "2024-06-16"
  - For hotel_789: HMGET availability:hotel_789:suite_ocean_001 "2024-06-15" "2024-06-16"

Results:
  - hotel_123: [value, value] → Available (both nights have suite with ocean view)
  - hotel_456: [value, null] → Unavailable (2024-06-16 has no suite with ocean view available)
  - hotel_789: [value, value] → Available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Capacity Validation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If user needs multiple rooms (e.g., 2 suites with ocean view):

&lt;ul&gt;
&lt;li&gt;Check &lt;code&gt;room_count&lt;/code&gt; in hash values: &lt;code&gt;{"room_count": 5, ...}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ensure &lt;code&gt;room_count &amp;gt;= required_rooms&lt;/code&gt; for all dates in range&lt;/li&gt;
&lt;li&gt;Example: If user needs 2 rooms, but room_count is 1 → Unavailable&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Price Validation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Price comes from Elasticsearch room type data (Redis is exclusively for availability - see Section 6)&lt;/li&gt;
&lt;li&gt;Check price from Elasticsearch results against user's budget&lt;/li&gt;
&lt;li&gt;Ensure price is within budget for the selected room type variation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Filtering Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hash Key Pattern&lt;/strong&gt;: &lt;code&gt;availability:hotel_id:room_type_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch Operations&lt;/strong&gt;: Use Redis pipeline to batch multiple HMGET commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early Exit&lt;/strong&gt;: Stop checking if any date returns null (hotel unavailable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result Filtering&lt;/strong&gt;: Remove hotels where room type is unavailable for any date in range&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Result Merging and Ranking Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Merging Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Results&lt;/strong&gt;: Ranked by relevance score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Availability&lt;/strong&gt;: Binary available/unavailable status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intersection&lt;/strong&gt;: Keep only available hotels from Elasticsearch results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final Ranking&lt;/strong&gt;: Maintain Elasticsearch relevance order for available hotels&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Ranking Factors:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Relevance&lt;/strong&gt;: BM25 score from Elasticsearch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Status&lt;/strong&gt;: Available hotels ranked higher&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price Competitiveness&lt;/strong&gt;: Lower prices get slight boost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Preferences&lt;/strong&gt;: Historical booking patterns, loyalty status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interview Tip: Streaming Results with Pagination&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once Elasticsearch returns ranked results, you don't need to wait for all availability checks before sending results to the UI. Instead, use a streaming/pagination approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming Optimization:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Returns&lt;/strong&gt;: Ranked list of hotels (e.g., 500 hotels)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch Processing&lt;/strong&gt;: Process hotels in batches of 10-20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Check&lt;/strong&gt;: Check availability for first batch (e.g., first 10 hotels)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send to UI&lt;/strong&gt;: Immediately send available hotels from first batch to UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continue Processing&lt;/strong&gt;: While UI displays first page, check availability for next batch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination&lt;/strong&gt;: As user scrolls, send next batch of available hotels&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time to First Result (TTFR)&lt;/strong&gt;: User sees results faster (e.g., 30ms instead of 200ms)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better UX&lt;/strong&gt;: Progressive loading feels faster than waiting for all results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced Latency&lt;/strong&gt;: Don't check availability for hotels user might never see&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient Resource Usage&lt;/strong&gt;: Only process what's needed for current page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Elasticsearch: Returns 500 ranked hotels (25ms)
↓
Batch 1: Check availability for hotels 1-10 (5ms) → Send to UI
↓
Batch 2: Check availability for hotels 11-20 (5ms) → Cache for next page
↓
Batch 3: Check availability for hotels 21-30 (5ms) → Cache for next page
...
User scrolls → Load cached batch 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;: Since results are already ranked by Elasticsearch, you can stream them in order. The UI only needs the first 10-20 results immediately, so there's no need to wait for all 500 hotels to be checked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Execution Pattern&lt;/th&gt;
&lt;th&gt;Elasticsearch&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;th&gt;Total Time&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sequential&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;td&gt;10ms&lt;/td&gt;
&lt;td&gt;35ms&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parallel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;td&gt;15ms&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cached Results&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;7ms&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Parallel execution provides 28% performance improvement while batch Redis operations can reduce availability check time by 50%, making the hybrid approach significantly faster than sequential processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. APIs and Query Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Search API Endpoint Structure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Primary Endpoint&lt;/strong&gt;: GET /api/v1/hotels/search&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;location&lt;/strong&gt; (required): City name or coordinates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check_in&lt;/strong&gt; (required): Check-in date (YYYY-MM-DD)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check_out&lt;/strong&gt; (required): Check-out date (YYYY-MM-DD)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;guests&lt;/strong&gt; (optional): Number of guests (default: 2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;room_type&lt;/strong&gt; (optional): Specific room type (deluxe, suite)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;amenities&lt;/strong&gt; (optional): Array of amenities (pool, spa, gym)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;price_min/max&lt;/strong&gt; (optional): Price range constraints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;radius&lt;/strong&gt; (optional): Search radius in kilometers (default: 30)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sort&lt;/strong&gt; (optional): Sort order (relevance, price, rating, distance)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step-by-Step Query Execution Flow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Query Processing (5ms)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input Validation&lt;/strong&gt;: Validate dates, location, parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Normalization&lt;/strong&gt;: Standardize location names, date formats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameter Extraction&lt;/strong&gt;: Extract search criteria and filters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Check&lt;/strong&gt;: Check for cached results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Elasticsearch Search (25ms)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Geographic Filter&lt;/strong&gt;: Filter by location and radius&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amenity Filter&lt;/strong&gt;: Filter by requested amenities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room Type Filter&lt;/strong&gt;: Nested queries for room type requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text Search&lt;/strong&gt;: Multi-match queries for hotel names and descriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price Filter&lt;/strong&gt;: Range queries for price constraints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result Ranking&lt;/strong&gt;: BM25 scoring and relevance ranking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Redis Availability Check (10ms)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hotel ID Extraction&lt;/strong&gt;: Get hotel IDs from Elasticsearch results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Keys&lt;/strong&gt;: Generate Redis keys for date range&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch Check&lt;/strong&gt;: MGET operation for multiple hotels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Filtering&lt;/strong&gt;: Remove unavailable hotels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price Validation&lt;/strong&gt;: Verify pricing within constraints&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Result Assembly (5ms)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Result Merging&lt;/strong&gt;: Combine Elasticsearch and Redis results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final Ranking&lt;/strong&gt;: Apply availability and pricing factors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response Formatting&lt;/strong&gt;: Structure response with hotel details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Storage&lt;/strong&gt;: Store results for future queries&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Response Format with Availability and Pricing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Response Structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hotels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hotel_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hotel_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Luxury Downtown Hotel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Los Angeles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"coordinates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;34.0522&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-118.2437&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123 Main St"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amenities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"pool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gym"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"room_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deluxe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"capacity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"price_per_night"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;299&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"rooms_left"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"total_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;598&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"search_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"search_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cache_hit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error Handling and Fallback Strategies
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Error Scenarios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Unavailable&lt;/strong&gt;: Fallback to database with reduced functionality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Unavailable&lt;/strong&gt;: Skip availability filtering, show all results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout Errors&lt;/strong&gt;: Return partial results with warning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid Parameters&lt;/strong&gt;: Return error with parameter validation details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fallback Strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database Fallback&lt;/strong&gt;: Use PostgreSQL for basic search when Elasticsearch fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached Results&lt;/strong&gt;: Serve cached results during outages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degraded Mode&lt;/strong&gt;: Reduce functionality but maintain basic search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Degradation&lt;/strong&gt;: Show maintenance message with trending hotels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitoring and Alerting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency Monitoring&lt;/strong&gt;: Alert if response time &amp;gt; 100ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Rate Monitoring&lt;/strong&gt;: Alert if error rate &amp;gt; 5%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Performance&lt;/strong&gt;: Monitor cache hit rates and memory usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Monitoring&lt;/strong&gt;: Track Elasticsearch and Redis health&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: The API design balances comprehensive search capabilities with performance optimization, using parallel execution and intelligent caching to maintain sub-100ms response times while providing rich search functionality and robust error handling.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Soundbite for interviews&lt;/strong&gt;: "Hotel search requires a hybrid Elasticsearch + Redis architecture because no single system can efficiently handle both complex search/filtering and real-time availability updates. The key is using Elasticsearch for what it does best (search and filtering) and Redis for what it does best (real-time data), with parallel execution improving time to first results."&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/sumedhbala/hotel-booking-schema-design-comparison-g3h"&gt;Hotel Booking Schema Design&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>elasticsearch</category>
      <category>redis</category>
      <category>bitmaps</category>
    </item>
    <item>
      <title>Part 4: Payments and Ticket Issuance</title>
      <dc:creator>Sumedh Bala</dc:creator>
      <pubDate>Thu, 23 Oct 2025 17:51:10 +0000</pubDate>
      <link>https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h</link>
      <guid>https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h</guid>
      <description>&lt;h2&gt;
  
  
  Idempotency, Consistency, and Race-Safe Design
&lt;/h2&gt;

&lt;p&gt;This section focuses on the critical final steps of the user journey: securely processing payments and reliably issuing digital tickets. These operations demand high integrity, fault tolerance, and consistency to prevent double-charging, lost tickets, or overselling — while also protecting sensitive financial data and preventing race conditions.&lt;/p&gt;

&lt;p&gt;Today, we're going to architect a production-grade solution using a modern, event-driven approach. Our design is built on three key principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Microservice Architecture:&lt;/strong&gt; We'll separate our system into distinct services, each with one job.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Saga Pattern:&lt;/strong&gt; We'll manage the complex, multi-step purchase process as a single, coordinated workflow.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Transactional Outbox:&lt;/strong&gt; We'll use this pattern as the "atomic glue" that guarantees our services stay in sync, even if one of them fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Saga Pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Saga Pattern&lt;/strong&gt; is a design pattern for managing distributed transactions across multiple services. Instead of using traditional ACID transactions (which don't work well across service boundaries), sagas break down complex business processes into a sequence of local transactions, each with a corresponding compensating action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each step in the saga is a local transaction within a single service&lt;/li&gt;
&lt;li&gt;If any step fails, the saga executes compensating transactions to undo previous steps&lt;/li&gt;
&lt;li&gt;This ensures eventual consistency across the entire system&lt;/li&gt;
&lt;li&gt;Perfect for complex workflows like: Reserve Seats → Process Payment → Generate Tickets → Send Notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Transactional Outbox Pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Transactional Outbox Pattern&lt;/strong&gt; solves the dual-write problem in distributed systems. When you need to update your database AND publish an event (like sending a notification), you can't do both atomically across service boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's useful:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity&lt;/strong&gt;: Write to database and outbox table in the same transaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: Guarantees events are eventually published, even if the service fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: Ensures database state and event publishing stay in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience&lt;/strong&gt;: Handles network failures, service restarts, and partial failures gracefully&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Application writes business data AND event data to the same database transaction&lt;/li&gt;
&lt;li&gt;A separate process (outbox processor) reads from the outbox table&lt;/li&gt;
&lt;li&gt;Publishes events to message queues (Kafka, RabbitMQ, etc.)&lt;/li&gt;
&lt;li&gt;Marks events as processed to prevent duplicates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The central theme is &lt;strong&gt;separation of concerns&lt;/strong&gt;. Let's see how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Architecture: Separating Our Concerns
&lt;/h2&gt;

&lt;p&gt;We'll decompose our system into three primary services. The key is to define what each service is—and is not—responsible for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order Management Service:&lt;/strong&gt; This is our "smart" orchestrator. It acts as the brain of the operation, managing the business workflow (the Saga). Its only concern is the &lt;em&gt;state&lt;/em&gt; of an order (e.g., &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;tickets_issued&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment Processing Service:&lt;/strong&gt; This is a "dumb," single-purpose service. Its &lt;em&gt;only&lt;/em&gt; concern is interacting with a payment gateway (like Stripe) and reporting success or failure. It knows nothing about seats or tickets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ticket Issuance Service:&lt;/strong&gt; This is another "dumb" service. Its &lt;em&gt;only&lt;/em&gt; concern is managing inventory (seats, sections) and generating ticket records. It knows nothing about credit cards or payment intents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is what allows us to scale and maintain the system. We can update our payment provider without ever touching the ticket generation code. If the ticket service is slow, it won't block new payments from being accepted.&lt;/p&gt;

&lt;p&gt;But how do these separate services talk to each other reliably?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Glue: A Saga for Distributed Transactions
&lt;/h2&gt;

&lt;p&gt;You can't use a single database &lt;code&gt;COMMIT&lt;/code&gt; across three different services. So, how do you guarantee that a successful payment &lt;em&gt;always&lt;/em&gt; results in a ticket?&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;Saga&lt;/strong&gt; and &lt;strong&gt;Transactional Outbox&lt;/strong&gt; patterns come in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Saga:&lt;/strong&gt; Our "Order Management Service" will manage the purchase as a step-by-step process.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Step 1:&lt;/strong&gt; Tell the &lt;code&gt;Payment Service&lt;/code&gt; to process a payment.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Step 2 (if success):&lt;/strong&gt; Tell the &lt;code&gt;Ticket Issuance Service&lt;/code&gt; to generate tickets.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Step 3 (if fail):&lt;/strong&gt; Tell the &lt;code&gt;Ticket Issuance Service&lt;/code&gt; (or Reservation Service) to release the held seats.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;The Transactional Outbox:&lt;/strong&gt; This is the &lt;em&gt;mechanism&lt;/em&gt; that makes the Saga reliable. When the &lt;code&gt;Payment Service&lt;/code&gt; confirms a payment, it can't just &lt;em&gt;hope&lt;/em&gt; to send a message to the &lt;code&gt;Ticket Issuance Service&lt;/code&gt;. What if it crashes right after it saves the payment to its own database? The payment would be successful, but the ticket would never be issued.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Outbox Pattern&lt;/strong&gt; solves this. In a single, atomic database transaction, the &lt;code&gt;Payment Service&lt;/code&gt; does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Updates the &lt;code&gt;payments&lt;/code&gt; table to &lt;code&gt;status = 'successful'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Inserts a &lt;em&gt;message&lt;/em&gt; into a &lt;code&gt;transactional_outbox&lt;/code&gt; table in its &lt;em&gt;own database&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because this is one transaction, it's 100% atomic. It's impossible for the payment to be marked "successful" without the "issue tickets" message being created. A separate background process (a "message relay") then reliably publishes this message from the outbox to the rest of the system.&lt;/p&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How the Message Relay Works:&lt;/strong&gt;&lt;br&gt;
The message relay is a critical infrastructure component that bridges the gap between database transactions and message queues. It has two common implementation patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Polling Pattern&lt;/strong&gt;: A simple service that polls the &lt;code&gt;transactional_outbox&lt;/code&gt; table every few seconds for &lt;code&gt;status = 'pending'&lt;/code&gt; events, publishes them to Kafka/RabbitMQ, then marks them as &lt;code&gt;status = 'published'&lt;/code&gt;. This is reliable but has higher latency (2-5 seconds).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Change Data Capture (CDC) Pattern&lt;/strong&gt;: A more advanced approach using tools like Debezium that tail the database's transaction log (WAL) and instantly convert table inserts into Kafka messages. This offers lower latency (milliseconds) but requires more sophisticated infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both patterns guarantee that every outbox event is eventually published, even if the service fails mid-process.&lt;/p&gt;

&lt;p&gt;This is the ultimate separation of concerns. The &lt;code&gt;Payment Service&lt;/code&gt;'s only job is to update its own state and "leave a memo" in its outbox. It doesn't know or care &lt;em&gt;who&lt;/em&gt; reads that memo, allowing our services to be beautifully decoupled.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Blueprint: Our Production-Grade Database Schema
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create ENUM types for reusability&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;order_payment_status_enum&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;-- Order created, awaiting payment&lt;/span&gt;
    &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Client-side /confirm received, awaiting webhook&lt;/span&gt;
    &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;-- Authoritative webhook received&lt;/span&gt;
    &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;-- Payment failed&lt;/span&gt;
    &lt;span class="s1"&gt;'refunded'&lt;/span&gt;               &lt;span class="c1"&gt;-- Payment refunded&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;payment_transaction_status_enum&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'initiated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- Payment intent created&lt;/span&gt;
    &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Client-side /confirm received&lt;/span&gt;
    &lt;span class="s1"&gt;'successful'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- Authoritative webhook received&lt;/span&gt;
    &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;-- Payment failed&lt;/span&gt;
    &lt;span class="s1"&gt;'refunded'&lt;/span&gt;               &lt;span class="c1"&gt;-- Payment refunded&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;ticket_status_enum&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'issued'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;ticket_scan_status_enum&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'scanned'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'canceled'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- New ENUM for outbox processing&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;outbox_status_enum&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sent'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Orders table with constraints&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="n"&gt;order_payment_status_enum&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ticket_status&lt;/span&gt; &lt;span class="n"&gt;ticket_status_enum&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Payments table with transaction tracking&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- NO CASCADE DELETE&lt;/span&gt;
    &lt;span class="n"&gt;payment_gateway_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;payment_method_token&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;transaction_reference&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_minor_units&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;char_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;payment_transaction_status_enum&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'initiated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Tickets table with seat/section exclusivity&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ticket_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- NO CASCADE DELETE&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;owner_user_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;qr_code_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;ticket_scan_status_enum&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;issued_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- prevent duplicate seat assignment within same order&lt;/span&gt;
    &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Critical constraint: prevent double-booking across orders for ACTIVE tickets&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_tickets_event_seat_unique_active&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'scanned'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- NEW: Transactional Outbox Table&lt;/span&gt;
&lt;span class="c1"&gt;-- This table guarantees atomic event publishing&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;transactional_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;outbox_status_enum&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Index for the message relay to efficiently find pending events&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_outbox_pending_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;transactional_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="c1"&gt;-- Indexes for performance&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_reservation_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_payments_order_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_tickets_event_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_tickets_owner&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner_user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The API &amp;amp; Workflow: A Step-by-Step Breakdown
&lt;/h2&gt;

&lt;p&gt;Here is the complete user flow, showing how each API call and background process works within our separated architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Payment Processing APIs
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Initiate Payment
&lt;/h4&gt;

&lt;p&gt;This is the first step. The client asks the &lt;code&gt;Payment Service&lt;/code&gt; to create a payment intent. &lt;strong&gt;No money is charged yet&lt;/strong&gt; - this only creates the payment intent with Stripe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: &lt;code&gt;POST /payments&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Request Body&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order_789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_method_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pm_123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"idem_987654321"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay_123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"initiated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_intent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pi_stripe_abc123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Associated SQL Operations&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Check for idempotency&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'idem_987654321'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Get server-side price (NEVER trust the client)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount_minor_units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. [Application calls Stripe API with the server-side amount]&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Create the payment record&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_minor_units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'pay_123456789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount_minor_units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- ✅ Server-validated amount&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;-- ✅ Server-validated currency&lt;/span&gt;
    &lt;span class="s1"&gt;'initiated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'idem_987654321'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;--    ⚠️  NOTE: This creates the PaymentIntent but does NOT charge the user yet.&lt;br&gt;
--    The actual payment happens on the client-side when the user confirms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complete Payment Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;POST /payments&lt;/code&gt;&lt;/strong&gt; (this endpoint): Creates PaymentIntent, returns &lt;code&gt;client_secret&lt;/code&gt; to browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-Side&lt;/strong&gt;: Browser uses &lt;code&gt;client_secret&lt;/code&gt; with Stripe.js to show payment form&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-Side&lt;/strong&gt;: User clicks "Pay" → &lt;code&gt;stripe.confirmCardPayment()&lt;/code&gt; is called → &lt;strong&gt;This is when money moves!&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-Side&lt;/strong&gt;: On success, browser calls &lt;code&gt;POST /payments/{id}/confirm&lt;/code&gt; (optimistic UX)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Side&lt;/strong&gt;: Stripe sends &lt;code&gt;POST /webhooks/payments&lt;/code&gt; (authoritative confirmation)&lt;/li&gt;
&lt;/ol&gt;


&lt;h4&gt;
  
  
  2. Payment Webhook (The Asynchronous Source of Truth)
&lt;/h4&gt;

&lt;p&gt;This is the &lt;strong&gt;most important&lt;/strong&gt; endpoint. It's called by Stripe's servers, not the user. It is the &lt;em&gt;authoritative&lt;/em&gt; record of a transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: &lt;code&gt;POST /webhooks/payments&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: &lt;code&gt;X-Webhook-Signature: &amp;lt;HMAC_SHA256&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardened Implementation Logic:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Verify Signature:&lt;/strong&gt; Cryptographically verify the signature.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Respond Immediately:&lt;/strong&gt; Return a &lt;code&gt;200 OK&lt;/code&gt; to Stripe to acknowledge receipt.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Queue for Processing:&lt;/strong&gt; Place the event on an internal queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous Handler SQL (Processing the event from the internal queue):&lt;/strong&gt;&lt;br&gt;
This logic demonstrates our separation of concerns perfectly. The &lt;code&gt;Payment Service&lt;/code&gt;'s &lt;em&gt;only&lt;/em&gt; job is to update its own tables and (atomically) create an &lt;code&gt;outbox&lt;/code&gt; event. It has no knowledge of &lt;em&gt;how&lt;/em&gt; to issue a ticket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- CASE 1: Payment Successful (e.g., 'payment_intent.succeeded')&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 1: Update the payment record idempotently.&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'successful'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;transaction_reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'txn_987654321'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- from webhook&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'initiated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 2: Update the master order status.&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 3: Update reservation status to confirmed&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;confirmed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 4: Create the outbox event to trigger Ticket Issuance&lt;/span&gt;
    &lt;span class="c1"&gt;-- This is ATOMIC with the reservation update.&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;transactional_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'order.paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'{ ... webhook payload ... }'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="c1"&gt;-- CASE 2: Payment Failed (e.g., 'payment_intent.payment_failed')&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 1: Update payment to failed&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'initiated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 2: Update order to failed&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 3: Update reservation status to canceled&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'canceled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 4: Create outbox event to trigger reservation release&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;transactional_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pi_stripe_abc123'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'order.failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'{ ... failure details ... }'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  3. Confirm Payment (The Optimistic UX Endpoint)
&lt;/h4&gt;

&lt;p&gt;This endpoint is &lt;em&gt;only&lt;/em&gt; for user experience. It's called by the user's browser to give them an &lt;em&gt;instant&lt;/em&gt; success screen, without waiting for the (slower) webhook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: &lt;code&gt;POST /payments/{payment_id}/confirm&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay_123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"confirmed_optimistic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Payment confirmed. Your tickets will be issued shortly."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Associated SQL Operations&lt;/strong&gt;:&lt;br&gt;
This transaction is deliberately fast and lightweight. It &lt;em&gt;only&lt;/em&gt; updates the status to &lt;code&gt;confirmed_optimistic&lt;/code&gt;. It &lt;strong&gt;does not&lt;/strong&gt; trigger ticket issuance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 1: Mark the payment as optimistically confirmed&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pay_123456789'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'initiated'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 2: Mark the order as optimistically confirmed&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'confirmed_optimistic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pay_123456789'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  The Saga Continues: Asynchronous Ticket Issuance
&lt;/h3&gt;

&lt;p&gt;This is &lt;strong&gt;not an API&lt;/strong&gt;. This is where our separation of concerns pays off. The &lt;code&gt;Ticket Issuance Service&lt;/code&gt; has &lt;em&gt;one&lt;/em&gt; job: listen for &lt;code&gt;order.paid&lt;/code&gt; events and make tickets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger:&lt;/strong&gt; Consuming an &lt;code&gt;order.paid&lt;/code&gt; event from the message bus (which was reliably published by the &lt;code&gt;Payment Service&lt;/code&gt;'s outbox).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Associated SQL Operations (Deadlock-Proofed)&lt;/strong&gt;:&lt;br&gt;
This is the big, complex transaction. By moving it to a background worker, we prevent it from ever blocking our payment flow. If it fails, it can safely retry without losing data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 1: IDEMPOTENCY CHECK - Prevent duplicate ticket generation&lt;/span&gt;
    &lt;span class="c1"&gt;-- This makes the entire operation idempotent against message re-delivery&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ticket_status&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Lock the order row&lt;/span&gt;

    &lt;span class="c1"&gt;-- [Application Logic]&lt;/span&gt;
    &lt;span class="c1"&gt;-- IF ticket_status is already 'issued', COMMIT immediately and ACK the message.&lt;/span&gt;
    &lt;span class="c1"&gt;-- The order is already processed - no need to generate tickets again.&lt;/span&gt;

    &lt;span class="c1"&gt;-- IF ticket_status is 'pending', proceed with the rest of the logic...&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 2: Lock the parent reservation&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 3: CRITICAL: Lock seats in a consistent order&lt;/span&gt;
    &lt;span class="c1"&gt;-- This (ORDER BY s.seat_id) is the key to preventing deadlocks&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; 
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; 
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reserved'&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;  &lt;span class="c1"&gt;-- This deterministic ordering is vital&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 4: Get reservation details for ticket generation&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_ga&lt;/span&gt; &lt;span class="n"&gt;rsg&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 5: Generate individual tickets (example)&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qr_code_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issued_at&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; 
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'TKT-ABC123DEF456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'event_456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A-101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'TKT-JKL345MNO678'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'event_456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GA1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 6: Atomically update seats from 'reserved' to 'sold'&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sold'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; 
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; 
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reserved'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 7: Atomically update reservation to 'completed'&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 8: Update order ticket status&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;ticket_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'issued'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Step 9: Create outbox event to notify user via email&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;transactional_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'tickets.issued'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'{ ... ticket URLs and delivery info ... }'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Supporting Operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ticket Status Update (Scan)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: &lt;code&gt;POST /tickets/{ticket_id}/scan&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Associated SQL Operations&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;-- Lock the specific ticket row to prevent double-scans&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; 
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ticket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'TKT-ABC123DEF456'&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Check status and update (if active)&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; 
    &lt;span class="k"&gt;SET&lt;/span&gt; 
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'scanned'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;scan_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scan_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;scanned_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ticket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'TKT-ABC123DEF456'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Event Cancellation Refunds (Bulk Refunds)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: &lt;code&gt;POST /refunds/event-cancellation&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Associated SQL Operations&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This query is CRITICAL: It selects the `transaction_reference`&lt;/span&gt;
&lt;span class="c1"&gt;-- which is the *actual charge ID* needed by the gateway to process a refund.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_minor_units&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'event_456'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'successful'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_reference&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(The application then loops over these results, calls the gateway for each refund, and updates the &lt;code&gt;payments&lt;/code&gt; and &lt;code&gt;orders&lt;/code&gt; tables to &lt;code&gt;status = 'refunded'&lt;/code&gt;.)&lt;/em&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  Verify Anonymous Refund Request
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: &lt;code&gt;POST /refunds/verify&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Associated SQL Operations&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Validate order exists and is eligible for refund&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'successful'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. [Application Logic: Check rate limits]&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Generate and store verification code&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;refund_verifications&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;verification_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contact_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;contact_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verification_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'ver_001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'user@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'123456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- Securely generated OTP&lt;/span&gt;
    &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'15 minutes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Our Design's Superpowers
&lt;/h2&gt;

&lt;p&gt;By strictly separating concerns and using the Saga + Transactional Outbox patterns, our system is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resilient:&lt;/strong&gt; If the &lt;code&gt;Ticket Issuance Service&lt;/code&gt; fails, payments are still safely accepted. The &lt;code&gt;order.paid&lt;/code&gt; events will simply wait in the outbox to be processed when the service recovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent:&lt;/strong&gt; We have an atomic guarantee that a committed payment &lt;em&gt;will always&lt;/em&gt; result in an event to generate a ticket. It's impossible to charge a customer and forget to issue their ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable:&lt;/strong&gt; We can add 100 more &lt;code&gt;Ticket Issuance&lt;/code&gt; workers to handle a massive on-sale event without ever needing to touch the &lt;code&gt;Payment Service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainable:&lt;/strong&gt; Our services are small and focused. Our &lt;code&gt;Payment Service&lt;/code&gt; team can work on compliance and gateways, while the &lt;code&gt;Ticket Issuance&lt;/code&gt; team can optimize for inventory and concurrency. They don't need to be in the same meetings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Improving Consistency: The Reconciliation Service
&lt;/h2&gt;

&lt;p&gt;While our Saga + Transactional Outbox patterns provide strong consistency guarantees, production systems can benefit from an additional layer of validation. A &lt;strong&gt;Reconciliation Service&lt;/strong&gt; can further enhance data consistency across all system components by continuously monitoring and validating that all financial transactions, seat allocations, and ticket issuances are properly synchronized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's valuable as an afterthought:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Financial Accuracy&lt;/strong&gt;: Ensures payment amounts match between our system and payment gateways&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory Consistency&lt;/strong&gt;: Verifies seat allocations match between reservations and actual seat status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ticket Integrity&lt;/strong&gt;: Confirms all issued tickets correspond to valid, paid orders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Compliance&lt;/strong&gt;: Provides complete audit trails for regulatory requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discrepancy Detection&lt;/strong&gt;: Identifies and flags inconsistencies before they impact customers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it reconciles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payment Gateway vs Database&lt;/strong&gt;: Stripe/PayPal transaction records vs our payment records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reservation vs Seat Status&lt;/strong&gt;: Reserved seats in our system vs actual seat availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order vs Ticket Count&lt;/strong&gt;: Number of tickets issued vs order quantities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue vs Transactions&lt;/strong&gt;: Total revenue calculations vs individual payment sums&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund Consistency&lt;/strong&gt;: Refund amounts vs original payment amounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled Reconciliation&lt;/strong&gt;: Runs every 15 minutes to check recent transactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Monitoring&lt;/strong&gt;: Continuously validates critical operations (payments, ticket generation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-system Validation&lt;/strong&gt;: Compares data across payment gateways, databases, and external systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Correction&lt;/strong&gt;: Fixes minor discrepancies automatically (e.g., status updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert Generation&lt;/strong&gt;: Flags major discrepancies for manual investigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Reporting&lt;/strong&gt;: Generates comprehensive reports for compliance and analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This reconciliation service acts as a safety net, providing an additional layer of confidence that our distributed system maintains data integrity even under extreme load and complex failure scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gemini.google.com/share/15d3d82059b5" rel="noopener noreferrer"&gt;Review by Google Gemini&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>idempotency</category>
      <category>payments</category>
    </item>
    <item>
      <title>Part 3: Seat Management</title>
      <dc:creator>Sumedh Bala</dc:creator>
      <pubDate>Thu, 23 Oct 2025 17:48:52 +0000</pubDate>
      <link>https://dev.to/sumedhbala/part-3-seat-management-nn2</link>
      <guid>https://dev.to/sumedhbala/part-3-seat-management-nn2</guid>
      <description>&lt;h2&gt;
  
  
  Reservations, Locking, Availability &amp;amp; Queuing
&lt;/h2&gt;

&lt;p&gt;Seat management ensures users can reliably reserve seats without conflicts, overselling, or double-booking. It handles reserved vs General Admission seats, tracks reservations until payment, manages concurrency, and provides real-time availability updates. Queuing prevents overload during high-demand events.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Baseline Functional Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Service Responsibilities &amp;amp; API Mapping
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Primary Responsibilities&lt;/th&gt;
&lt;th&gt;API Endpoints&lt;/th&gt;
&lt;th&gt;Data Sources&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Seat Management Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Seat reservations, locking, cancellations, expired cleanup&lt;/td&gt;
&lt;td&gt;POST /events/{event_id}/reservations&lt;/td&gt;
&lt;td&gt;Primary Database, Redis Cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue Management Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User queuing, position tracking, admission control&lt;/td&gt;
&lt;td&gt;POST /events/{event_id}/queue&lt;br&gt;GET /events/{event_id}/queue/status&lt;/td&gt;
&lt;td&gt;Redis Queue, Database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability Aggregation Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Section counts, real-time availability tracking&lt;/td&gt;
&lt;td&gt;GET /events/{event_id}/sections&lt;/td&gt;
&lt;td&gt;Message Bus, Redis Cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Real-time Notification Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSE connections for live updates&lt;/td&gt;
&lt;td&gt;SSE /events/{event_id}/sections/updates&lt;/td&gt;
&lt;td&gt;Redis Pub/Sub, Message Bus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Databases &amp;amp; Caches
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary Database&lt;/strong&gt; (PostgreSQL / MySQL / DynamoDB): Stores canonical seat and reservation state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reservation Store&lt;/strong&gt; (DynamoDB / Redis / PostgreSQL): Tracks in-progress reservations. Only stores queue_token for anonymous users; logged-in users are identified via JWT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt; (Redis / Memcached): Hot lookups for section counts, seat maps, and user queue positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Bus&lt;/strong&gt; (Kafka / Pulsar / Kinesis / Pub/Sub): Streams reservation events for real-time updates and availability aggregation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queueing Infrastructure&lt;/strong&gt;: Queued requests for high-demand events. Distributed coordination ensures unique positions and queue ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Updates Infrastructure&lt;/strong&gt;: In-memory counters maintain section-level seats_remaining. Fanout layer scales push notifications to many clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example Database Schema
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The following are simplified schema overviews for understanding the basic structure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Seats Table
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"A-101"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="n"&gt;seat_number&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Reservations Table (Header)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;queue_token&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;only&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;anonymous&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;logged&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending_payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canceled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total_amount_minor_units&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;smallest&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;avoid&lt;/span&gt; &lt;span class="n"&gt;floating&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="n"&gt;confirmed_at&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Reservation_Seats Table (Reserved Seats)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;reservation_seat_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Reservation_GA Table (General Admission)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;reservation_ga_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;GA&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Sections Table
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"section_A"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"104"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;
&lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;real&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Queue Table
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;queue_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;FK&lt;/span&gt;
&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="k"&gt;nullable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logged&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;queue_token&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="k"&gt;nullable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_updated_at&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;timestamps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Reservation Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enter Queue (High-Demand Events)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anonymous users receive a queue_token.&lt;/li&gt;
&lt;li&gt;Logged-in users are identified via JWT; no token issued.&lt;/li&gt;
&lt;li&gt;If the queue is empty, users are immediately ready to reserve seats.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Browse Section Availability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anyone can fetch aggregated section counts before joining the queue.&lt;/li&gt;
&lt;li&gt;Individual reserved seat details require ready status (queue_token or JWT).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reserve Seats / General Admission Slots
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Must be ready in the queue.&lt;/li&gt;
&lt;li&gt;Backend validates queue readiness via queue_token (anonymous) or JWT/account ID (logged-in).&lt;/li&gt;
&lt;li&gt;Reserved seats have TTL until payment is completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real-Time Updates
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Users receive live updates on section availability via Server-Sent Events (SSE).&lt;/li&gt;
&lt;li&gt;Anonymous users subscribe with queue_token.&lt;/li&gt;
&lt;li&gt;Logged-in users subscribe with JWT.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. APIs (in order of user flow)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enter Queue
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: POST /events/{event_id}/queue&lt;br&gt;
&lt;strong&gt;Request Body&lt;/strong&gt;: {}&lt;br&gt;
&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queue_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;signed_token&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"estimated_wait_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position_in_queue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;: Logged-in users receive no token; identity tracked via JWT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue Status
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: GET /events/{event_id}/queue/status&lt;br&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: Authorization: Bearer  (anonymous) OR Authorization: Bearer  (logged-in)&lt;br&gt;
&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position_in_queue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"waiting"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;: All users must check queue status before reserving seats during high-demand events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get Section Availability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: GET /events/{event_id}/sections&lt;br&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: Optional&lt;br&gt;
&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"section_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"available_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"section_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"available_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"section_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GA1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"General Admission"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"available_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;: Anyone can fetch counts even before joining the queue. Reserved seat details are not returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get Seats in a Section (Reserved Seating Only, Paginated)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: GET /events/{event_id}/sections/{section_id}/seats&lt;br&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: Authorization: Bearer  (anonymous, ready) OR Authorization: Bearer  (logged-in, ready)&lt;br&gt;
&lt;strong&gt;Query Params&lt;/strong&gt;: page=1, page_size=50&lt;br&gt;
&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"section_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page_size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seat_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A-101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"row"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seat_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A-102"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"row"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reserved"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seat_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A-103"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"row"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reserve Seats / General Admission Slots
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: POST /events/{event_id}/reservations&lt;br&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: Authorization: Bearer  (anonymous) OR Authorization: Bearer  (logged-in)&lt;br&gt;
&lt;strong&gt;Request Body&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"A-101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"A-103"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ga_section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GA1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ga_quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Database Operations&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create Reservation Header&lt;/strong&gt;: Insert into &lt;code&gt;reservations&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link Reserved Seats&lt;/strong&gt;: Insert into &lt;code&gt;reservation_seats&lt;/code&gt; table for each seat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link GA Quantities&lt;/strong&gt;: Insert into &lt;code&gt;reservation_ga&lt;/code&gt; table for GA sections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Seat Status&lt;/strong&gt;: Mark seats as 'reserved' in &lt;code&gt;seats&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Section Counts&lt;/strong&gt;: Decrement &lt;code&gt;seats_remaining&lt;/code&gt; in &lt;code&gt;sections&lt;/code&gt; table&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Response&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reservation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"res_789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reserved_until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-09-02T12:35:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"A-101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"A-103"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ga_section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GA1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ga_quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending_payment"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;: Backend validates queue readiness. TTL ensures release if payment isn't completed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Section Updates (Server-Sent Events)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why Server-Sent Events (SSE) for Seat Notifications:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SSE is the optimal choice for seat availability notifications because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-Way Communication&lt;/strong&gt;: Seat updates flow server → client only (no client → server needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler Implementation&lt;/strong&gt;: Native browser support with automatic reconnection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better Performance&lt;/strong&gt;: Lower memory overhead (~2KB vs ~8KB per WebSocket connection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher Scalability&lt;/strong&gt;: Can handle 50K+ connections per pod vs 10K for WebSockets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Resilience&lt;/strong&gt;: Automatic reconnection and error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perfect Use Case Match&lt;/strong&gt;: Ideal for push notifications like seat availability changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  WebSocket vs SSE: When to Use Each
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Why WebSocket Has Lower Latency (1ms vs 8ms):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Connection&lt;/strong&gt;: No HTTP handshake overhead per message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary Protocol&lt;/strong&gt;: More efficient than HTTP text-based SSE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional&lt;/strong&gt;: Client can send acknowledgments, reducing server-side queuing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Connection&lt;/strong&gt;: No intermediate services (SNS/SQS) adding latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use WebSocket When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional Communication&lt;/strong&gt;: Chat, real-time collaboration, gaming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Latency Critical&lt;/strong&gt;: Real-time trading, live sports updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Features&lt;/strong&gt;: User can send commands/responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Protocols&lt;/strong&gt;: Need binary data or custom message formats&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use SSE When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-Way Communication&lt;/strong&gt;: Seat availability updates, notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Infrastructure&lt;/strong&gt;: Leverage existing HTTP caching, CDNs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser Compatibility&lt;/strong&gt;: Better support for automatic reconnection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler Implementation&lt;/strong&gt;: No need for connection state management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: /events/{event_id}/sections/updates&lt;br&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: Authorization: Bearer  (anonymous, ready) OR Authorization: Bearer  (logged-in, ready)&lt;br&gt;
&lt;strong&gt;Message&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"section_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"available_seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-09-02T12:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;: General Admission counts and reserved seat holds are pushed in real time. Updates are throttled to reduce load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Params Are in the Path vs. Request Body
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event ID, Section ID&lt;/strong&gt; → Path Params: Represent resources in a hierarchy (/events/{event_id}/sections/{section_id}/seats). REST convention prefers path params when accessing a specific resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seats, GA Section, GA Quantity&lt;/strong&gt; → Request Body: Represent actions or state changes (reserving seats). Describe what the client wants to do, not the resource being fetched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Token / JWT&lt;/strong&gt; → Headers: Authentication and authorization tokens always go in headers, not bodies or paths, to keep APIs clean, stateless, and cacheable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation keeps the API design RESTful, predictable, and consistent with industry practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anonymous users are tracked via queue_token.&lt;/li&gt;
&lt;li&gt;Logged-in users are tracked via JWT/account ID; no token issued.&lt;/li&gt;
&lt;li&gt;Queue status is required for all users during high-demand events before reserving seats.&lt;/li&gt;
&lt;li&gt;Pagination improves performance for large sections.&lt;/li&gt;
&lt;li&gt;Real-time updates keep section availability accurate.&lt;/li&gt;
&lt;li&gt;Unauthorized users (no JWT or queue_token) cannot reserve seats.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Virtual Waiting Room: A Deep Dive into High-Scale Queueing
&lt;/h2&gt;

&lt;p&gt;This deep dive focuses on the Queue Management Service that gracefully manages demand spikes for seat reservations. We'll cover the full system design: its runtime architecture, the exact Redis data structures and operations, a modern approach to consistency and recovery, and a head-to-head comparison of two primary Redis queue implementations.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Surviving a Demand Spike
&lt;/h3&gt;

&lt;p&gt;Imagine a major concert ticket sale. Millions of users hit your site simultaneously. Without an admission control system, your backend services—for seat reservation and payment—would be instantly overwhelmed. This leads to a chaotic user experience: slow, unresponsive pages, frustrating timeouts, and a flood of angry customer service calls. This is a critical business problem, costing millions in lost sales and brand trust. The solution isn't to build a bigger backend overnight; it's to create a virtual waiting room that gracefully manages the flood of users and ensures an orderly process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Goals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Protect downstream seat/reservation systems from overload (admission control).&lt;/li&gt;
&lt;li&gt;Maintain predictable, mostly FIFO ordering across millions of waiting users.&lt;/li&gt;
&lt;li&gt;Provide "where am I?" and "how long left?" with low latency.&lt;/li&gt;
&lt;li&gt;Allow anonymous and logged-in users (queue_token vs JWT).&lt;/li&gt;
&lt;li&gt;Recover from cache failures quickly without losing order.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-Goals (handled elsewhere)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Payments and final ticket issuance.&lt;/li&gt;
&lt;li&gt;Full bot mitigation (you will still rate-limit and verify).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  High-Level Architecture
&lt;/h3&gt;

&lt;p&gt;The system's core is a hybrid, two-part architecture: a durable database for long-term state and a blazing-fast Redis cache for real-time operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  Write Path (Join Queue)
&lt;/h4&gt;

&lt;p&gt;The API handles a dual-write process for speed and durability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user joins the queue by hitting an API endpoint. The API authenticates them (logged-in via JWT, anonymous gets a signed queue_token).&lt;/li&gt;
&lt;li&gt;The API performs two writes in parallel: it persists a minimal user entry into the Queue DB (our single source of truth) and enqueues the user into Redis (our hot path for ordering).&lt;/li&gt;
&lt;li&gt;The API instantly returns the user's initial position and ETA, derived from the monotonic sequence number (join_seq) received from the Redis write. &lt;em&gt;We'll discuss the O(1) position calculation algorithm in detail in the "Technical Deep Dive" section below.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Admission (Making People Ready)
&lt;/h4&gt;

&lt;p&gt;A Gatekeeper worker steadily admits users from the head of the queue at a controlled rate (e.g., N users/sec). It marks them "ready" in the DB and emits an event to the message bus for observability and client notification.&lt;/p&gt;

&lt;h4&gt;
  
  
  Read Path (Position, ETA)
&lt;/h4&gt;

&lt;p&gt;In steady state, all reads are served from Redis, providing low-latency updates. If Redis is degraded, the system falls back to a degraded mode (details in section 5).&lt;/p&gt;

&lt;h4&gt;
  
  
  State Transitions
&lt;/h4&gt;

&lt;p&gt;waiting → ready → (reserve) → done/expired. A user who doesn't reserve in time can re-queue subject to system policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Model (DB + Redis)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Relational DB (minimal, durable)
&lt;/h4&gt;

&lt;p&gt;This is our rock-solid, durable layer. We store only the essential, long-lived data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Redis (Reconstructible Cache)
&lt;/h4&gt;

&lt;p&gt;This is our high-performance, live-ordering layer. We'll explore two primary implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A) Sorted Set (ZSET) per event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key: q:{event_id}:z&lt;/li&gt;
&lt;li&gt;Member: user_key (e.g., u:{user_id})&lt;/li&gt;
&lt;li&gt;Score: join_seq (monotonic sequence number)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;B) List + Hash (list as deque, hash for metadata)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key: q:{event_id}:list (RPUSH/LPOP)&lt;/li&gt;
&lt;li&gt;Hash: q:{event_id}:meta:{user_key} → field: join_seq (monotonic sequence number)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Consistency, Failure &amp;amp; Recovery (Event-Sourced Approach)
&lt;/h3&gt;

&lt;p&gt;To build a truly resilient system, we use an event-sourced model where the database is the single source of truth and Redis acts as a reconstructible, high-speed cache. This approach eliminates race conditions and manual intervention.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DB-First Writes&lt;/strong&gt;: All changes, like a user joining or being admitted, are first written to the database by the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized Recovery (The Snapshot + Stream Model)&lt;/strong&gt;: Our primary cache, Redis, is highly available but can still fail. To ensure a fast and accurate rebuild, we combine periodic snapshots with a continuous event stream.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Periodic Snapshots&lt;/strong&gt;: The system periodically takes a snapshot of the Redis queue's live ordering state. This is a quick baseline backup of the essential user data and their order. These snapshots are stored in durable storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid Rebuild&lt;/strong&gt;: If Redis crashes, a dedicated recovery worker performs two steps: It loads the latest durable snapshot into Redis. It then queries the event log table to replay only the events that occurred since that snapshot was taken. This is a much smaller number of events, allowing for a fast and reliable catch-up.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How CDC Event Replay Works&lt;/strong&gt;: CDC maintains a complete log of all database changes with timestamps. When Redis needs to be rebuilt, the recovery worker queries the CDC log for all changes that occurred after the last snapshot timestamp. This approach provides complete coverage since CDC captures every database change automatically, ensuring no events are missed during recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis Design Choices &amp;amp; Time Complexity
&lt;/h3&gt;

&lt;p&gt;A fundamental challenge in high-scale queueing is guaranteeing predictable behavior. A simple timestamp isn't reliable because millions of users could join the queue at the exact same millisecond, leading to a race condition and a random, non-deterministic order. Additionally, system clocks can sometimes go backwards due to clock synchronization issues (NTP adjustments, server reboots, etc.), which would make timestamp-based ordering even more confusing. To solve this, we use a monotonically increasing sequence number (join_seq) generated by Redis's atomic INCR command. This ensures that every single user gets a unique, sequential ticket, making the queue's internal logic perfectly deterministic.&lt;/p&gt;

&lt;p&gt;This simple design choice unlocks a massive performance benefit: O(1) position lookup. While native Redis commands for position finding are slow (O(logN) for Sorted Sets and O(N) for Lists), we can bypass them entirely. We simply track the sequence number of the queue's head and subtract it from the user's join_seq to get their exact position in constant time. This is the "Ah-Ha!" moment that makes our system so fast and responsive for millions of users checking their status.&lt;/p&gt;

&lt;h4&gt;
  
  
  A) Sorted Set (ZSET)
&lt;/h4&gt;

&lt;p&gt;A Redis Sorted Set is a unique data structure that combines a hash table and a skip list to balance performance. A skip list is a probabilistic data structure that offers logarithmic time complexity (O(logN)) for searches, insertions, and deletions, much like a balanced binary search tree, but is generally simpler to implement.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enqueue (ZADD)&lt;/strong&gt;: O(logN)

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: When you add a new user to the Sorted Set using the ZADD command, Redis needs to place it in the correct, sorted position. A Redis Sorted Set is a hybrid data structure that uses a skip list to maintain order. Finding the correct spot in a skip list to insert an element requires traversing a subset of the elements, which is a logarithmic time operation, hence O(logN).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Dequeue (Batch ZPOPMIN)&lt;/strong&gt;: O(M⋅logN)

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: The ZPOPMIN command removes the element with the lowest score (the head of your queue). The complexity is O(logN) for a single removal. When the operation is done in a batch to remove M users, Redis has to perform this O(logN) removal process M times. This multiplies the complexity, resulting in O(M⋅logN).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Get User's Position&lt;/strong&gt;: O(1) (bypasses the native ZRANK command).&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why ZRANK Has O(logN) Complexity&lt;/strong&gt;: Redis Sorted Sets use skip lists where each node maintains &lt;strong&gt;counts&lt;/strong&gt; - the number of elements between itself and the next node at each level. To find a user's rank:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start at the header&lt;/strong&gt;: Begin at the top level of the skip list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traverse levels&lt;/strong&gt;: For each level, follow forward pointers while the next node's score &amp;lt; target score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accumulate counts&lt;/strong&gt;: Add the count stored at each node to the running rank total&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level descent&lt;/strong&gt;: Move down one level and repeat until reaching the bottom level&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Count Maintenance&lt;/strong&gt;: When inserting/deleting elements, Redis must update counts at all affected levels. Each insertion requires updating counts at log(N) levels on average, and each deletion requires similar updates to maintain count accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why O(logN)&lt;/strong&gt;: Even with counts, ZRANK must traverse log(N) levels, performing count accumulation at each level. For 1M users: ~20 level traversals × count operations = O(logN) complexity.&lt;/p&gt;

&lt;h4&gt;
  
  
  B) List + Hash
&lt;/h4&gt;

&lt;p&gt;This design separates the two primary functions of the queue: a List for ordered processing and a Hash for user metadata. A List is a doubly linked list, optimized for fast additions and removals from the head and tail. A Hash provides O(1) access to a user's metadata.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enqueue (RPUSH)&lt;/strong&gt;: O(1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dequeue (LPOP)&lt;/strong&gt;: O(1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get User's Position&lt;/strong&gt;: O(1) (bypasses the native LINDEX command).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why LINDEX Has O(N) Complexity&lt;/strong&gt;: The native LINDEX command finds an element at a specific index within a Redis List. A Redis List is implemented as a doubly linked list. To find the position of a specific user, Redis has no choice but to start from the beginning of the list and traverse each element one by one until it finds the matching user ID. This means the time it takes is directly proportional to the number of elements in the list, making it a linear time operation, or O(N). For a large queue with millions of users, this would be prohibitively slow and would not scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  How We Achieve O(1)
&lt;/h3&gt;

&lt;p&gt;We achieve a constant time position lookup by bypassing native Redis commands entirely and using simple application-level logic. This is possible because our queue design is based on a monotonically increasing sequence number (join_seq).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monotonic Counter&lt;/strong&gt;: The key to this approach is that every user is assigned a unique, sequential number upon joining the queue. This number (join_seq) serves as their unchanging ticket number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking the Head&lt;/strong&gt;: The system maintains a global counter for the "head" of the queue, representing the join_seq of the user currently being processed or the last user admitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Simple Math&lt;/strong&gt;: A user's position is simply the difference between their join_seq and the current head of the queue. This is a single subtraction operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: If the queue head is at join_seq = 1,000,000, and a user has join_seq = 1,000,150, their position is 1,000,150 − 1,000,000 = 150. This calculation is a single step that takes constant time, regardless of whether there are 10 users or 10 million in the queue. This mathematical approach transforms a potentially slow operation into an instant one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Con of O(1) Application-Level Logic
&lt;/h3&gt;

&lt;p&gt;While powerful, relying on application-level logic for O(1) lookups creates a risk of race conditions. A user's position could be calculated incorrectly if the head_seq counter changes mid-request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to Prevent Race Conditions&lt;/strong&gt;: The most reliable method is to use a Redis Lua script. The script atomically fetches the user's join_seq and the head_seq counter. Redis guarantees that the entire script runs as a single, indivisible operation, eliminating any risk of a race condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lua Script for Atomic Position Lookup&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Atomic position calculation to prevent race conditions&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;user_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;head_seq_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;queue_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;-- Get user's join sequence number&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;user_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ZSCORE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user_seq&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"User not in queue"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- Get current head sequence&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;head_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_seq_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;head_seq&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;head_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- Calculate position atomically&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head_seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;head_seq&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Race Condition Prevention&lt;/strong&gt;: The Lua script ensures atomicity by fetching both user_seq and head_seq in a single Redis operation, preventing the race condition where head_seq changes between the two reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The specific implementation of the Lua script will vary slightly based on the chosen Redis data structure. For the List + Hash model, the join_seq is fetched from the Hash, while for the Sorted Set model, it is retrieved as the score of the user's member in the set.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Gives Your Product
&lt;/h3&gt;

&lt;p&gt;This design delivers a robust, scalable, and resilient queue that transforms a chaotic demand spike into a predictable user experience. By separating concerns—the DB for durable identity, Redis for real-time ordering, and an event bus for reliable communication—we've built a system that provides a smooth user experience, ensures business continuity, and is easy to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Grand Unification of Ticketing: Seat Management Done Right
&lt;/h2&gt;

&lt;p&gt;The art of building a reliable ticketing platform lies in one core principle: preventing chaos. When millions of fans are clamoring for a limited number of tickets, your system must be a bastion of order. This deep dive into the Seat Management Service reveals the crucial mechanisms—database transactions and locking—that ensure every reservation is a clean, atomic operation, preventing the nightmare of overselling and double-booking.&lt;/p&gt;

&lt;p&gt;We'll build this system using three key tables: Seats to manage unique tickets, Sections for overall availability, and Reservations to handle temporary holds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Database as Your Security Guard 🛡️
&lt;/h3&gt;

&lt;p&gt;At the heart of our strategy is the database, specifically PostgreSQL. Its powerful transactional capabilities allow us to treat a series of operations as a single, indivisible unit. The entire reservation process either succeeds completely, or it fails and is entirely rolled back, leaving no trace behind. This is the ACID principle in action, guaranteeing atomicity and consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Atomic Reservation Flow
&lt;/h3&gt;

&lt;p&gt;When a user requests seats, the backend initiates a carefully choreographed sequence of database operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Start the Transaction
&lt;/h4&gt;

&lt;p&gt;The very first action is to begin a transaction. Think of this as putting a "Do Not Disturb" sign on the data you're about to work on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Check &amp;amp; Lock for a Flawless Hold
&lt;/h4&gt;

&lt;p&gt;This is where we prevent overselling. The process differs slightly depending on the type of seating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Reserved Seating: Pessimistic Locking 🔒&lt;/strong&gt;&lt;br&gt;
When a user selects a unique seat (e.g., "Seat A101"), the system immediately places a lock on that exact seat row in the database. This is a pessimistic lock, so named because we're being pessimistic and assuming another user might want the same seat. It guarantees that no other user can even read or attempt to modify that seat's status until our transaction is complete. The other user is forced to wait, preventing a conflict from ever occurring.&lt;/p&gt;

&lt;p&gt;This approach is perfect for reserved seats because they are unique and non-fungible. If the query returns fewer seats than requested, it means some were already taken. The transaction is immediately rolled back, and the user receives an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Select the requested seat and lock it for update&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A101'&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why not Optimistic Locking for Reserved Seats?&lt;/strong&gt; While optimistic locking can offer higher concurrency, it's a poor fit for unique, non-fungible items like reserved seats. The approach involves checking for conflicts at the final moment of the transaction. A second user could start a reservation on the same seat, only to have their entire transaction rejected at the very end when the system detects the conflict. This creates a frustrating and unpredictable user experience, as a "seat not available" message delivered late in the process is far worse than an immediate one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparing the Costs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost of a Pessimistic Lock&lt;/strong&gt;: The cost is a single, brief wait at the beginning of the process. The application sends one SELECT...FOR UPDATE query to the database, which handles all locking and waiting internally. This is a predictable, easy-to-manage cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost of a Failed Optimistic Lock&lt;/strong&gt;: This cost is deceptive. While it avoids an up-front wait, it introduces the high cost of a complete re-run when a conflict occurs. The application must perform an entire sequence of operations—fetching data, running business logic, and attempting the final update—only for the update to fail. The application then has to discard all the work and restart the entire process, leading to redundant CPU cycles and wasted database round trips.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The essential difference is that a pessimistic lock's cost is a single, brief, and transparent wait. A failed optimistic lock's cost is the wasteful re-execution of a full and complex reservation attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For General Admission (GA): Optimistic Locking 🟢&lt;/strong&gt;&lt;br&gt;
For GA, we must avoid a bottleneck. Many users will try to reserve GA slots at the same time, all hitting the same Sections table row. A pessimistic lock would serialize these requests, forcing them to wait in a single line, which defeats the purpose of the queuing system.&lt;/p&gt;

&lt;p&gt;Instead, we use optimistic locking, which assumes conflicts are rare. We don't lock the data upfront. We rely on a single, atomic SQL statement to perform a compare-and-swap (CAS) operation, checking and updating the value simultaneously. This approach is highly performant and scalable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Conflicts Are Rare in GA Reservations:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Conflicts occur when &lt;code&gt;seats_remaining&lt;/code&gt; drops to 0 or below the requested quantity between the time a user checks availability and attempts to reserve. This is rare because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large Capacity Buffers&lt;/strong&gt;: GA sections typically have thousands of seats (2,000-10,000+), making it unlikely for the last few seats to be contested simultaneously.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Queue-Controlled Access&lt;/strong&gt;: The queuing system already limits concurrent users. Only users who have been "admitted" from the queue can attempt reservations, reducing simultaneous access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time Windows&lt;/strong&gt;: Users have limited time (10-15 minutes) to complete reservations, and most successful reservations happen within the first few minutes of admission.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Natural Distribution&lt;/strong&gt;: User behavior naturally spreads out reservation attempts - some users are faster at selecting, others take time to decide.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch Processing&lt;/strong&gt;: The system can process multiple small reservations (1-4 tickets) simultaneously without conflict, as the remaining capacity buffer is usually large enough.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;When Conflicts Do Occur&lt;/strong&gt;: Conflicts become more likely only when &lt;code&gt;seats_remaining&lt;/code&gt; approaches very low numbers (e.g., &amp;lt; 10 seats remaining), but by then most users have already completed their reservations, and the queue system has already controlled the flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Conflicts Are NOT Rare for Reserved Seats&lt;/strong&gt;: Unlike GA sections with thousands of seats, reserved seating creates high conflict scenarios. Popular seats (front row, center sections, VIP areas) are unique and non-fungible - only one person can have seat A-101. When thousands of users simultaneously target the same premium seats, conflicts are inevitable. This is why reserved seating requires pessimistic locking to prevent overselling and ensure data consistency, while GA's large capacity buffers make optimistic locking viable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ga_quantity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'section_GA1'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ga_quantity&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single statement is executed atomically. The WHERE clause acts as our check, ensuring the database only performs the UPDATE if the seats_remaining count is sufficient at that exact moment. If another transaction has already updated the count, the WHERE condition will fail, and the UPDATE will affect zero rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is better than Pessimistic Locking for GA&lt;/strong&gt;: This approach avoids the single-row bottleneck. The database does not need to serialize all reservation requests; it only needs to check for a conflict at the moment of the update. This allows for massive parallelism, making the system highly scalable for high-demand GA events.&lt;/p&gt;

&lt;h4&gt;
  
  
  Performance Analysis: Locking Strategies Comparison
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Pessimistic Locking Performance (Reserved Seats)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row Lookup&lt;/strong&gt;: O(log N) - B-tree index lookup by seat_id&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Acquisition&lt;/strong&gt;: O(1) - Single row lock after finding row&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Duration&lt;/strong&gt;: O(1) - Constant time for transaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contention Impact&lt;/strong&gt;: O(N) - Linear with concurrent users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Case&lt;/strong&gt;: 10-20ms (no contention)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worst Case&lt;/strong&gt;: 5-10 seconds (high contention)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average Case&lt;/strong&gt;: 50-100ms (moderate contention)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CAS-Based Performance (General Admission)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row Lookup&lt;/strong&gt;: O(log N) - B-tree index lookup by section_id&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Operation&lt;/strong&gt;: O(1) - Single row update with condition check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Retries&lt;/strong&gt;: O(1) - Either succeeds or fails atomically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success Case&lt;/strong&gt;: 20-30ms (atomic update succeeds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure Case&lt;/strong&gt;: 20-30ms (atomic update fails due to insufficient seats)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Concurrency&lt;/strong&gt;: O(log N) per transaction regardless of load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Locking Strategy Decision Matrix&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Pessimistic&lt;/th&gt;
&lt;th&gt;CAS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consistency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency (Low Load)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency (High Load)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slow&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Usage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;td&gt;Complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reserved Seats&lt;/td&gt;
&lt;td&gt;General Admission&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Step 3: Update Counts
&lt;/h4&gt;

&lt;p&gt;With the seats or GA section securely locked, we can now confidently update their availability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- For reserved seating, update the seat status to 'reserved' (with availability check)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reserved'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A101'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Also update the count in the sections table&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'section_A'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step ensures the section-level availability information is always accurate for other users browsing the event.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: Create the Reservation Record
&lt;/h4&gt;

&lt;p&gt;A new record is inserted into the Reservations table. This record is the master holding information, linking the user to their selected seats or GA quantity. Crucially, we set a Time-to-Live (TTL) using the expires_at timestamp. This is an essential safety net. It ensures that if a user doesn't complete the payment, the reservation will automatically expire and release the seats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create reservation header&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount_minor_units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'res_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'10 minutes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8950&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;-- Link reserved seat to reservation&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation_seat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reservation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rs_001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'res_789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A-101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 5: The Final Commitment
&lt;/h4&gt;

&lt;p&gt;If all preceding steps succeed, we execute the COMMIT command. This makes all changes permanent. At this point, the seats are officially marked as reserved (or the GA count is reduced), and the locks are released. If any step failed, the ROLLBACK command is issued, and the database returns to its state before the transaction began.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- All changes are made permanent and locks are released&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- If a failure occurs, all changes are undone&lt;/span&gt;
&lt;span class="c1"&gt;-- ROLLBACK;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Expiring Abandoned Reservations
&lt;/h3&gt;

&lt;p&gt;No matter how robust your system, some users will abandon their carts. To prevent these "seat leaks," a separate background process, or worker, periodically scans the Reservations table for records where expires_at is in the past. When an expired reservation is found, the worker executes a new transaction to return the seats to the available pool. For reserved seats, it updates their status to available. For GA, it adds the ga_quantity back to the seats_remaining in the Sections table. This mechanism ensures that inventory is never held indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Race Condition Considerations&lt;/strong&gt;: The cleanup process must handle concurrent operations safely. Multiple cleanup jobs, new reservations being created during cleanup, and concurrent seat releases can all cause data inconsistency if not properly managed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Cleanup log table for monitoring and debugging&lt;/span&gt;
&lt;span class="c1"&gt;-- This table tracks every cleanup operation so we can monitor the system's health&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cleanup_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;log_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- Unique identifier for each log entry&lt;/span&gt;
    &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                          &lt;span class="c1"&gt;-- Which section was cleaned up&lt;/span&gt;
    &lt;span class="n"&gt;expired_ga_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                            &lt;span class="c1"&gt;-- How many GA seats were released&lt;/span&gt;
    &lt;span class="n"&gt;expired_reserved_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;-- How many reserved seats were released&lt;/span&gt;
    &lt;span class="n"&gt;affected_reservations&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;-- How many reservations were marked as expired&lt;/span&gt;
    &lt;span class="n"&gt;cleaned_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- When the cleanup happened&lt;/span&gt;
    &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_cleaned_at&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;-- Index for efficient queries by time&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- PostgreSQL version (more robust with better locking)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;cleanup_expired_reservations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_section_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;v_expired_ga_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_expired_reserved_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_affected_rows&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="c1"&gt;-- Start a transaction block (automatically handled in procedures in PG &amp;gt;=11)&lt;/span&gt;
    &lt;span class="c1"&gt;-- We still use EXCEPTION handling for rollback on any error.&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="c1"&gt;-- Lock the section row to prevent concurrent cleanups&lt;/span&gt;
        &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Lock all affected reservations early to prevent concurrent modifications&lt;/span&gt;
        &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_ga&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;
        &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Count expired GA reservations for this section&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_expired_ga_count&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_ga&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Count expired reserved seat reservations for this section&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_expired_reserved_count&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Restore seats_remaining count&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_expired_ga_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;v_expired_reserved_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seats_remaining&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;v_expired_ga_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;v_expired_reserved_count&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Re-release reserved seats for this section only&lt;/span&gt;
        &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Mark expired reservations (both GA and reserved) as expired for this section&lt;/span&gt;
        &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'expired'&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending_payment'&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_ga&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt;
              &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; 
                  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; 
                  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;-- Count affected rows (optional: count only those for this section)&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_affected_rows&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'expired'&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_ga&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rga&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt;
              &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_seats&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; 
                  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seat_id&lt;/span&gt; 
                  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_section_id&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;-- Log cleanup operation&lt;/span&gt;
        &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;cleanup_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;expired_ga_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;expired_reserved_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;affected_reservations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;cleaned_at&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;p_section_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;v_expired_ga_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;v_expired_reserved_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;v_affected_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Error during cleanup: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SQLERRM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Race Condition Protections&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row-Level Locking&lt;/strong&gt;: &lt;code&gt;FOR UPDATE&lt;/code&gt; prevents concurrent section modifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Operations&lt;/strong&gt;: All updates in a single transaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Handling&lt;/strong&gt;: Automatic rollback on any failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Trail&lt;/strong&gt;: Preserves expired reservations for compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Cleanup log tracks operations for debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Hyper-Scale Real-Time System: A Production-Ready Deep Dive
&lt;/h2&gt;

&lt;p&gt;A detailed technical guide to designing a resilient, high-performance, and scalable notification platform.&lt;/p&gt;

&lt;p&gt;In the high-stakes world of live event ticketing, a real-time notification system is a critical business tool, not a luxury. It directly drives revenue by preventing lost sales and builds brand trust by providing a transparent user experience.&lt;/p&gt;

&lt;p&gt;A static "seats available" number leads to frustration and cart abandonment when users attempt to reserve seats that are already gone. A real-time system provides an accurate, live view of inventory, which eliminates this negative experience. Additionally, seeing seat availability change dynamically creates a sense of urgency that encourages faster purchasing decisions.&lt;/p&gt;

&lt;p&gt;Building a real-time system capable of serving millions of concurrent users requires a series of deliberate architectural choices that prioritize decoupling, resilience, and performance. This article provides a deep dive into a production-ready architecture for a large-scale ticketing notification system. We will define each component's role, trace the data flow in detail, and analyze the system's resilience to failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1: Foundational Components
&lt;/h3&gt;

&lt;p&gt;This architecture is composed of several specialized components, each responsible for a distinct part of the data lifecycle.&lt;/p&gt;

&lt;h4&gt;
  
  
  Event Producers &amp;amp; The Durable Log (Amazon MSK)
&lt;/h4&gt;

&lt;p&gt;The system is event-driven. State changes are captured automatically using Change Data Capture (CDC) from the database, ensuring complete decoupling between high-frequency reservation operations and event publishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How CDC Works in Practice:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database Changes&lt;/strong&gt;: When the Seat Management Service commits a reservation transaction (e.g., seat status changed from 'available' to 'reserved'), the database records the change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Capture&lt;/strong&gt;: CDC automatically captures all database changes at the transaction log level, including:

&lt;ul&gt;
&lt;li&gt;Table modifications (seats, reservations, sections)&lt;/li&gt;
&lt;li&gt;Before/after values for each change&lt;/li&gt;
&lt;li&gt;Transaction metadata and timestamps&lt;/li&gt;
&lt;li&gt;User context from the application&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Transformation&lt;/strong&gt;: A CDC processor transforms raw database changes into business events:

&lt;ul&gt;
&lt;li&gt;Raw UPDATE → 'seat_reserved' event&lt;/li&gt;
&lt;li&gt;Raw INSERT → 'reservation_created' event&lt;/li&gt;
&lt;li&gt;Raw DELETE → 'reservation_expired' event&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MSK Publishing&lt;/strong&gt;: Transformed events are published to appropriate Kafka topics (e.g., 'seat-events', 'reservation-events')&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Performance Impact&lt;/strong&gt;: Reservation operations have no latency impact since CDC runs asynchronously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream Processing&lt;/strong&gt;: Other services (Availability Aggregation, Notifications) consume these events to update their state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MSK serves as the system's durable log and provides several critical functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling&lt;/strong&gt;: It decouples producers from consumers, allowing them to operate and scale independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability &amp;amp; Replayability&lt;/strong&gt;: Events are durably stored, allowing them to be re-processed in case of a consumer-side error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure Management&lt;/strong&gt;: The bus acts as a large buffer, absorbing traffic spikes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Processing Engine (Apache Flink)
&lt;/h4&gt;

&lt;p&gt;The Availability Aggregation Service is implemented as a stateful stream processing application using Apache Flink. It consumes the raw event stream from MSK and transforms it into clean, aggregated availability counts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateful Processing&lt;/strong&gt;: Flink uses a keyBy operation on the section_id to ensure all events for a single section are processed sequentially by the same task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault Tolerance&lt;/strong&gt;: Flink is configured to take periodic checkpoints of its state to a durable store like Amazon S3, guaranteeing data accuracy through failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windowed Aggregation &amp;amp; Throttling&lt;/strong&gt;: Instead of emitting an update for every single event, Flink can operate on windows of time. For example, it can collect all events for a specific section over a 2-second window, perform a single aggregation at the end of that window, and then emit a single, consolidated update. This micro-batching is crucial for throttling the update stream, reducing the load on the entire downstream notification system and preventing the end user's UI from flickering with an excessive number of rapid updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Distribution Fleet (Amazon EKS)
&lt;/h4&gt;

&lt;p&gt;The Real-time Notification Service is a fleet of containerized applications deployed on Amazon EKS (Elastic Kubernetes Service). These long-running pods are the "SSE hosts."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Connections&lt;/strong&gt;: Each pod is capable of maintaining thousands of persistent Server-Sent Events (SSE) connections with clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local State&lt;/strong&gt;: Each pod maintains two private, in-memory hash tables to manage its local connections, enabling extremely fast lookups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Scalability&lt;/strong&gt;: The fleet runs as a Kubernetes Deployment, allowing it to be scaled horizontally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Connection Manager (Application Load Balancer)
&lt;/h4&gt;

&lt;p&gt;An Application Load Balancer (ALB) sits in front of the EKS fleet, terminating TLS, performing health checks, and distributing incoming SSE connection requests to the EKS pods using a "Least Connections" algorithm.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Routing Directory (Amazon ElastiCache for Redis)
&lt;/h4&gt;

&lt;p&gt;A central, low-latency in-memory database acts as our real-time routing table, or "Director."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Function&lt;/strong&gt;: Its primary job is to maintain a live map between a logical topic (e.g., section_id) and the physical identifiers of the EKS pods that are currently serving clients interested in that topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation&lt;/strong&gt;: This is implemented using Amazon ElastiCache for Redis, configured for high availability. The data structure is a simple Redis Set: section:104:subscribers -&amp;gt; { "pod-a-ip", "pod-c-ip" }.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Part 2: The Core Architecture — A Deep Dive into the Data Flow
&lt;/h3&gt;

&lt;p&gt;The architecture is designed for low-latency, targeted message delivery.&lt;/p&gt;

&lt;h4&gt;
  
  
  Phase 1: Connection &amp;amp; State Registration
&lt;/h4&gt;

&lt;p&gt;This phase establishes the client connection and builds the two-layer routing map.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A client initiates an SSE connection request to the ALB. The ALB selects a target pod, pod-C, and forwards the request.&lt;/li&gt;
&lt;li&gt;The application inside pod-C establishes the SSE connection and assigns it a unique internal ID.&lt;/li&gt;
&lt;li&gt;The client sends a subscription message (e.g., for section-104).&lt;/li&gt;
&lt;li&gt;pod-C updates its two local, in-memory hash tables:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forward Map&lt;/strong&gt; (connectionId -&amp;gt; sections): Used for efficient cleanup when the client disconnects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse Map&lt;/strong&gt; (section -&amp;gt; connectionIds): Used for O(1) lookups during message delivery.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pod-C then updates the Redis Director, adding its own unique, addressable identifier to the Redis Set for section-104.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   redis.SADD("section:104:subscribers", "pod-c-ip")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Phase 2: Real-Time Message Delivery
&lt;/h4&gt;

&lt;p&gt;This phase traces an event from processing to final delivery.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Flink application publishes a processed update for section-104 to an Amazon SNS Topic.&lt;/li&gt;
&lt;li&gt;SNS invokes a subscribed AWS Lambda function (the "Router Lambda").&lt;/li&gt;
&lt;li&gt;The Lambda function queries the Redis Director to get the set of pod identifiers subscribed to section-104.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   redis.SMEMBERS("section:104:subscribers") -&amp;gt; returns { "pod-c-ip", "pod-f-ip" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The Lambda performs a direct, service-to-service push to the target pods via an internal API endpoint on each pod.&lt;/li&gt;
&lt;li&gt;The target pod, pod-C, receives this internal API call.&lt;/li&gt;
&lt;li&gt;pod-C performs a highly efficient O(1) lookup in its local in-memory reverse_map to get the list of specific client connectionIds.&lt;/li&gt;
&lt;li&gt;The pod's application iterates through this list and writes the message into the open SSE streams for each connection.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Message Delivery Optimization Analysis
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Fanout Pattern Performance&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct Broadcast (Naive Approach)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 message → 100K users = 100K individual WebSocket writes
Time Complexity: O(N) where N = number of subscribers
Memory Complexity: O(N) for connection management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SNS Fanout (Optimized Approach)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 message → SNS → 100K Lambda invocations → 100K WebSocket writes
Time Complexity: O(1) for publish + O(N) for delivery
Memory Complexity: O(1) for SNS + O(N) for Lambda memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Batching Optimization&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 messages → Batch → 1K WebSocket writes
Time Complexity: O(N/K) where K = batch size
Memory Complexity: O(N/K) for batched connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Message Delivery Guarantees&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At-Most-Once Delivery&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implementation&lt;/strong&gt;: Fire-and-forget with no acknowledgments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Non-critical updates (seat availability changes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Highest throughput, lowest latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off&lt;/strong&gt;: Some messages may be lost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;At-Least-Once Delivery&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implementation&lt;/strong&gt;: Retry mechanism with acknowledgments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Critical updates (payment confirmations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Lower throughput, higher latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off&lt;/strong&gt;: Some messages may be duplicated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Exactly-Once Delivery&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implementation&lt;/strong&gt;: Idempotent processing with deduplication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Case&lt;/strong&gt;: Financial transactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Lowest throughput, highest latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off&lt;/strong&gt;: Highest complexity, highest reliability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Part 3: Bulletproofing the System — A Deep Dive into Resilience
&lt;/h3&gt;

&lt;p&gt;A production-ready architecture must be designed explicitly for failure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Resilience of the EKS Fleet
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pod Failure&lt;/strong&gt;: If a pod crashes, Kubernetes's ReplicaSet immediately launches a replacement. The ALB's health check will have already stopped routing traffic to the failed pod. Clients that were connected to it will trigger their automatic reconnect logic and will be seamlessly routed to a healthy pod by the ALB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node Failure&lt;/strong&gt;: The EKS worker nodes are managed by an Auto Scaling Group. If an EC2 instance fails, the ASG will terminate it and launch a replacement. Kubernetes will then automatically reschedule the affected pods onto the remaining healthy nodes in the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Resilience of the Director (Redis Cluster)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary Defense (High Availability)&lt;/strong&gt;: The Director is deployed as an Amazon ElastiCache for Redis cluster with Multi-AZ and automatic failover enabled. If the primary Redis node fails, ElastiCache automatically promotes a replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster Recovery (Graceful Degradation)&lt;/strong&gt;: In a total service failure, the Router Lambda can be programmed with a fallback. Upon detecting that Redis is unavailable, it could revert to a less efficient broadcast model or log the failure and wait for recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Benchmarks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Queue Management Performance Comparison
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Maximum QPS &amp;amp; Memory at 10,000 Concurrent Users:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;P95 Latency&lt;/th&gt;
&lt;th&gt;Max QPS&lt;/th&gt;
&lt;th&gt;Memory (10K users)&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis ZSET&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;25,000&lt;/td&gt;
&lt;td&gt;400MB&lt;/td&gt;
&lt;td&gt;O(logN)&lt;/td&gt;
&lt;td&gt;Skip list implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis List+Hash&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;320MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Linked list + hash table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database Queue&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;1GB&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;PostgreSQL-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-Memory Array&lt;/td&gt;
&lt;td&gt;1ms&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;200MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Single-threaded only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reservation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pessimistic Lock&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;800MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Strong consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reservation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optimistic Lock&lt;/td&gt;
&lt;td&gt;15ms&lt;/td&gt;
&lt;td&gt;8,000&lt;/td&gt;
&lt;td&gt;600MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;High concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reservation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database Transaction&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;td&gt;1,500&lt;/td&gt;
&lt;td&gt;500MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;ACID guarantees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Notification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis Pub/Sub&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;30,000&lt;/td&gt;
&lt;td&gt;200MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Fire-and-forget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Notification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WebSocket Direct&lt;/td&gt;
&lt;td&gt;1ms&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;800MB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Bidirectional, persistent connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Notification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSE + SNS&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;td&gt;1.2GB&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;AWS managed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Memory Usage Calculation Breakdown
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Performance at 10,000 Concurrent Users:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue Methods:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis ZSET (400MB)&lt;/strong&gt;: 10K users × 32 bytes (user_id + score + skip list pointers) + 25% Redis overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis List+Hash (320MB)&lt;/strong&gt;: 10K users × 40 bytes (list + hash entries) + Redis metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Queue (1GB)&lt;/strong&gt;: 10K users × 100 bytes (PostgreSQL row) + connection pools + indexes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory Array (200MB)&lt;/strong&gt;: 10K users × 8 bytes (integer user_id) + JVM overhead + GC buffers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reservation Methods:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pessimistic Lock (800MB)&lt;/strong&gt;: 10K concurrent locks × 1KB (lock metadata) + database buffers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic Lock (600MB)&lt;/strong&gt;: No lock overhead, just version numbers + database buffers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Transaction (500MB)&lt;/strong&gt;: 10K transactions × 50 bytes (transaction state) + database buffers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notification Methods:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Pub/Sub (200MB)&lt;/strong&gt;: 10K QPS × 100 bytes (message overhead) + connection buffers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket Direct (800MB)&lt;/strong&gt;: 10K connections × 8KB (connection state) + WebSocket server overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE + SNS (1.2GB)&lt;/strong&gt;: 10K connections × 2KB (SSE state) + AWS Lambda + SNS message processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Assumptions&lt;/strong&gt;: 10K concurrent users baseline, Redis overhead 20-25%, database connection pools, network buffers, AWS service overhead. Actual usage varies by implementation details and configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Metrics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;High-Demand Event Benchmarks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Peak Queue Length&lt;/strong&gt;: 2M+ users waiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admission Rate&lt;/strong&gt;: 1,000 users/second (controlled)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reservation Success Rate&lt;/strong&gt;: 95%+ (5% timeout/abandonment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification Delivery&lt;/strong&gt;: 99.9% within 2 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Recovery&lt;/strong&gt;: &amp;lt;30 seconds from Redis failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resource Utilization&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Cluster&lt;/strong&gt;: 3 nodes, 16GB RAM each, 50% CPU utilization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: 8 cores, 32GB RAM, 70% CPU utilization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification Fleet&lt;/strong&gt;: 20 pods, 4GB RAM each, 60% CPU utilization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring &amp;amp; Observability
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Metrics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Queue Management Metrics&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue Length&lt;/strong&gt;: Current number of users waiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average Wait Time&lt;/strong&gt;: Mean time from join to admission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admission Rate&lt;/strong&gt;: Users admitted per second&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Position Accuracy&lt;/strong&gt;: Consistency of position calculations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Memory Usage&lt;/strong&gt;: Memory consumption by queue data structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reservation System Metrics&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reservation Success Rate&lt;/strong&gt;: Percentage of successful reservations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Contention&lt;/strong&gt;: Number of lock conflicts per second&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Duration&lt;/strong&gt;: Average time for reservation transactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abandonment Rate&lt;/strong&gt;: Percentage of users who don't complete payment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat Release Rate&lt;/strong&gt;: Expired reservations released per minute&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-Time Notification Metrics&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Message Delivery Rate&lt;/strong&gt;: Notifications delivered per second&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery Latency&lt;/strong&gt;: Time from event to user notification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection Health&lt;/strong&gt;: Active WebSocket/SSE connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Loss Rate&lt;/strong&gt;: Failed deliveries due to connection drops&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fanout Efficiency&lt;/strong&gt;: Messages per SNS publish&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alerting Thresholds
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical Alerts&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queue length &amp;gt; 1M users&lt;/li&gt;
&lt;li&gt;Reservation failure rate &amp;gt; 10%&lt;/li&gt;
&lt;li&gt;Notification delivery latency &amp;gt; 5 seconds&lt;/li&gt;
&lt;li&gt;Redis memory usage &amp;gt; 80%&lt;/li&gt;
&lt;li&gt;Database connection pool exhaustion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Warning Alerts&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queue length &amp;gt; 500K users&lt;/li&gt;
&lt;li&gt;Reservation failure rate &amp;gt; 5%&lt;/li&gt;
&lt;li&gt;Notification delivery latency &amp;gt; 2 seconds&lt;/li&gt;
&lt;li&gt;Redis memory usage &amp;gt; 60%&lt;/li&gt;
&lt;li&gt;Average wait time &amp;gt; 15 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Channels&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Critical&lt;/strong&gt;: PagerDuty (immediate escalation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning&lt;/strong&gt;: Slack (team notification)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Info&lt;/strong&gt;: Email (daily reports)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Logging Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Queue System Logs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User join/leave events with timestamps&lt;/li&gt;
&lt;li&gt;Position changes and ETA updates&lt;/li&gt;
&lt;li&gt;Redis operation performance&lt;/li&gt;
&lt;li&gt;Queue admission decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reservation System Logs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lock acquisition/release events&lt;/li&gt;
&lt;li&gt;Transaction success/failure with reasons&lt;/li&gt;
&lt;li&gt;Seat status changes&lt;/li&gt;
&lt;li&gt;Payment timeout events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notification System Logs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message publish events&lt;/li&gt;
&lt;li&gt;Delivery confirmations&lt;/li&gt;
&lt;li&gt;Connection establishment/teardown&lt;/li&gt;
&lt;li&gt;Fanout performance metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Disaster Recovery
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure Scenarios
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Queue System Failures&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Cluster Failure&lt;/strong&gt;: 15-30 second recovery with automatic failover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Order Corruption&lt;/strong&gt;: 2-5 minute recovery using database reconstruction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position Calculation Errors&lt;/strong&gt;: Immediate detection via monitoring, &amp;lt;1 minute fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admission Rate Overload&lt;/strong&gt;: Automatic throttling, 30 second stabilization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reservation System Failures&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database Connection Loss&lt;/strong&gt;: 10-20 second recovery with connection pooling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Deadlock&lt;/strong&gt;: Automatic detection and resolution in &amp;lt;5 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Rollback&lt;/strong&gt;: Immediate cleanup, no data corruption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat Status Inconsistency&lt;/strong&gt;: Background reconciliation process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notification System Failures&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket Connection Drops&lt;/strong&gt;: Automatic reconnection in &amp;lt;3 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SNS Service Outage&lt;/strong&gt;: Fallback to direct database polling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda Function Timeout&lt;/strong&gt;: Automatic retry with exponential backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Bus Failure&lt;/strong&gt;: Graceful degradation to polling mode&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recovery Procedures
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Queue System Recovery&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Redis Failover&lt;/strong&gt;: Automatic promotion of replica to primary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Reconstruction&lt;/strong&gt;: Rebuild from database using join_seq ordering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position Recalculation&lt;/strong&gt;: Update all user positions using head_seq tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admission Resume&lt;/strong&gt;: Restart controlled admission at safe rate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Reservation System Recovery&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connection Pool Reset&lt;/strong&gt;: Clear failed connections and establish new ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Cleanup&lt;/strong&gt;: Release any orphaned locks from failed transactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat Status Audit&lt;/strong&gt;: Verify all seat statuses match reservation records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Log Replay&lt;/strong&gt;: Replay any committed transactions that weren't reflected&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Notification System Recovery&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connection Re-establishment&lt;/strong&gt;: Reconnect all dropped WebSocket/SSE connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Replay&lt;/strong&gt;: Replay missed notifications from event log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fanout Restart&lt;/strong&gt;: Resume SNS-based message distribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Notification&lt;/strong&gt;: Notify users of temporary service interruption&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Backup Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Queue State Backup&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Replication&lt;/strong&gt;: Continuous Redis replication to standby cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Log Backup&lt;/strong&gt;: All queue events stored in durable database (primary recovery mechanism)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Reconstruction&lt;/strong&gt;: Rebuild queue from database events if Redis fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTO/RPO&lt;/strong&gt;: 30 second recovery time, 5 second data loss maximum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reservation Data Backup&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database Replication&lt;/strong&gt;: Real-time replication to standby database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Log Backup&lt;/strong&gt;: Continuous backup of all reservation transactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat Map Backup&lt;/strong&gt;: Daily backup of seat configuration and status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTO/RPO&lt;/strong&gt;: 15 second recovery time, 1 second data loss maximum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notification System Backup&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Message Queue Backup&lt;/strong&gt;: SNS topic replication across regions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection State Backup&lt;/strong&gt;: Periodic backup of active connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Stream Backup&lt;/strong&gt;: All notification events stored in durable log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTO/RPO&lt;/strong&gt;: 45 second recovery time, 10 second data loss maximum&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h"&gt;&lt;strong&gt;Part 4&lt;/strong&gt;: Payments and ticket issuance (idempotency and consistency)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gemini.google.com/share/15d3d82059b5" rel="noopener noreferrer"&gt;Review by Google Gemini&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>database</category>
      <category>redis</category>
      <category>sse</category>
    </item>
    <item>
      <title>Part 2: Event Discovery</title>
      <dc:creator>Sumedh Bala</dc:creator>
      <pubDate>Thu, 23 Oct 2025 17:46:01 +0000</pubDate>
      <link>https://dev.to/sumedhbala/part-2-event-discovery-32o5</link>
      <guid>https://dev.to/sumedhbala/part-2-event-discovery-32o5</guid>
      <description>&lt;h2&gt;
  
  
  Search &amp;amp; Indexing
&lt;/h2&gt;

&lt;p&gt;Event discovery is the foundation of any ticketing system. The goal is to let users quickly find concerts, sports matches, or theater events by name, location, or venue. While it looks simple from the outside, the design must handle millions of queries, abbreviations (LA vs. Los Angeles), nearby cities (Pasadena), and fuzzy matches (typos like San Franciso).&lt;/p&gt;

&lt;p&gt;This breakdown covers the baseline design first, followed by a deep dive into enhanced search capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Baseline Functional Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Services
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search API Service&lt;/strong&gt; – Handles user queries, normalizes input, executes searches, and returns ranked results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexing Service&lt;/strong&gt; – Updates the search index whenever new events are created or updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Management Service (EMS)&lt;/strong&gt; – Manages canonical event metadata and feeds it to the search index.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Databases &amp;amp; Caches
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary Database&lt;/strong&gt; (PostgreSQL / MySQL / DynamoDB) – Stores event metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Index&lt;/strong&gt; (Elasticsearch / OpenSearch / Solr) – Provides full-text and geospatial search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt; (Redis / Memcached) – Stores trending searches and frequently accessed results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Database Schema (Events Table)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"Taylor Swift Eras Tour"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;venue_name&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;Venue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"SoFi Stadium"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;city_normalized&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;Canonical&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"Los Angeles"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="k"&gt;State&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;province&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"CA"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="nv"&gt;"USA"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;location_lat&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;Venue&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;
&lt;span class="n"&gt;location_lng&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;Venue&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;
&lt;span class="n"&gt;event_start_time&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="k"&gt;Start&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;
&lt;span class="n"&gt;event_end_time&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="k"&gt;End&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;
&lt;span class="n"&gt;search_synonyms&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;synonyms&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;"LA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"L.A."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"Los Angeles"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Why Indexes Differ by Column
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;event_id&lt;/strong&gt; → B-Tree index, fast lookup after search returns candidate IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;event_name, venue_name, search_synonyms&lt;/strong&gt; → Full-text or trigram indexes (support partial matches/typos).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;city_normalized, state, country&lt;/strong&gt; → B-Tree index (exact match queries).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;location_lat, location_lng&lt;/strong&gt; → GiST or SP-GiST index (PostGIS) for geo queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;event_start_time, event_end_time&lt;/strong&gt; → B-Tree index for range queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite Indexes&lt;/strong&gt; → (city_normalized, event_start_time) for common multi-column queries like "finding all upcoming events in Los Angeles, ordered by date."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Use the right index type per query pattern: B-Trees for equality/range, GIN/trigrams for text, GiST for geo, and composite for multi-column queries. PostgreSQL handles the system of record; Elasticsearch handles fuzzy candidate generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. APIs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Search API (Basic)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;: GET /search&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Parameters&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;q (string) → user query ("LA concerts")&lt;/li&gt;
&lt;li&gt;location (optional, lat/lng) → geo filtering&lt;/li&gt;
&lt;li&gt;radius (optional, default 30km)&lt;/li&gt;
&lt;li&gt;date_range (optional, start + end)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Response&lt;/strong&gt;: JSON object with an events array, each including:&lt;br&gt;
event_id, event_name, venue, city, state, country, event_start_time, location (lat/lng)&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Example SQL Queries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Find events in a city&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;city_normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Los Angeles'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_start_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find events within 30 km radius&lt;/strong&gt;:&lt;br&gt;
Use spherical distance function with lat/lng coordinates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find events by name (text search)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%Taylor Swift%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Problems Using PostgreSQL Alone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free-text search is slow at scale.&lt;/li&gt;
&lt;li&gt;Fuzzy/typo handling is limited.&lt;/li&gt;
&lt;li&gt;Synonyms/aliases require hacks.&lt;/li&gt;
&lt;li&gt;Ranking, autocomplete, aggregations are inefficient.&lt;/li&gt;
&lt;li&gt;High-QPS text search is not distributed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why Elasticsearch/OpenSearch is Needed&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inverted indices → sub-linear text lookup&lt;/li&gt;
&lt;li&gt;Built-in typo tolerance, synonyms, stemming&lt;/li&gt;
&lt;li&gt;BM25 ranking &amp;amp; boosting&lt;/li&gt;
&lt;li&gt;Efficient geo + text + time queries&lt;/li&gt;
&lt;li&gt;Autocomplete and faceted counts&lt;/li&gt;
&lt;li&gt;Horizontally scalable and fault-tolerant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Soundbite for interviews&lt;/strong&gt;: "Postgres is the source of truth, but breaks down for fuzzy search, synonyms, relevance, autocomplete, or scale. Production systems pair it with Elasticsearch/OpenSearch."&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Enhancing User Experience
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synonym Expansion&lt;/strong&gt;: Map "LA" → "Los Angeles" and expand queries accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geolocation Queries&lt;/strong&gt;: Lat/lng filtering ensures nearby cities (e.g., Pasadena) appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Similarity Search&lt;/strong&gt;: Fuzzy matching using Elasticsearch analyzers or trigram indexes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User Query Flow Example&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User types "LA concerts."&lt;/li&gt;
&lt;li&gt;Query normalized (lowercase, punctuation removed).&lt;/li&gt;
&lt;li&gt;Synonym expansion (LA → Los Angeles).&lt;/li&gt;
&lt;li&gt;Geo coordinates retrieved.&lt;/li&gt;
&lt;li&gt;Search index queried (text + geo filter).&lt;/li&gt;
&lt;li&gt;Results ranked (relevance, time, popularity, distance).&lt;/li&gt;
&lt;li&gt;Results cached if trending.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  7. Non-Functional Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: Redis caching → sub-100ms for popular searches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Elasticsearch distributed indexing/sharding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt;: Replicated DB + multi-AZ Elasticsearch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: Event Management Service is source of truth; eventual consistency to search index&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Deeper Dive: Fuzzy Search
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dictionary: all cities (~200k–500k entries)&lt;/li&gt;
&lt;li&gt;User input: "San Franciso" (typo of "San Francisco")&lt;/li&gt;
&lt;li&gt;Goal: find the closest matching city efficiently, despite typos&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8.1 Naive Levenshtein Distance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare input string with every city in the dictionary&lt;/li&gt;
&lt;li&gt;Compute edit distance (insertions, deletions, substitutions) for each city&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: "San Franciso"&lt;/li&gt;
&lt;li&gt;"San Francisco" → edit distance = 2 → match&lt;/li&gt;
&lt;li&gt;"Los Angeles" → edit distance = 11 → ignored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Data storage&lt;/strong&gt;: Simple array or list of city names (no preprocessing)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time complexity&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let n = number of cities, m = average city name length&lt;/li&gt;
&lt;li&gt;Computing edit distance per city: O(m²) &lt;em&gt;(see detailed explanation at end of document)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Total: O(n * m²) → very slow for large n&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Simple to implement&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Not scalable for real-time search with hundreds of thousands of entries&lt;/p&gt;
&lt;h3&gt;
  
  
  8.2 BK-Tree (Burkhard-Keller Tree)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: BK-Trees are designed for approximate string matching under a metric (commonly Levenshtein distance). They allow you to find all strings within a given "distance" efficiently, without scanning the entire dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each node = a string (city name)&lt;/li&gt;
&lt;li&gt;Each edge = edit distance between parent and child&lt;/li&gt;
&lt;li&gt;If a child exists at that edge, insertion continues down that edge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Root Choice&lt;/strong&gt;: The first inserted word becomes the root. Root choice doesn't affect correctness, but a "central" root can reduce tree depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insertion Example (Detailed)&lt;/strong&gt;:&lt;br&gt;
Insertion order: Los Angeles → Pasadena → Glendale → San Jose → San Mateo → San Francisco → Fresno&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert Los Angeles → root&lt;/li&gt;
&lt;li&gt;Insert Pasadena → dist(Pasadena, Los Angeles) = 8 → add under Los Angeles&lt;/li&gt;
&lt;li&gt;Insert Glendale → dist(Glendale, Los Angeles) = 8 → edge exists → recurse into Pasadena → dist(Glendale, Pasadena) = 7 → add under Pasadena&lt;/li&gt;
&lt;li&gt;Insert San Jose → dist(San Jose, Los Angeles) = 11 → add under Los Angeles&lt;/li&gt;
&lt;li&gt;Insert San Mateo → dist(San Mateo, Los Angeles) = 11 → recurse into San Jose → dist(San Mateo, San Jose) = 4 → add under San Jose&lt;/li&gt;
&lt;li&gt;Insert San Francisco → dist(San Francisco, Los Angeles) = 10 → add under Los Angeles&lt;/li&gt;
&lt;li&gt;Insert Fresno → dist(Fresno, Los Angeles) = 10 → recurse into San Francisco → dist(Fresno, San Francisco) = 9 → add under San Francisco&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Final Tree&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Los Angeles──(8)──▶ Pasadena  ──(7)──▶ Glendale
              ──(10)──▶ San Francisco  ──(9)──▶ Fresno
              ──(11)──▶ San Jose       ──(4)──▶ San Mateo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Search Logic (Fuzzy Search)&lt;/strong&gt;:&lt;br&gt;
Query: "San Jsoe" (typo for San Jose), max edit distance = 2&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start at Los Angeles → dist = 11 → allowed child edges [9,13]&lt;/li&gt;
&lt;li&gt;Children within range: San Francisco (10), San Jose (11)&lt;/li&gt;
&lt;li&gt;Check San Francisco → dist = 6 → prune branch&lt;/li&gt;
&lt;li&gt;Check San Jose → dist = 1 → ✅ Match found&lt;/li&gt;
&lt;li&gt;Explore San Jose's children → San Mateo → dist = 6 → prune&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key Observations&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Branch pruning via [d-k, d+k] makes search efficient&lt;/li&gt;
&lt;li&gt;Chains form naturally when cities share distances&lt;/li&gt;
&lt;li&gt;Insertion order affects tree shape but not correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BK-Tree Complexity&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distance computation per node: O(m²)&lt;/li&gt;
&lt;li&gt;Average case: O(log n) nodes visited&lt;/li&gt;
&lt;li&gt;Worst case: O(n) nodes visited&lt;/li&gt;
&lt;li&gt;Total search time: O(v * m²), v = nodes actually visited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Intuition&lt;/strong&gt;: BK-Tree is like a binary tree where only relevant branches are explored, but each node comparison costs O(m²).&lt;/p&gt;
&lt;h3&gt;
  
  
  8.3 Trie + Levenshtein Automaton
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store all city names in a Trie (prefix tree)&lt;/li&gt;
&lt;li&gt;Example: "San Francisco" → S → a → n → _ → F → r → …&lt;/li&gt;
&lt;li&gt;Use Levenshtein Automaton to traverse the Trie:

&lt;ul&gt;
&lt;li&gt;Track allowed edits at each step&lt;/li&gt;
&lt;li&gt;Prune paths exceeding threshold&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time Complexity&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worst-case: O(n)&lt;/li&gt;
&lt;li&gt;Average case with pruning: much less → efficient for large dictionaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Scales well, precise, dynamic typo handling&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Higher implementation complexity than BK-Tree&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Fuzzy Search with Trie + Levenshtein Automaton (Beginner-Friendly)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Suppose you have a list of city names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;["San Francisco", "San Jose", "San Mateo", "Los Angeles", "Fresno"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to search for &lt;code&gt;"San Jsoe"&lt;/code&gt; (typo) and still find &lt;code&gt;"San Jose"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can do this efficiently using &lt;strong&gt;two things together&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;Trie&lt;/strong&gt; (prefix tree) to store city names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Levenshtein distance&lt;/strong&gt; to measure "how different" two strings are.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;1. Trie Basics&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Trie&lt;/strong&gt; is a tree where each node is a letter. Words are formed by following a path from the root to a leaf.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Root
 |
 S
 |
 a
 |
 n
 |
(space)
 ├── F → "San Francisco"
 ├── J → "San Jose"
 └── M → "San Mateo"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Every path from root → leaf = a city name.&lt;/li&gt;
&lt;li&gt;We can quickly follow letters to see which words match a prefix.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;2. Levenshtein Distance Basics&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Levenshtein distance = &lt;strong&gt;minimum edits to turn one string into another&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edit = insert, delete, or change a letter&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;"San Jsoe"&lt;/code&gt; → &lt;code&gt;"San Jose"&lt;/code&gt; → 1 edit (swap &lt;code&gt;'s'&lt;/code&gt; and &lt;code&gt;'o'&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We allow a &lt;strong&gt;maximum distance&lt;/strong&gt; (e.g., 2) so we match words even with typos.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;3. How the Search Works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of comparing &lt;code&gt;"San Jsoe"&lt;/code&gt; with every city (slow), we combine &lt;strong&gt;Trie + Levenshtein Automaton&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At each Trie node, we maintain a &lt;strong&gt;distance row&lt;/strong&gt; representing &lt;strong&gt;edits needed&lt;/strong&gt; to match the query so far.&lt;/li&gt;
&lt;li&gt;Row length = query length + 1 = 8 + 1 = 9.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 0: Start at empty string&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Query = &lt;code&gt;"San Jsoe"&lt;/code&gt; (8 chars)&lt;br&gt;
Trie path = &lt;code&gt;""&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Distance row from empty string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0, 1, 2, 3, 4, 5, 6, 7, 8]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Index = number of query characters considered&lt;/li&gt;
&lt;li&gt;Value = edits needed to turn empty string → query prefix&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Add first letter &lt;code&gt;'S'&lt;/code&gt; → Trie path &lt;code&gt;"S"&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Compute new row using formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;new_row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;previous_row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# deletion
&lt;/span&gt;    &lt;span class="n"&gt;new_row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# insertion
&lt;/span&gt;    &lt;span class="n"&gt;previous_row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;  &lt;span class="c1"&gt;# substitution (0 if match, 1 if not)
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example table:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;j&lt;/th&gt;
&lt;th&gt;Query prefix&lt;/th&gt;
&lt;th&gt;Compare 'S' vs query[j-1]&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Computation&lt;/th&gt;
&lt;th&gt;new_row[j]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;""&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0 + 1 = 1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;"S"&lt;/td&gt;
&lt;td&gt;S vs S&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;min(1+1,1+1,0+0)=0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;"Sa"&lt;/td&gt;
&lt;td&gt;S vs a&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(2+1,0+1,1+1)=1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;"San"&lt;/td&gt;
&lt;td&gt;S vs n&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(3+1,1+1,2+1)=2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;"San "&lt;/td&gt;
&lt;td&gt;S vs (space)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(4+1,2+1,3+1)=3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;"San J"&lt;/td&gt;
&lt;td&gt;S vs J&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(5+1,3+1,4+1)=4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;"San Js"&lt;/td&gt;
&lt;td&gt;S vs s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(6+1,4+1,5+1)=5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;"San Jso"&lt;/td&gt;
&lt;td&gt;S vs o&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(7+1,5+1,6+1)=6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;"San Jsoe"&lt;/td&gt;
&lt;td&gt;S vs e&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(8+1,6+1,7+1)=7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Resulting row:&lt;/strong&gt; &lt;code&gt;[1, 0, 1, 2, 3, 4, 5, 6, 7]&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 2: Add &lt;code&gt;'a'&lt;/code&gt; → Trie path &lt;code&gt;"Sa"&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Example table:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;j&lt;/th&gt;
&lt;th&gt;Query prefix&lt;/th&gt;
&lt;th&gt;Compare 'a' vs query[j-1]&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Computation&lt;/th&gt;
&lt;th&gt;new_row[j]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;""&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1 + 1 = 2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;"S"&lt;/td&gt;
&lt;td&gt;a vs S&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(0+1,2+1,1+1)=1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;"Sa"&lt;/td&gt;
&lt;td&gt;a vs a&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;min(1+1,1+1,0+0)=0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;"San"&lt;/td&gt;
&lt;td&gt;a vs n&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(2+1,0+1,1+1)=1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;"San "&lt;/td&gt;
&lt;td&gt;a vs (space)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(3+1,1+1,2+1)=2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;"San J"&lt;/td&gt;
&lt;td&gt;a vs J&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(4+1,2+1,3+1)=3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;"San Js"&lt;/td&gt;
&lt;td&gt;a vs s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(5+1,3+1,4+1)=4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;"San Jso"&lt;/td&gt;
&lt;td&gt;a vs o&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(6+1,4+1,5+1)=5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;"San Jsoe"&lt;/td&gt;
&lt;td&gt;a vs e&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min(7+1,5+1,6+1)=6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Resulting row:&lt;/strong&gt; &lt;code&gt;[2, 1, 0, 1, 2, 3, 4, 5, 6]&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 3: Add &lt;code&gt;'n'&lt;/code&gt; → Trie path &lt;code&gt;"San"&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Distance row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3, 2, 1, 0, 1, 2, 3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Minimum distance = 0 → path &lt;code&gt;"San"&lt;/code&gt; matches query &lt;code&gt;"San"&lt;/code&gt; perfectly so far.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 4: Add space &lt;code&gt;' '&lt;/code&gt; → Trie path &lt;code&gt;"San "&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Distance row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[4, 2, 1, 1, 0, 1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Minimum distance = 0 → &lt;code&gt;"San "&lt;/code&gt; matches query so far&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 5: Continue down &lt;code&gt;"San J"&lt;/code&gt; → &lt;code&gt;"San Jose"&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;'J'&lt;/code&gt; → distance row: &lt;code&gt;[5,4,3,2,1,0,1,2,3]&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;'o'&lt;/code&gt; → &lt;code&gt;[6,5,4,3,2,1,1,2,3]&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;'s'&lt;/code&gt; → &lt;code&gt;[7,6,5,4,3,2,1,1,2]&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;'e'&lt;/code&gt; → &lt;code&gt;[8,7,6,5,4,3,2,1,1]&lt;/code&gt; ✅&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final distance = 1 → &lt;strong&gt;MATCH FOUND&lt;/strong&gt; &lt;code&gt;"San Jose"&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 6: Early Pruning&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"San Francisco"&lt;/code&gt; → distance eventually &amp;gt; 2 → stop exploring&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"San Jose"&lt;/code&gt; → distance ≤ 2 → match&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"San Mateo"&lt;/code&gt; → distance &amp;gt; 2 → stop&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 7: Intuition&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Walk down the Trie (letter by letter).&lt;/li&gt;
&lt;li&gt;Track minimum edits to match the query at each step (distance row).&lt;/li&gt;
&lt;li&gt;Stop exploring paths where edits exceed max allowed distance.&lt;/li&gt;
&lt;li&gt;If a complete word reaches end node within max edits → success!&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This method powers &lt;strong&gt;fuzzy search&lt;/strong&gt;, &lt;strong&gt;autocomplete&lt;/strong&gt;, and &lt;strong&gt;spell correction&lt;/strong&gt; in real systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Key Advantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Early Pruning&lt;/strong&gt;: Stop exploring paths when distance exceeds threshold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Prefixes&lt;/strong&gt;: Multiple cities with same prefix share computation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental Updates&lt;/strong&gt;: Distance matrix updates are O(m) per character&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory Efficient&lt;/strong&gt;: Only store one distance matrix per search path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Performance Comparison&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Time Complexity&lt;/th&gt;
&lt;th&gt;Space&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Naive Levenshtein&lt;/td&gt;
&lt;td&gt;O(n × m²)&lt;/td&gt;
&lt;td&gt;O(m²)&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BK-Tree&lt;/td&gt;
&lt;td&gt;O(log n × m²)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trie + Levenshtein Automaton&lt;/td&gt;
&lt;td&gt;O(n × m)&lt;/td&gt;
&lt;td&gt;O(n + m)&lt;/td&gt;
&lt;td&gt;Complex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;7. When to Use&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large dictionaries&lt;/strong&gt; (100k+ entries)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frequent fuzzy searches&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory constraints&lt;/strong&gt; (trie is more compact than BK-tree)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Need for prefix-based filtering&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Trie + Levenshtein Automaton approach is the most sophisticated but also the most efficient for large-scale fuzzy string matching in production systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.4 Trigram Index / Elasticsearch Approach
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create Trigrams: Break each city name into 3-character overlapping sequences

&lt;ul&gt;
&lt;li&gt;Example: "San Francisco" → ["San", "an ", "n F", " Fr", "Fra", "ran", "anc", "nci", "cis", "isc", "sco"]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Build Inverted Index: Each trigram → list of city IDs

&lt;ul&gt;
&lt;li&gt;Example: "Fra" → [San Francisco, Frankfurt]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Querying: Input "San Franciso" → generate trigrams → retrieve all cities containing at least one trigram → compute similarity score → return top matches&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scales to millions of entries&lt;/li&gt;
&lt;li&gt;Handles typos, partial matches, multi-word queries&lt;/li&gt;
&lt;li&gt;Produces ranked results for UX&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Requires search engine infrastructure, slightly more memory overhead&lt;/p&gt;

&lt;h2&gt;
  
  
  9. How Elasticsearch Ranks Results: BM25 + Boosting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  BM25 (Best Match 25)
&lt;/h3&gt;

&lt;p&gt;Statistical ranking algorithm (improvement on TF-IDF)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key ingredients&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Term Frequency (TF)&lt;/strong&gt; → frequency of term in field, diminishing returns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inverse Document Frequency (IDF)&lt;/strong&gt; → rare terms weigh more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field Length Normalization&lt;/strong&gt; → shorter fields get higher weight&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Boosting
&lt;/h3&gt;

&lt;p&gt;Manually tweak ranking&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field Boosting&lt;/strong&gt;: event_name^3, artist_name^2, city^1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Boosting&lt;/strong&gt;: recency, proximity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Logic Boosting&lt;/strong&gt;: trending events, sponsored events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interview Takeaway&lt;/strong&gt;: BM25 + Boosting ensures results are relevant (BM25) and aligned with business/user intent (boosting).&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Indexing Every PostgreSQL Field: Pros &amp;amp; Cons
&lt;/h2&gt;

&lt;p&gt;When designing a large-scale ticketing system, deciding which fields to index in PostgreSQL is crucial, especially when pairing it with a specialized search engine like Elasticsearch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros of PostgreSQL Indexing:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backup System&lt;/strong&gt;: PostgreSQL can act as a reliable fallback if Elasticsearch experiences an outage, ensuring continuous service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Access for Specific Queries&lt;/strong&gt;: For certain precise queries or filters (e.g., direct lookups by event_id), direct access through PostgreSQL indexes can be significantly faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong Consistency &amp;amp; Data Integrity&lt;/strong&gt;: Indexes on crucial fields (like event_id, often as a primary key) are paramount for enforcing strong consistency and data integrity. While the guarantee of integrity comes from database constraints (e.g., PRIMARY KEY, UNIQUE) and transactional properties, indexes are essential for the efficient and scalable enforcement of these guarantees. They enable the database to quickly check for uniqueness and relationships, preventing issues like duplicate events, even when the events table contains millions of records. This is a core requirement that search engines are not designed to fulfill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified Read-Your-Own-Writes&lt;/strong&gt;: Immediate query results for newly created or updated events are possible directly from PostgreSQL, providing consistent results for the user who made the change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of PostgreSQL Indexing:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Increased Write Overhead&lt;/strong&gt;: More indexes generally lead to slower insert and update operations, as each index needs to be maintained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher Storage Costs&lt;/strong&gt;: Each index consumes additional disk space, increasing overall storage expenses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inefficient for Complex Queries&lt;/strong&gt;: PostgreSQL's built-in text and fuzzy search capabilities are less efficient at scale compared to specialized search engines like Elasticsearch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance Complexity&lt;/strong&gt;: Managing and tuning a large number of indexes can add to operational overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recommendation for Ticketing Systems: Strategic PostgreSQL Indexing
&lt;/h3&gt;

&lt;p&gt;While it's not necessary to index every field in PostgreSQL, it plays a vital role. The primary responsibility for high-volume, complex search queries is offloaded to Elasticsearch, which is horizontally scalable and optimized for such tasks.&lt;/p&gt;

&lt;p&gt;PostgreSQL indexes serve two critical, complementary roles in this architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ensuring Data Integrity and Transactional Consistency&lt;/strong&gt;: Indexes on crucial fields like event_id (typically as a primary key) are paramount. They enable the database to efficiently enforce UNIQUE and PRIMARY KEY constraints, ensuring that event data is always correct and preventing issues like duplicate event entries. This is a core requirement that search engines cannot guarantee.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Providing a Failsafe&lt;/strong&gt;: Event creation and updates are typically low-volume operations in a ticketing system (compared to search queries). Therefore, the write overhead incurred by maintaining additional PostgreSQL indexes is acceptable. This investment ensures a robust and consistent backup of critical event data, providing a crucial safety net in the event of a search engine outage. The key here is that writes to the events table are very infrequent (as very few events are added relative to read operations), so the overhead of more indexes does not pose a significant problem in terms of performance.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Soundbite for Interview&lt;/strong&gt;: "Mentioning PostgreSQL indexing demonstrates you've considered distributed system failure modes and have a plan B. Downsides are outweighed by reliability, consistency, and data integrity."&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Indexing Service &amp;amp; Event Management Service
&lt;/h2&gt;

&lt;p&gt;These services ensure search data is accurate, timely, and scalable, maintaining a strong connection between canonical event data in PostgreSQL and the search index in Elasticsearch/OpenSearch.&lt;/p&gt;

&lt;h3&gt;
  
  
  11.1 Event Management Service (EMS)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: EMS is the source of truth for all event metadata. Responsible for creating, updating, and managing canonical event records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsibilities&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage all event-related data:

&lt;ul&gt;
&lt;li&gt;Event name, venue, city, state, country&lt;/li&gt;
&lt;li&gt;Start and end times&lt;/li&gt;
&lt;li&gt;Event capacity and ticket types&lt;/li&gt;
&lt;li&gt;Synonyms for search queries (e.g., LA → Los Angeles)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Validate incoming event data for consistency and correctness&lt;/li&gt;

&lt;li&gt;Track changes via event versioning or timestamps&lt;/li&gt;

&lt;li&gt;Emit notifications for downstream systems when events are created or updated&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Considerations for High Scale&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use atomic transactions in PostgreSQL to ensure consistency&lt;/li&gt;
&lt;li&gt;Implement soft deletes to prevent broken references in search index&lt;/li&gt;
&lt;li&gt;Emit event change logs to message broker (Kafka, RabbitMQ) for asynchronous processing by Indexing Service&lt;/li&gt;
&lt;li&gt;Ensure idempotency in updates to avoid duplicate indexing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Flow&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Event created/updated in PostgreSQL&lt;/li&gt;
&lt;li&gt;EMS validates the record&lt;/li&gt;
&lt;li&gt;EMS publishes event notification (message queue / Kafka topic)&lt;/li&gt;
&lt;li&gt;Indexing Service consumes the message to update the search index&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  11.2 Indexing Service
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: Keep the search index up-to-date with canonical data for fast, accurate results&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsibilities&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consume event change notifications from EMS&lt;/li&gt;
&lt;li&gt;Transform data into search-friendly format:

&lt;ul&gt;
&lt;li&gt;Text fields → tokenized&lt;/li&gt;
&lt;li&gt;Locations → geo-points&lt;/li&gt;
&lt;li&gt;Dates → sortable timestamps&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Update/delete documents in Elasticsearch/OpenSearch&lt;/li&gt;

&lt;li&gt;Handle batch updates and real-time streaming&lt;/li&gt;

&lt;li&gt;Maintain versioning to detect stale updates&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Implementation Details&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Message Queue Consumption&lt;/strong&gt;: Kafka, RabbitMQ, or Kinesis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent Updates&lt;/strong&gt;: Each event has a unique ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk Indexing&lt;/strong&gt;: For new events or large updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Handling&lt;/strong&gt;: Failed updates retried, dead-letter queue stores permanently failed updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Consistency Model&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Elasticsearch is eventually consistent&lt;/li&gt;
&lt;li&gt;EMS remains the source of truth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Flow&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive "event_created" message from EMS&lt;/li&gt;
&lt;li&gt;Transform event record (normalize names, add synonyms, compute geo coordinates)&lt;/li&gt;
&lt;li&gt;Push to Elasticsearch via bulk API&lt;/li&gt;
&lt;li&gt;Confirm update or retry if it fails&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  11.3 Architecture &amp;amp; Reliability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling&lt;/strong&gt;: EMS and Indexing Service communicate via message queues → prevents blocking writes in Postgres during indexing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience&lt;/strong&gt;: Retry mechanisms and dead-letter queues for failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Multiple consumers can scale horizontally for high volumes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: Metrics (throughput, error rates, lag), logging full trace (event_id, timestamp, status)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  11.4 Interview Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;EMS ensures canonical, consistent event data&lt;/li&gt;
&lt;li&gt;Indexing Service keeps search index current and performant&lt;/li&gt;
&lt;li&gt;Decoupled, message-driven architecture → scalable &amp;amp; fault-tolerant&lt;/li&gt;
&lt;li&gt;Idempotent + bulk operations → reliability under high load&lt;/li&gt;
&lt;li&gt;Plan B for indexing failures demonstrates awareness of real-world distributed system design&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Performance Benchmarks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Search Performance Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;P95 Latency&lt;/th&gt;
&lt;th&gt;QPS&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB-native&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL ILIKE&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;2GB&lt;/td&gt;
&lt;td&gt;Exact text matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB-native&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL Trigram&lt;/td&gt;
&lt;td&gt;500ms&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;4GB&lt;/td&gt;
&lt;td&gt;Fuzzy text search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Elasticsearch (Fuzzy)&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;td&gt;10,000+&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;Scalable fuzzy search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Elasticsearch (Regex)&lt;/td&gt;
&lt;td&gt;80ms&lt;/td&gt;
&lt;td&gt;8,000+&lt;/td&gt;
&lt;td&gt;10GB&lt;/td&gt;
&lt;td&gt;Regex/wildcard queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenSearch&lt;/td&gt;
&lt;td&gt;60ms&lt;/td&gt;
&lt;td&gt;9,000+&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;AWS-managed Elasticsearch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solr&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;8,500+&lt;/td&gt;
&lt;td&gt;9GB&lt;/td&gt;
&lt;td&gt;Enterprise search platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Algorithm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BK-Tree&lt;/td&gt;
&lt;td&gt;20ms&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;16GB&lt;/td&gt;
&lt;td&gt;Specialized fuzzy matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Algorithm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trie + Levenshtein&lt;/td&gt;
&lt;td&gt;10ms&lt;/td&gt;
&lt;td&gt;15,000+&lt;/td&gt;
&lt;td&gt;6GB&lt;/td&gt;
&lt;td&gt;Efficient fuzzy matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis Cached&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;50,000+&lt;/td&gt;
&lt;td&gt;4GB&lt;/td&gt;
&lt;td&gt;Hot queries only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Trigram Indexes Excel at Complex Queries
&lt;/h3&gt;

&lt;p&gt;While Trie + Levenshtein Automaton is faster for simple fuzzy matching, &lt;strong&gt;Trigram indexes are superior for complex queries&lt;/strong&gt; because they handle multiple search patterns simultaneously. Trigram indexes break text into overlapping 3-character sequences, allowing them to efficiently process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-word queries&lt;/strong&gt;: "Taylor Swift concert" matches documents containing "Taylor", "Swift", and "concert" even with typos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial matches&lt;/strong&gt;: "San Fran*" matches "San Francisco", "San Fernando", "San Francisco Bay"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex patterns&lt;/strong&gt;: Complex regular expressions like &lt;code&gt;[A-Z][a-z]+ [A-Z][a-z]+&lt;/code&gt; for proper names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wildcard queries&lt;/strong&gt;: "conc*" matches "concert", "conference", "conclusion"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrase proximity&lt;/strong&gt;: Finding documents where "Taylor" and "Swift" appear within 5 words of each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance advantage&lt;/strong&gt;: Trigram indexes pre-compute all possible 3-character combinations, making complex pattern matching O(1) lookup time instead of O(n×m²) for each query. This is why Elasticsearch uses trigrams for regex/wildcard queries despite higher memory overhead - the complexity of pattern matching makes the trade-off worthwhile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-world example&lt;/strong&gt;: A query like "LA concerts this weekend" with typos becomes much more efficient with trigram indexing because it can simultaneously match "LA" (city), "concert*" (event type), and "weekend" (time) across multiple fields and handle variations in each term.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Cluster&lt;/strong&gt;: 3-node, 16GB RAM, 500GB index&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Latency&lt;/strong&gt;: P50: 15ms, P95: 45ms, P99: 100ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt;: 8,000 QPS sustained, 15,000 QPS burst&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Hit Rate&lt;/strong&gt;: 85% for trending searches&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  13. Caching Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-Layer Architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CDN Cache&lt;/strong&gt; (CloudFlare) - Static content, 24h TTL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Cache&lt;/strong&gt; (Redis) - Query results, 5-15min TTL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch Query Cache&lt;/strong&gt; - Search index cache&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cache Layers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query Results&lt;/strong&gt;: Popular searches cached for 15 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autocomplete&lt;/strong&gt;: City/venue suggestions cached for 1 hour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo Data&lt;/strong&gt;: Location lookups cached for 24 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trending Searches&lt;/strong&gt;: Top queries cached for 1 hour&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cache Invalidation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event Updates&lt;/strong&gt;: Invalidate related searches by city/venue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-based&lt;/strong&gt;: Different TTLs based on data volatility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Warming&lt;/strong&gt;: Pre-populate trending searches during off-peak&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  14. Monitoring &amp;amp; Observability
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Performance&lt;/strong&gt;: Latency (P50/P95/P99), success rate, error rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Health&lt;/strong&gt;: Elasticsearch cluster status, node health, shard status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Metrics&lt;/strong&gt;: Search volume, popular queries, user engagement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Performance&lt;/strong&gt;: Hit rate, memory usage, eviction rate&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alerting Thresholds
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Critical&lt;/strong&gt;: Search latency &amp;gt; 100ms, error rate &amp;gt; 5%, cluster health = Red&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning&lt;/strong&gt;: Search latency &amp;gt; 50ms, error rate &amp;gt; 1%, cluster health = Yellow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channels&lt;/strong&gt;: PagerDuty (critical), Slack (warnings), Email (reports)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Logging
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Queries&lt;/strong&gt;: Query, user, latency, results count, cache hit status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors&lt;/strong&gt;: Timeout, connection failures, retry attempts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Elasticsearch response times, cache hit rates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  15. Disaster Recovery
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure Scenarios
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Node&lt;/strong&gt;: 0 downtime, automatic shard rebalancing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster Failure&lt;/strong&gt;: 15-30 min downtime, restore from snapshots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Corruption&lt;/strong&gt;: 2-4 hours downtime, full reindex from PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recovery Procedures
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster Health Check&lt;/strong&gt;: Verify cluster status and shard allocation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot Restore&lt;/strong&gt;: Restore from daily snapshots stored in S3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Rebuild&lt;/strong&gt;: Reindex entire dataset from PostgreSQL source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental Sync&lt;/strong&gt;: Catch up on missed updates using timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fallback Strategies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL Fallback&lt;/strong&gt;: Use ILIKE queries with reduced functionality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-Only Mode&lt;/strong&gt;: Serve cached results only during outages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Degradation&lt;/strong&gt;: Show maintenance message with trending events&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backup Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily Snapshots&lt;/strong&gt;: Automated daily backups to S3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Region Replication&lt;/strong&gt;: Real-time replication to backup cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTO/RPO&lt;/strong&gt;: 15-30 min recovery time, 15 min data loss maximum&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Appendix: Why Edit Distance is O(m²)
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Levenshtein distance&lt;/strong&gt; (edit distance) has &lt;strong&gt;O(m²)&lt;/strong&gt; time complexity because it uses a &lt;strong&gt;dynamic programming approach&lt;/strong&gt; with a &lt;strong&gt;2D table&lt;/strong&gt; where m is the length of the strings being compared.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Algorithm Structure
&lt;/h3&gt;

&lt;p&gt;For two strings of length &lt;code&gt;m&lt;/code&gt; each, the algorithm creates a &lt;strong&gt;m × m table&lt;/strong&gt; and fills it using the following recurrence relation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;levenshtein_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Create (m+1) × (n+1) table
&lt;/span&gt;    &lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="c1"&gt;# Base cases
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;

    &lt;span class="c1"&gt;# Fill the table
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# No operation needed
&lt;/span&gt;            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;# Deletion
&lt;/span&gt;                    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;# Insertion  
&lt;/span&gt;                    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# Substitution
&lt;/span&gt;                &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding the Base Cases
&lt;/h3&gt;

&lt;p&gt;The base cases initialize the first row and first column of the DP table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Base cases
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What these base cases represent:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dp[i][0] = i&lt;/code&gt;&lt;/strong&gt; (First column): &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting string1[0...i-1] to an empty string&lt;/li&gt;
&lt;li&gt;Requires &lt;strong&gt;i deletions&lt;/strong&gt; (delete all i characters)&lt;/li&gt;
&lt;li&gt;Example: "San Francisco" → "" needs 13 deletions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dp[0][j] = j&lt;/code&gt;&lt;/strong&gt; (First row):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting empty string to string2[0...j-1] &lt;/li&gt;
&lt;li&gt;Requires &lt;strong&gt;j insertions&lt;/strong&gt; (insert all j characters)&lt;/li&gt;
&lt;li&gt;Example: "" → "San Franciso" needs 12 insertions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Visual representation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     ""  S  a  n     F  r  a  n  c  i  s  o
""  [0][1][2][3][4][5][6][7][8][9][10][11][12]
S   [1][?][?][?][?][?][?][?][?][?][?][?][?]
a   [2][?][?][?][?][?][?][?][?][?][?][?][?]
n   [3][?][?][?][?][?][?][?][?][?][?][?][?]
    [4][?][?][?][?][?][?][?][?][?][?][?][?]
F   [5][?][?][?][?][?][?][?][?][?][?][?][?]
r   [6][?][?][?][?][?][?][?][?][?][?][?][?]
a   [7][?][?][?][?][?][?][?][?][?][?][?][?]
n   [8][?][?][?][?][?][?][?][?][?][?][?][?]
c   [9][?][?][?][?][?][?][?][?][?][?][?][?]
i   [10][?][?][?][?][?][?][?][?][?][?][?][?]
s   [11][?][?][?][?][?][?][?][?][?][?][?][?]
c   [12][?][?][?][?][?][?][?][?][?][?][?][?]
o   [13][?][?][?][?][?][?][?][?][?][?][?][?]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why these base cases make sense:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row 0, Column 0&lt;/strong&gt;: &lt;code&gt;dp[0][0] = 0&lt;/code&gt; (empty string to empty string = 0 operations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row 0, Column j&lt;/strong&gt;: &lt;code&gt;dp[0][j] = j&lt;/code&gt; (empty string to j-character string = j insertions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row i, Column 0&lt;/strong&gt;: &lt;code&gt;dp[i][0] = i&lt;/code&gt; (i-character string to empty string = i deletions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These base cases provide the foundation for the dynamic programming recurrence relation that fills the rest of the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why O(m²)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Table Size&lt;/strong&gt;: We create a table of size &lt;code&gt;(m+1) × (n+1)&lt;/code&gt;, which is approximately &lt;code&gt;m × m&lt;/code&gt; when strings are similar length&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested Loops&lt;/strong&gt;: We have two nested loops:

&lt;ul&gt;
&lt;li&gt;Outer loop: &lt;code&gt;for i in range(1, m + 1)&lt;/code&gt; → &lt;strong&gt;O(m) iterations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Inner loop: &lt;code&gt;for j in range(1, n + 1)&lt;/code&gt; → &lt;strong&gt;O(m) iterations&lt;/strong&gt; (when n ≈ m)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total Operations&lt;/strong&gt;: &lt;code&gt;O(m) × O(m) = O(m²)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Visual Example
&lt;/h3&gt;

&lt;p&gt;For strings "San Francisco" (m=13) and "San Franciso" (m=12):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     S  a  n     F  r  a  n  c  i  s  o
   [0][1][2][3][4][5][6][7][8][9][10][11][12]
S  [1][0][1][2][3][4][5][6][7][8][9][10][11]
a  [2][1][0][1][2][3][4][5][6][7][8][9][10]
n  [3][2][1][0][1][2][3][4][5][6][7][8][9]
   [4][3][2][1][0][1][2][3][4][5][6][7][8]
F  [5][4][3][2][1][0][1][2][3][4][5][6][7]
r  [6][5][4][3][2][1][0][1][2][3][4][5][6]
a  [7][6][5][4][3][2][1][0][1][2][3][4][5]
n  [8][7][6][5][4][3][2][1][0][1][2][3][4]
c  [9][8][7][6][5][4][3][2][1][0][1][2][3]
i  [10][9][8][7][6][5][4][3][2][1][0][1][2]
s  [11][10][9][8][7][6][5][4][3][2][1][0][1]
c  [12][11][10][9][8][7][6][5][4][3][2][1][0]
o  [13][12][11][10][9][8][7][6][5][4][3][2][1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Each cell requires constant time to compute&lt;/strong&gt;, but we need to fill &lt;strong&gt;13 × 12 = 156 cells&lt;/strong&gt;, which is &lt;strong&gt;O(m²)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters in the Ticketing System
&lt;/h3&gt;

&lt;p&gt;In the context of the ticketing system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;City Names&lt;/strong&gt;: "San Francisco" vs "San Franciso" (typo)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale Problem&lt;/strong&gt;: With 200k-500k cities, computing edit distance for each one would be:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per city&lt;/strong&gt;: O(m²) where m ≈ 10-15 characters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total&lt;/strong&gt;: O(n × m²) = O(500,000 × 15²) = O(112.5 million operations)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Impact&lt;/strong&gt;: This is why the document mentions it's "very slow for large n"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Alternative Solutions
&lt;/h3&gt;

&lt;p&gt;This is exactly why the document suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BK-Trees&lt;/strong&gt;: O(log n) nodes visited, but each comparison still costs O(m²)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trie + Levenshtein Automaton&lt;/strong&gt;: More complex but can be optimized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigram Indexing&lt;/strong&gt;: O(1) lookup time for approximate matches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;O(m²)&lt;/strong&gt; complexity is inherent to the edit distance algorithm itself - it's the mathematical cost of computing the minimum number of operations needed to transform one string into another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-3-seat-management-nn2"&gt;&lt;strong&gt;Part 3&lt;/strong&gt;: Seat selection and reservation (concurrency control)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h"&gt;&lt;strong&gt;Part 4&lt;/strong&gt;: Payments and ticket issuance (idempotency and consistency)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gemini.google.com/share/15d3d82059b5" rel="noopener noreferrer"&gt;Review by Google Gemini&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>elasticsearch</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Designing a Large-Scale Ticketing System</title>
      <dc:creator>Sumedh Bala</dc:creator>
      <pubDate>Thu, 23 Oct 2025 17:41:50 +0000</pubDate>
      <link>https://dev.to/sumedhbala/designing-a-large-scale-ticketing-system-2h1c</link>
      <guid>https://dev.to/sumedhbala/designing-a-large-scale-ticketing-system-2h1c</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Inspired by &lt;a href="https://www.youtube.com/watch?v=fhdPyoO6aXI&amp;amp;t=48s" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=fhdPyoO6aXI&amp;amp;t=48s&lt;/a&gt;&lt;br&gt;
Ticketing systems such as Ticketmaster or Eventbrite appear simple on the surface: a user finds an event, selects a seat, and completes the purchase. To a user, it feels like a straightforward transaction. In reality, these systems are among the most challenging distributed systems to design because they must handle extreme concurrency, strict consistency requirements, fraud prevention, and unpredictable demand spikes.&lt;/p&gt;

&lt;p&gt;This series will walk through the design of a ticketing system step by step, highlighting the functional requirements, non-functional requirements, design trade-offs, and architectural patterns that are relevant for a system design interview.&lt;/p&gt;

&lt;p&gt;This series takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go deep into multiple areas – sometimes interviewers drive the follow-ups, not the candidate. Each functional requirement is treated as a system design interview in itself.&lt;/li&gt;
&lt;li&gt;Cover database schemas and APIs – some interviewers want to see practical modeling and integration.&lt;/li&gt;
&lt;li&gt;Build reusable solutions – patterns from one domain (e.g., event search) can often apply to others (e.g., product search).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Functional Requirements
&lt;/h2&gt;

&lt;p&gt;A ticketing system supports the entire user journey: discovery, selection, purchase, and ticket delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Discovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Search for events by location, category, or date.&lt;/li&gt;
&lt;li&gt;View event details (venue, performers, time).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Seat Selection
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Display seating maps with real-time availability.&lt;/li&gt;
&lt;li&gt;Support both reserved seating (specific seat numbers) and general admission (ticket buckets).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reservation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Temporarily reserve seats while a user is in the checkout flow.&lt;/li&gt;
&lt;li&gt;Expire reservations if payment is not completed within a time limit (usually 2–10 minutes depending on policy).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Purchase &amp;amp; Payment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Securely process payments.&lt;/li&gt;
&lt;li&gt;Ensure idempotency (no double-charging if retries occur).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ticket Issuance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate a digital ticket (QR/barcode) upon successful purchase.&lt;/li&gt;
&lt;li&gt;Send confirmation via email and/or mobile app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Non-Functional Requirements
&lt;/h2&gt;

&lt;p&gt;Large-scale ticketing systems are defined as much by how they perform under stress as by their feature set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Handle sudden spikes (e.g., millions of users joining at ticket release time).&lt;/li&gt;
&lt;li&gt;Support horizontal scaling across multiple regions.&lt;/li&gt;
&lt;li&gt;Adapt to bursty traffic patterns rather than steady-state load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Low Latency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Fast responses for search, seat availability, and reservation status (&amp;lt;200 ms target for most read queries).&lt;/li&gt;
&lt;li&gt;Low latency matters because delays directly increase user drop-off during checkout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reliability &amp;amp; Consistency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ensure no double-booking of seats.&lt;/li&gt;
&lt;li&gt;Strong consistency for seat inventory, eventual consistency acceptable for less critical data (e.g., search).&lt;/li&gt;
&lt;li&gt;Techniques such as distributed locks or optimistic concurrency control are critical here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fault Tolerance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gracefully handle partial failures (e.g., payment succeeds but DB write fails).&lt;/li&gt;
&lt;li&gt;Use retries, dead-letter queues, and idempotent APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fairness &amp;amp; Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Queueing or rate limiting to prevent system overload.&lt;/li&gt;
&lt;li&gt;Anti-bot measures such as CAPTCHA, token-based access, and dynamic queueing systems to protect inventory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Real-time logging, monitoring, and alerting.&lt;/li&gt;
&lt;li&gt;Traceability for debugging failed transactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap for This Series
&lt;/h2&gt;

&lt;p&gt;Each upcoming part of this series will address a key aspect of the system, with diagrams, real-world examples, and interview tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-2-event-discovery-32o5"&gt;&lt;strong&gt;Part 2&lt;/strong&gt;: Event discovery (search &amp;amp; indexing)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-3-seat-management-nn2"&gt;&lt;strong&gt;Part 3&lt;/strong&gt;: Seat selection and reservation (concurrency control)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumedhbala/part-4-payments-and-ticket-issuance-3h7h"&gt;&lt;strong&gt;Part 4&lt;/strong&gt;: Payments and ticket issuance (idempotency and consistency)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gemini.google.com/share/15d3d82059b5" rel="noopener noreferrer"&gt;Review by Google Gemini&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of this series, you should be able to discuss the design of a large-scale ticketing system in a structured, interview-ready manner, while demonstrating the ability to balance correctness, scalability, and user experience.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>ticketmaster</category>
    </item>
  </channel>
</rss>
