---
title: "Canonical Offer, Order and Booking State Model"
description: "Design canonical Offer, Order and Booking lifecycle boundaries with an explicit booking state machine for travel integrations."
slug: "canonical-offer-order-booking-state-model"
translationKey: "architecture-canonical-offer-order-booking-state-model"
locale: "en"
type: "guide"
category: "architecture"
tags: ["offer","order","booking","state-machine","canonical-model","travel"]
publishedAt: "2026-09-27"
updatedAt: "2026-09-27"
reviewedAt: "2026-09-27"
technicalVerifiedAt: "2026-09-27"
codeExampleStatus: "illustrative"
---

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

| Concept | Meaning | Mutation model |
|---|---|---|
| Offer | A purchasable travel-product snapshot under specific conditions | Create new versions |
| Order / BookingIntent | The user's intent to purchase the selected offer | Controlled transitions |
| Booking | The reservation created at the supplier | Evolves through its lifecycle |

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

## Offer state

```mermaid
%% title: Canonical offer state machine
%% description: An offer is discovered, selected, repriced, changed, expired and finally consumed by a booking attempt.
stateDiagram-v2
  [*] --> Discovered
  Discovered --> Selected
  Selected --> Repricing
  Repricing --> Repriced: still valid
  Repricing --> Changed: price/policy changed
  Repricing --> Expired: unavailable
  Changed --> Selected: user accepts new offer
  Repriced --> LockedForBooking
  LockedForBooking --> Consumed: booking attempt created
```

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

```mermaid
%% title: Canonical booking state machine
%% description: Booking transitions across requested, confirmed, unknown, failed, modification and cancellation states.
stateDiagram-v2
  [*] --> Requested
  Requested --> Confirmed: supplier confirmation
  Requested --> Failed: authoritative rejection
  Requested --> Unknown: ambiguous outcome
  Unknown --> Confirmed: reconciliation finds booking
  Unknown --> Failed: authoritative lookup finds none
  Confirmed --> ModificationPending: modify requested
  ModificationPending --> Confirmed: modification confirmed
  ModificationPending --> Unknown: modify outcome ambiguous
  Confirmed --> CancellationPending: cancel requested
  CancellationPending --> Cancelled: cancellation confirmed
  CancellationPending --> Confirmed: cancellation rejected
  CancellationPending --> Unknown: cancel outcome ambiguous
  Cancelled --> [*]
  Failed --> [*]
```

## 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 evidence | Canonical state | Evidence |
|---|---|---|
| confirmed + reference | CONFIRMED | API response |
| validation_error | FAILED | authoritative rejection |
| HTTP timeout | UNKNOWN | transport ambiguity |
| webhook cancelled | CANCELLED | async 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.

## Related model

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?"
