system design

Low Level Design: Travel Itinerary Service

What Is a Travel Itinerary Service?

Users plan and manage multi-component trips: flights, hotels, car rentals, and activities stitched together in a timeline. The low-level design must handle heterogeneous booking types, time-zone-aware scheduling, conflict detection, and collaborative sharing between travelers.

Data Model

-- Core tables (SQL pseudocode)

CREATE TABLE itineraries (
  itinerary_id    BIGINT PRIMARY KEY,
  owner_user_id   BIGINT,
  title           VARCHAR(255),
  start_date      DATE,
  end_date        DATE,
  status          VARCHAR(32),  -- 'DRAFT', 'CONFIRMED', 'COMPLETED', 'CANCELLED'
  currency        CHAR(3),
  created_at      TIMESTAMP,
  updated_at      TIMESTAMP
);

CREATE TABLE itinerary_items (
  item_id         BIGINT PRIMARY KEY,
  itinerary_id    BIGINT REFERENCES itineraries(itinerary_id),
  item_type       VARCHAR(32),  -- 'FLIGHT', 'HOTEL', 'CAR', 'ACTIVITY'
  external_ref_id VARCHAR(128), -- booking ID from external service
  title           VARCHAR(255),
  start_datetime  TIMESTAMP WITH TIME ZONE,
  end_datetime    TIMESTAMP WITH TIME ZONE,
  location        VARCHAR(255),
  cost            DECIMAL(10,2),
  currency        CHAR(3),
  status          VARCHAR(32),  -- 'PENDING', 'CONFIRMED', 'CANCELLED'
  notes           TEXT,
  sort_order      INT
);

CREATE TABLE itinerary_collaborators (
  itinerary_id    BIGINT REFERENCES itineraries(itinerary_id),
  user_id         BIGINT,
  role            VARCHAR(16),  -- 'VIEWER', 'EDITOR', 'OWNER'
  invited_at      TIMESTAMP,
  accepted_at     TIMESTAMP,
  PRIMARY KEY (itinerary_id, user_id)
);

CREATE TABLE itinerary_documents (
  doc_id          BIGINT PRIMARY KEY,
  itinerary_id    BIGINT REFERENCES itineraries(itinerary_id),
  doc_type        VARCHAR(32),  -- 'TICKET', 'VOUCHER', 'PASSPORT'
  storage_url     VARCHAR(512),
  uploaded_at     TIMESTAMP
);

Core Algorithm: Conflict Detection

When a user adds or moves an itinerary item, the service checks for time overlaps against existing confirmed items. The overlap condition for two intervals A and B is: A.start < B.end AND A.end > B.start. All datetime comparisons are performed in UTC after normalizing from local time zones using the item location’s IANA timezone.

  • On item insert or update, load all confirmed items for the itinerary.
  • Convert all timestamps to UTC.
  • Check the new item against each existing item for overlap.
  • Return a list of conflicts (not a hard block, since layovers and quick transfers are valid in some cases); flag them as warnings in the UI.

Aggregation Workflow: Building the Timeline

The itinerary timeline is assembled by:

  • Fetching all itinerary_items for the given itinerary_id, ordered by start_datetime ASC, sort_order ASC.
  • Grouping items by local calendar date at the destination timezone to produce a day-by-day view.
  • Computing gaps between consecutive items and surfacing them as free time slots in the UI.
  • Aggregating total cost across all items grouped by currency, with an optional FX conversion to the itinerary base currency using a daily exchange rate lookup.

Failure Handling

  • For partial booking failures, if a flight books but the hotel call fails, the itinerary item for the hotel is left in PENDING status and a retry job is enqueued. The user is notified of the partial failure and can manually retry or substitute.
  • For cancellation propagation, cancelling a flight that is the only way to reach a hotel destination triggers a warning that downstream hotel and activity items may need to be cancelled too. The system does not auto-cancel dependents; it surfaces a checklist.
  • For collaborative edit conflicts, two editors moving the same item simultaneously use optimistic locking via an updated_at version check. The second writer receives a conflict error and must reload before saving.
  • For time-zone edge cases, all stored timestamps are UTC. Display conversion happens client-side using the item location timezone, preventing DST ambiguity in stored data.

Scalability Considerations

  • The read-heavy profile matters: itinerary reads (viewing the timeline) vastly outnumber writes. Cache the assembled timeline in Redis, keyed by itinerary_id:version, and invalidate on any item write.
  • For document storage, tickets and vouchers are stored in object storage (S3-compatible) with signed URLs; the database holds only the metadata reference.
  • For event-driven updates, external booking status changes (flight delay, hotel confirmation) arrive as webhook events and are processed by a consumer that updates the relevant itinerary_items row and pushes a real-time notification via WebSocket or push notification.
  • For sharing at scale, public share links generate a read-only token stored alongside the itinerary. Shared views are served from a CDN-cached snapshot refreshed on write, eliminating database load for viral itinerary shares.
  • For search, users search their past and upcoming itineraries by destination or date range. Index itineraries in Elasticsearch with denormalized location and date fields to support fast full-text and range queries without scanning the main database.

See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack →
Scroll to Top