Payment + Booking Distributed Transaction
Model payment and supplier booking as a distributed transaction using authorization, capture, compensation, UNKNOWN outcomes and reconciliation.
A payment gateway and a supplier booking API do not participate in one ACID transaction. Travel systems therefore need to model booking + payment as a distributed transaction, with success, failure and unknown outcomes handled independently for every external step.
The core problem
Two outcomes need to line up:
- the traveler payment obligation,
- the supplier reservation.
They happen in different systems, so intermediate divergence is normal:
- payment authorized, booking failed,
- booking confirmed, capture failed,
- payment timeout with unknown outcome,
- booking timeout with unknown outcome,
- cancellation succeeded but refund failed,
- refund succeeded while local booking state stayed stale.
There is no single transaction
Payment and booking distributed transaction
Why authorize → book → capture?
In many models, authorizing first and capturing only after booking confirmation reduces risk:
- the traveler is not fully charged when no booking exists,
- a failed booking can be followed by an authorization void.
This is not universal. Some suppliers or commercial models require full capture, pay-at-property, or virtual-card settlement.
Other sequencing models
Book → pay
Create the reservation first, then collect payment.
Risk: a payment failure may require cancelling an already-confirmed booking.
Pay/capture → book
Finalize payment before the reservation.
Risk: if booking fails, the system must compensate with a refund or void.
Pay at property
The platform may not orchestrate the payment, but booking state and payment liability still need separate modeling.
Canonical transaction state
Do not use one transaction_status. Keep separate state machines:
PaymentState:
CREATED -> AUTHORIZING -> AUTHORIZED -> CAPTURING -> CAPTURED
-> FAILED
-> UNKNOWN
-> VOIDED
-> REFUNDED
BookingState:
REQUESTED -> CONFIRMED
-> FAILED
-> UNKNOWN
-> CANCELLEDAn orchestration state can sit above them:
BookingTransaction:
STARTED
PAYMENT_READY
BOOKING_PENDING
BOOKING_CONFIRMED
PAYMENT_FINALIZING
COMPLETED
RECOVERY_REQUIRED
COMPENSATING
FAILEDSaga pattern
Distributed work cannot be rolled back like one database transaction, so use compensation.
Travel booking compensation saga
Compensation is not a true rollback. Supplier cancellation fees or payment costs may already exist.
UNKNOWN outcomes can happen on both sides
Booking UNKNOWN
The supplier request times out. Do not blindly repeat create; perform lookup and reconciliation.
Payment UNKNOWN
The gateway times out. Starting a new payment transaction can double-charge the traveler. Use the provider's idempotency or status-lookup mechanism.
Idempotency boundaries
Use a distinct key per semantic operation:
- booking create idempotency key,
- payment authorization key,
- capture key,
- cancellation key,
- refund key.
Do not reuse one key across different operations.
Transaction log
The orchestrator should retain immutable attempt history as well as current state.
{
"bookingIntentId": "bi_123",
"operation": "BOOK",
"attempt": 1,
"idempotencyKey": "book_bi_123_v1",
"status": "UNKNOWN",
"providerReference": null,
"startedAt": "2026-09-27T10:00:00Z"
}Recovery policy
| Situation | First action |
|---|---|
| Payment authorization UNKNOWN | provider lookup / idempotent status query |
| Booking create UNKNOWN | booking lookup / reconciliation |
| Booking FAILED + payment AUTHORIZED | void authorization |
| Booking CONFIRMED + capture FAILED | capture recovery; cancel if policy requires |
| Cancellation CONFIRMED + refund FAILED | retry/reconcile refund |
| Refund UNKNOWN | payment lookup; no blind retry |
Outbox for local consistency
A supplier may confirm the booking while local event publication fails. Use a transactional outbox:
local transaction:
update booking state
insert outbox event
commit
background publisher:
publish outbox event
mark publishedThis does not make the external booking call ACID. It only improves consistency between local state and event publication.
Retry policy
Retry should not happen simply because an error looks technical.
A safe retry requires:
- idempotent semantics or provider idempotency support,
- known outcome of the previous attempt,
- exponential backoff with jitter,
- bounded retry budget,
- rate-limit awareness.
Booking create without idempotency is one of the most dangerous operations to retry.
Reconciliation worker
Continuously inspect records such as:
- long-lived UNKNOWN booking,
- AUTHORIZED payment with no terminal booking,
- CONFIRMED booking with incomplete payment,
- CANCELLED booking with incomplete refund,
- local/provider state mismatch.
Fetch authoritative provider evidence and repair canonical state.
Manual operations queue
Some divergence needs human review. An operations queue should show:
- traveler-safe booking summary,
- provider reference,
- payment reference,
- current booking/payment states,
- attempt history,
- suggested next action,
- risk indicators such as duplicate charge, duplicate booking or cancellation fee.
Failure modes
- booking confirmed while capture fails,
- payment captured while booking is FAILED/UNKNOWN,
- compensation itself timing out,
- duplicate capture/refund retries,
- local outbox/state diverging from provider state,
- manual operations creating a second booking or refund.
Observability
Track:
- authorization success rate,
- booking confirmation rate,
- capture success rate,
- compensation rate,
- void/refund latency,
- booking UNKNOWN rate,
- payment UNKNOWN rate,
- booking-payment divergence count,
- reconciliation age,
- manual-intervention rate,
- duplicate-prevention hits.
Production checklist
- separate payment and booking state machines,
- choose sequencing explicitly per provider/commercial contract,
- use idempotency keys for every write operation,
- model UNKNOWN as a first-class state,
- define compensation policy,
- provide reconciliation workers and manual operations queues,
- use a transactional outbox for local state/event consistency.
Design principle
Payment and Booking are separate state machines; orchestration coordinates them.
Keeping intent, attempt, evidence and compensation explicit makes timeouts and partial failures manageable instead of hiding them inside one status field.
Planning a similar integration?
We can review requirements, feed/API design and the production approach with you.