Canonical Offer, Order and Booking State Model
Design canonical Offer, Order and Booking lifecycle boundaries with an explicit booking state machine for travel integrations.
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
Canonical offer state machine
Offer state is not booking state. An expired offer does not cancel a booking that has already been created.
Canonical Order / BookingIntent state
CREATED
-> VALIDATING
-> READY
-> SUBMITTING
-> SUBMITTED
-> COMPLETED | FAILED | UNKNOWNBookingIntent 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 machine
Why UNKNOWN is first class
A timeout does not mean:
request timed out == booking failedIt means:
request timed out == the local system does not know the outcomeThe 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:
provider status -> canonical state -> confidence/evidenceExample:
| 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.
{
"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.
Booking
-> ModificationAttempt
-> provider modify
-> CONFIRMED / FAILED / UNKNOWN
-> reconciliationUse 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:
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?"
Let’s review your architecture.
We can assess your travel distribution and metasearch architecture for scalability, failure modes and operations.