Skip to article

Checkout is the midpoint: designing the travel order lifecycle

A booking is not a receipt. It is a living order that must survive rechecks, confirmations, schedule changes, cancellations and money moving on different clocks.

Travel is the only thing you will ever sell that keeps changing after you have sold it. Treat “order confirmed” as the end of the transaction and you have designed your support queue before your product.

Ecommerce earns that ending honestly: the warehouse takes over and the shoes stay the shoes. Travel sells a promise about the future, assembled from systems that carry on moving without asking you. A fare expires. A hotel swaps a room. A flight moves three hours. A traveller cancels one component and keeps the rest. A refund settles days after the decision.

So the central object cannot be a receipt. It has to be a living order: one durable record of what was offered, what was accepted, what each supplier actually confirmed, what changed afterwards and what money followed. Not a row with a status column.

The lifecycle in six moments

OnArrival’s platform page frames travel as discover, transact and service. That is the right shape for a deck and too coarse to build against. Six moments is the granularity you can implement.

MomentCore questionEvidence to retain
SearchWhat could be bought?Query, traveller context, ranked offers
SelectionWhat did the traveller choose?Offer, conditions, expiry, ancillaries
RecheckIs that promise still valid?Current price and availability
ConfirmationWhat actually committed?Payment and supplier references
ServiceWhat changed after sale?Requests, quotes, approvals and events
SettlementWhere did the money finish?Capture, payout, adjustment and refund entries

Not every search deserves an order. But the moment an offer is selected, open a transaction context: what you fail to write down there is evidence you will want back in three weeks.

Drawn as a state machine, those six moments collapse into five durable states, and the shape makes the argument on its own: checkout is a transition in the middle of the graph, not the exit. Two loops do most of the work. An expired offer falls back to intent and re-shops. Every supplier event or traveller request reopens the order into servicing, which commits a new order version and emits ledger entries, as many times as the trip demands.

schedule change · cancellation · change request select recheck · accept · pay event reopens commit emits entries INTENT OFFER ORDER SERVICING SETTLEMENT search + context priced · expiring versioned record quote then commit ledger entries offer expired · re-shop commit → order v(n+1) runs 0..n times per order refunds settle days later CHECKOUT COMMITS STATE 3 · SERVICING AND SETTLEMENT RUN FOR THE LIFE OF THE TRIP
The order state machine. Checkout commits state three of five; the servicing loop can run many times before settlement closes.

Offer state is not order state

An offer is a purchasable possibility: time-bound, conditional, unfulfilled. An order is a commitment and its entire history. Storing them in one table is the most common structural mistake in travel software, and it is expensive because the bugs it produces do not look related.

They are. A search response gets treated as a booking quote long after it expired. A front end posts price fields back, so anyone with a browser console can book at yesterday’s fare. Support cannot tell which cancellation policy the traveller accepted, so the company refunds by vibes. Three tickets, one root cause.

A clean handoff looks like this:

  1. Search returns typed offers with an identifier and validity context.
  2. Selection attaches chosen ancillaries and traveller information.
  3. Recheck obtains the current supplier-backed price and terms.
  4. The customer accepts that rechecked total.
  5. Confirmation writes payment and supplier outcomes to the order.

The accepted snapshot stays inspectable after the live product moves on. It is what you argue from when traveller and supplier remember the terms differently.

Confirmation needs more than success and failure

The single worst boolean in travel is success. Supplier calls time out after the booking already exists on their side. Payment authorises and then inventory fails. In a bundle, one component confirms while another is unresolved. A two-valued status buckets all three wrongly, and the bucket decides what your retry logic does next.

Use explicit states:

  • pending: work has not completed.
  • confirmed: the external commitment is known.
  • failed: the external system definitively rejected it.
  • unknown: the request may have committed; reconcile before retry.
  • compensating: a confirmed component is being reversed after a wider failure.

Name them however you like. What you cannot do is delete the uncertainty. unknown is uncomfortable to render and honest to hold; collapsing it into failed and retrying is how a traveller ends up with two tickets on one flight, and you pay for one of them.

Every mutation needs an idempotency key and an attempt record persisted before the call goes out. Repeat the same confirmation request and you get the existing result or a pending reconciliation. Never a second trip.

The commit itself, across suppliers with no shared transaction coordinator, is a saga with ordered steps and compensating actions. The travel cart essay walks that sequence, including what happens when the hotel fails after the flight ticketed. The lifecycle inherits whatever the commit leaves behind, which is why compensating is in the list above.

One trip can contain several fulfilments

A bundle carries a flight order, a hotel reservation, an experience voucher and an insurance policy: four references, four lifecycles, one trip as far as the traveller is concerned. Flatten that into a single booking record and you will spend the next year writing special cases. Model the hierarchy instead:

  • Trip: the customer-facing journey and shared traveller context.
  • Order: the commercial agreement and overall totals.
  • Item: a flight, room, activity, transfer or cover product.
  • Fulfilment: the supplier-specific ticket, reservation, voucher or policy.
  • Ledger entry: the money movement attached to an order or item.

Now one hotel room can cancel without pretending the flight disappeared, and item-level changes roll up into a trip status a human can read out loud.

“Partially confirmed” is not an edge case to hide. It is a state to design, communicate and resolve.

Events are the order’s memory

The current snapshot answers “what is true now?” Only the event history answers “how did we get here?”, and in travel that is the question that arrives by phone at 2am.

Emit business facts, not worker activity. Nobody outside your team benefits from job.retried:

  • offer.rechecked
  • payment.authorized
  • booking.confirmed
  • schedule.changed
  • change.quoted
  • refund.requested
  • refund.settled

Each event carries an order ID, event ID, version or sequence, occurrence time and enough context to act on without calling you back. Assume retries. Consumers deduplicate by event ID and tolerate a later event arriving first, because a schedule change can land before the confirmation it modifies.

And webhooks are delivery, not storage. If an order’s history exists only in a queue that dropped a message at 3am, that history is gone. Expose the order and its event stream so a consumer can repair itself.

Servicing is a quote-and-commit product

Changes and refunds are checkout again, with worse economics and less patience on the phone. Same two beats: calculate the action and its exact financial consequence, then let a traveller, a policy or an agent accept it.

A voluntary flight change quotes fare difference, penalty and the conditions the traveller ends up with. A hotel cancellation quotes the refundable amount at that moment, which is not what it was yesterday. So a service quote is a first-class object with its own identity, breakdown and expiry:

{
  "service_quote_id": "sq_4e19b2",
  "order_id": "ord_82f4c7",
  "item_id": "itm_flight_outbound",
  "action": "date_change",
  "expires_at": "2026-08-03T14:32:00Z",
  "breakdown": {
    "fare_difference": { "amount": 2100, "currency": "INR" },
    "change_penalty": { "amount": 1500, "currency": "INR" },
    "total_due_now": { "amount": 3600, "currency": "INR" }
  },
  "new_conditions": { "refundable": false, "changeable": true }
}

The commit references service_quote_id and nothing else. Never totals reassembled by a client. If the quote expired, the server re-quotes rather than trust arithmetic done against stale state, and the traveller sees the new number before anyone is charged.

That separation buys four things:

  • The customer sees the consequence before authorising it.
  • Policy can approve or block the action.
  • Retries reference one service intent.
  • The accepted terms stay attached to the event history.

One request shape does not imply one supplier mechanic, and pretending otherwise is how servicing lands on an operations team. OnArrival’s Flights page describes source-native servicing across 517+ carriers: NDC order changes, GDS reissues and LCC flows, translated into one normalised result. An NDC date change is an order amendment, the GDS equivalent is a reissue with an exchange calculation, a low-cost carrier may require cancel-and-rebook. Three machines, one quote. The order layer owns that translation.

Keep the money lifecycle adjacent

A booking status without a financial state is half a status. “Cancelled” does not say whether payment was voided, a refund requested, a penalty applied, or any money reached the customer. Four different conversations, and support is having all of them.

Represent the financial transitions explicitly:

Travel eventPossible financial event
Offer acceptedPayment authorised
Booking confirmedPayment captured
Item cancelledRefund calculated
Supplier reversesPayout adjusted
Refund completedRefund settled

The order total is a view over ledger entries, never a field you overwrite. Overwrite it once and the record of what the trip cost before the change is gone. Keep the entries and finance gets an audit trail, and the product gets to say something better than “refund processing.” One booking, many money movements follows that lineage through collection, settlement and refund.

Product states deserve product language

Operational state is not customer copy. Translate it into a precise promise:

  • “We are confirming your flight” instead of an endless spinner.
  • “Your room is confirmed; your flight is still resolving” for a partial bundle.
  • “The airline changed your departure” with available actions.
  • “Your refund was sent to the original payment method” when settlement occurs.

Never show “confirmed” because payment succeeded, or “refunded” because a support ticket exists. Those two lies cost more than an outage.

Lifecycle readiness checklist

Before shipping, verify that your order model can:

  • Reconstruct the accepted offer and its terms.
  • Distinguish definite failure from unknown outcome.
  • Retry every mutation without duplicate effects.
  • Hold multiple items and supplier fulfilments.
  • Quote a change or refund before committing it.
  • Publish deduplicable events and expose their history.
  • Connect each service action to ledger entries.
  • Explain partial states in traveller language.
  • Give a human operator a safe next action.

Search wins the click. The lifecycle wins the second trip. Build the order assuming the itinerary will change, because it will, and the only real question is whether your system finds out before the traveller does. Then test what you show them against the post-booking trust checklist.