Why a travel cart needs more than one checkout call
Flights, hotels and activities can expire or fail independently during checkout. A travel cart needs price checks, booking recovery and a payment plan for partial success.
A travel cart can show one total while holding offers from several independent suppliers. Each offer has its own price, availability and booking conditions. Those can change while the traveller is checking out.
The flight may confirm while the hotel fails. A supplier may create the booking but time out before returning a response. The product has to handle those outcomes without charging twice or showing a whole trip as confirmed when only one part is booked.
This is a coordination problem behind a familiar checkout screen. The cart needs to track offer validity, check changes before payment and recover each booking attempt from the evidence it has.
Each offer in the cart has its own expiry
A cart should store the validity and booking conditions of each offer. Some suppliers provide an explicit expiry or hold. Others require a new availability and price check before booking. The distribution channel alone does not establish that behaviour.
These are integration cases to handle, not guarantees about a particular supplier:
| Offer condition | What the cart records | What to do before booking |
|---|---|---|
| Explicit offer expiry | Offer reference and expiry time | Check that the offer is still valid |
| Inventory held by the supplier | Hold reference and release conditions | Confirm within the supported hold window |
| No hold | Last checked price and availability | Recheck using the supplier's supported flow |
| Validity is unclear | The unresolved supplier condition | Resolve it before presenting the offer as guaranteed |
Each component needs its own record. One cart-wide countdown cannot explain several suppliers with different conditions.
Track the time to live (TTL), or remaining validity, of each offer where it is provided. Show relevant deadlines to the traveller and check price and availability again before committing the booking.
Check prices again before booking
Between "add to cart" and "pay" there is always a gap, and in that gap prices move. So revalidate every component immediately before commit: one final availability-and-price check per supplier, in parallel, on a short timeout. A cart that books against the price it fetched during search is not a cart, it is a promise you have delegated to luck.
Three outcomes come back. Each needs a policy decided before launch, not whatever the on-call engineer improvises the first time it happens.
Everything holds. Proceed. This is the common case and it should feel instant.
One leg repriced. You have exactly three moves. Surface the delta: show the new total with the changed component highlighted and require an explicit re-accept. Re-shop: fetch alternatives for the changed component and offer a swap, which is the right move when the original fare class simply sold out. Absorb: eat the difference inside a policy threshold, because interrupting a high-intent checkout over a trivial delta costs more in abandonment than the delta ever will.
The mistake is treating this as an error path. On a volatile route a reprice is a Tuesday. It is business policy, and business policy belongs in configuration where a commercial team can change it, not in a conditional a developer has to redeploy:
The following configuration is illustrative, including its amounts and timeouts; it is not an API contract.
{
"revalidation": {
"timeout_ms": 4000,
"on_reprice": {
"absorb_up_to": {
"amount": 300,
"currency": "INR",
"pct_of_component": 2.0
},
"above_threshold": "surface_delta",
"component_unavailable": "reshop_same_constraints"
},
"on_supplier_timeout": "treat_component_as_unverified"
}
}
A component died. The fare class is gone, the room type sold out. Re-shop within the traveller's original constraints, and whatever you do, do not let the rest of the cart quietly expire while they consider the replacement. Refresh the surviving holds in the background. Losing the second component while someone picks a fix for the first is the most common way a recoverable checkout becomes an abandoned one.
Revalidation does not close the race, it narrows it. The check and the booking are still separate calls and the price can still move between them: you go from a window of minutes to a window of seconds. Only the commit protocol handles those seconds.
Recover when only part of the trip is booked
Here the ecommerce analogy stops being unhelpful and turns dangerous. When the traveller pays, you must book with three or four suppliers, and there is no transaction coordinator on Earth that spans an airline's ticketing system and a bedbank's reservation database. Two-phase commit needs every participant to expose a prepare phase, hold locks through the coordinator's decision and obey an unlock protocol. Airlines and bedbanks expose none of that. You get "book" and "cancel," with fees, deadlines and moods attached. So the only correctness model available is a saga: a sequence of local transactions, each with a compensating action if a later step fails.
The sequence below illustrates one design. Booking order and payment capture must follow the capabilities and reversal conditions of the actual suppliers and payment provider:
- Authorise payment first. One authorisation for the full total. No supplier calls until the money is real. An auth is reversible; a booking may not be.
- Book the most volatile item first. Usually the flight. It is the component most likely to fail or reprice, and failing early is cheap: nothing to compensate yet. It is also often the hardest to reverse once ticketed, which is exactly why you want it committed while every other component is still safely inside a hold.
- Confirm the held items next. The bedbank hold converts to a booking. This should be near-deterministic inside the hold window, which is why it can safely wait its turn.
- Book the no-hold items. The experience, the transfer. Highest uncertainty per call, cheapest to compensate.
- Capture per confirmation. Convert slices of the auth to captures as each component confirms.
Persist a request identifier before each booking attempt. Use the supplier's idempotency mechanism where it is supported; do not assume every supplier honours the same key or retry rules. A timeout means the outcome may be unknown, because the booking may already exist. Retrieve it using the supported reference or reconcile with the supplier before retrying or reversing another component. The customer should see that confirmation is being checked, rather than an invitation to pay again.
Then a later step fails and the saga earns its keep. Say the flight ticketed and the hotel confirm failed hard: the hold lapsed mid-flow, or the bedbank rejected the conversion.
The compensation path is where policy and engineering meet, and where most teams discover they hold an opinion they never wrote down. Many tickets void free inside a window; miss the window and compensation becomes a refund flow with entirely different economics. Often the right compensation is not reversal at all. Re-shop the hotel and offer the traveller a save, because they wanted the trip, not the refund, and a rescued booking beats a clean rollback every time. Either way the orchestrator journals every step, because a crash between "flight ticketed" and "hotel failed" has to resume into compensation, not amnesia. Partial states are not exceptions to hide. They are states to design, as the order lifecycle essay argues from the servicing side.
Match payments and refunds to each booking component
The payment design must support partial booking outcomes. A single authorisation with staged captures is one approach where the payment provider supports it. If separate payments are required, the product still needs to link them to the relevant booking components.
One auth, staged captures. The traveller sees one payment. Underneath, authorise the full total once, then capture per supplier confirmation. If the saga aborts, the un-captured remainder releases automatically and only confirmed components were ever charged. Capture-per-confirmation also gives finance a clean join: every capture maps to exactly one supplier reference.
Refunds map back to components. When the hotel cancels three weeks later, the refund must trace to the hotel's captures, not to "the order." This is why the ledger lives at component level from day one: order totals are a view over component entries, never a field you overwrite. The payments and reconciliation essay follows that thread through settlement.
Currency is the quiet one that bites hardest. The airline prices in EUR, the bedbank in USD, the experience in THB, and the traveller pays in INR. Record a rate per component at commit time, because a refund a month later has to reverse at the booked rate and not today's. Then rounding. Each component rounds to the presentment currency's minor unit independently, and the sum of rounded components must equal the charged total exactly. Round the components, compute the residual against the rounded total, assign the remainder deterministically to a designated component. Skip it and your cart is one paisa off, forever, on some fraction of orders, and someone spends a week chasing that paisa across four supplier statements.
What the "just call two APIs" demo never shows
The demo always works. Two API calls, a combined total, a happy path on stage. It dies in the first production week, and by now the reasons are visible. No TTLs, so the fare on screen was stale before the traveller reached the card field. No revalidation, so the first reprice became a support ticket. Sequential booking with no compensation, so the first hotel failure stranded a ticketed flight on the company card. Per-component charging, so the first partial failure double-charged someone. No rounding rule, so finance found it before the customers did.
The order system needs to track each component’s expiry, check prices before commitment and record every booking attempt. It also needs a recovery action for partial success and a payment plan suited to the suppliers and processor. That is the work behind a single checkout button.
For a few seconds at commit, every supplier's clock, price and failure mode has to agree. None of them are trying to. Someone has to be accountable for making it happen anyway, and that someone is the order layer, not the checkout screen and never the traveller.