Canonical Offer, Order and Booking State Model

Design canonical Offer, Order and Booking lifecycle boundaries with an explicit booking state machine for travel integrations.

Editorial information
Advertisement

A common modeling mistake in travel integrations is mapping one provider status field directly into the internal booking state. A canonical model should instead treat Offer, Order/Intent and Booking as separate lifecycles.

Conceptual boundaries

ConceptMeaningMutation model
OfferA purchasable travel-product snapshot under specific conditionsCreate new versions
Order / BookingIntentThe user's intent to purchase the selected offerControlled transitions
BookingThe reservation created at the supplierEvolves through its lifecycle

Provider terminology varies. APIs may say order, reservation, booking, itinerary or PNR. Canonical modeling normalizes behavior, not vocabulary.

Offer state

Canonical offer state machineCanonical offer state machine

Mermaid source (.mmd)

Offer state is not booking state. An expired offer does not cancel a booking that has already been created.

Canonical Order / BookingIntent state

text
CREATED
 -> VALIDATING
 -> READY
 -> SUBMITTING
 -> SUBMITTED
 -> COMPLETED | FAILED | UNKNOWN

BookingIntent represents the user intent and the idempotency boundary. A second supplier create call for the same intent should happen only when the previous attempt is authoritatively known not to have created a booking.

Booking state machine

Canonical booking state machineCanonical booking state machine

Mermaid source (.mmd)

Why UNKNOWN is first class

A timeout does not mean:

text
request timed out == booking failed

It means:

text
request timed out == the local system does not know the outcome

The supplier may have created the booking while the response was lost. UNKNOWN should trigger reconciliation rather than an immediate create retry.

Evidence required for FAILED

Set FAILED only from authoritative evidence such as:

  • explicit supplier rejection,
  • a valid authoritative lookup proving that no booking exists,
  • deterministic validation failure before supplier submission.

A transport error by itself is not FAILED.

Keep provider mappings explicit

Map provider-specific states inside configuration or the adapter:

text
provider status -> canonical state -> confidence/evidence

Example:

Provider evidenceCanonical stateEvidence
confirmed + referenceCONFIRMEDAPI response
validation_errorFAILEDauthoritative rejection
HTTP timeoutUNKNOWNtransport ambiguity
webhook cancelledCANCELLEDasync provider event

Do not spread raw provider status-string comparisons through the business layer.

Separate state from events

Materialize current state for efficient reads while preserving the evidence separately.

json
{
  "bookingId": "bk_42",
  "state": "CONFIRMED",
  "version": 7,
  "lastEvidence": {
    "type": "PROVIDER_CONFIRMED",
    "source": "webhook",
    "providerReference": "ABC123"
  }
}

Current state may be mutable. Event history should be immutable.

Transition guards

For every transition define:

  • which sources may cause it,
  • which previous states permit it,
  • what happens if the same event arrives twice,
  • how stale events are handled,
  • whether a provider reference is required,
  • whether any payment state is a prerequisite.

Treat modification as another attempt

A booking modification should not be a blind CRUD overwrite.

text
Booking
 -> ModificationAttempt
 -> provider modify
 -> CONFIRMED / FAILED / UNKNOWN
 -> reconciliation

Use the same pattern for cancellation.

Concurrency

An API response, webhook and reconciliation worker can all update the same booking concurrently. Use optimistic concurrency or versions:

text
UPDATE booking
SET state = ?, version = version + 1
WHERE id = ? AND version = ?

If the write loses the race, reload the state and re-evaluate the event.

Useful invariants

  • A CONFIRMED booking should have a provider reference.
  • CANCELLED should come from an existing confirmed-booking lifecycle.
  • FAILED should not silently resubmit the same intent; retries require an explicit policy.
  • UNKNOWN is not terminal.
  • Payment CAPTURED does not imply Booking CONFIRMED.
  • Booking CONFIRMED does not imply Payment CAPTURED.

Failure modes

  • incorrect provider-to-canonical state mapping,
  • resolving UNKNOWN to FAILED too early,
  • stale/out-of-order events rolling back newer state,
  • parallel create attempts for one BookingIntent,
  • conflating payment state with booking state,
  • version races during modification or cancellation.

Observability

Track:

  • state-transition count,
  • invalid transitions,
  • UNKNOWN count and age,
  • duplicate events,
  • out-of-order events,
  • reconciliation resolution rate,
  • median time in each state,
  • modification/cancellation UNKNOWN rate.

Production checklist

  • keep Offer, BookingIntent and Booking state separate,
  • define terminal and non-terminal states explicitly,
  • provide reconciliation for UNKNOWN,
  • isolate provider state mapping inside adapters,
  • use optimistic concurrency/versioning,
  • preserve immutable event history,
  • monitor invalid transitions and duplicate events.

Use this state machine together with the existing Booking Lifecycle Event Model. The state model answers "where are we now?"; the event model answers "how did we get here?"

Technical advisory

Let’s review your architecture.

We can assess your travel distribution and metasearch architecture for scalability, failure modes and operations.

Discuss your project →

Related content