Saga Pattern in Flight, Hotel, Payment and Procurement Workflows

How I apply Saga orchestration to real multi-step business workflows such as flight/hotel booking, payment, RFx and procurement where one ACID transaction cannot span all services and providers.

Romharshan Singh
Romharshan SinghSenior Solution Architect • AI & Cloud Mentor
1 September 20265 min read0 viewsUpdated 1 Sept 2026
Saga Pattern in Flight, Hotel, Payment and Procurement Workflows

Saga Pattern in Flight, Hotel, Payment and Procurement Workflows

Why I needed Saga thinking

Travel booking is one of the easiest ways to understand distributed transactions.

A booking may involve:

plaintext
create itinerary
   |
reserve flight
   |
reserve hotel
   |
capture payment
   |
confirm booking
   |
notify traveller

The airline, hotel supplier and payment provider do not share one database transaction.

In procurement/RFx flows, the sequence is different but the problem is similar:

plaintext
create RFx
 -> publish
 -> invite vendors
 -> receive bids
 -> evaluate
 -> award

Each step may commit independently and may take minutes, hours or days.

I use the Saga pattern to make state, compensation and recovery explicit.

Why normal rollback does not work

Suppose:

plaintext
Flight reserved   YES
Hotel reserved    YES
Payment captured  NO

There is no ROLLBACK statement that undoes external supplier reservations.

We need business compensations:

plaintext
payment failed
 -> cancel hotel reservation
 -> cancel flight reservation
 -> mark booking FAILED

Compensation is not a database rollback. It is a new business operation and it can fail too.

Orchestrated Saga

For a complex booking flow I prefer orchestration because the current state and next action are explicit.

typescript
async execute(command: CreateBookingCommand) {
  const saga = await this.sagaRepo.start(command.requestId);

  try {
    const flight = await this.flight.reserve(command.flight);
    await this.sagaRepo.record(
      saga.id,
      'FLIGHT_RESERVED',
      flight.id,
    );

    const hotel = await this.hotel.reserve(command.hotel);
    await this.sagaRepo.record(
      saga.id,
      'HOTEL_RESERVED',
      hotel.id,
    );

    const payment = await this.payment.capture(command.payment);
    await this.sagaRepo.record(
      saga.id,
      'PAYMENT_CAPTURED',
      payment.id,
    );

    await this.booking.confirm(command.bookingId);
    await this.sagaRepo.complete(saga.id);

    return { status: 'CONFIRMED' };
  } catch (error) {
    await this.compensate(saga.id);
    throw error;
  }
}

The critical detail is persisted progress. If the orchestrator process dies after the hotel reservation, another instance must recover from stored state.

Compensation

typescript
async compensate(sagaId: string) {
  const state = await this.sagaRepo.get(sagaId);

  if (state.paymentCaptured && !state.paymentRefunded) {
    await this.payment.refund(
      state.paymentId,
      sagaId,
    );
  }

  if (state.hotelReserved && !state.hotelCancelled) {
    await this.hotel.cancel(
      state.hotelReservationId,
      sagaId,
    );
  }

  if (state.flightReserved && !state.flightCancelled) {
    await this.flight.cancel(
      state.flightReservationId,
      sagaId,
    );
  }

  await this.sagaRepo.markCompensated(sagaId);
}

Every compensating action needs idempotency. The orchestrator may retry after a timeout without knowing whether the remote system already completed the cancellation.

Uncertain external result

This is one of the hardest real-world cases:

plaintext
capture payment request sent
          |
network timeout
          |
Did provider capture or not?

Retrying immediately can double-charge.

I introduce an intermediate state:

plaintext
PAYMENT_STATUS_UNKNOWN

Then perform reconciliation:

plaintext
query provider by merchant reference
          |
     captured? yes/no
          |
continue or compensate

Distributed systems contain uncertainty. A good Saga model represents uncertainty instead of forcing every outcome into success/failure too early.

Choreography

For simple flows choreography can work:

plaintext
BookingCreated
    |
Flight service reserves
    |
FlightReserved
    |
Payment service captures
    |
PaymentCaptured

The risk is that after many services participate, nobody can easily answer:

What is the complete workflow state for booking B1001?

For complex travel/procurement flows I normally prefer orchestration for the core business process and events for secondary reactions.

Saga + Outbox

When a Saga state transition must emit an event, I combine it with outbox:

plaintext
DB transaction
  |- update saga state
  `- insert outbox event

outbox publisher
  |
Kafka

This prevents a committed Saga state from silently failing to publish its next event.

Timeouts

A Saga needs business deadlines, not just HTTP timeouts.

Examples:

plaintext
flight hold expires in 15 minutes
payment authorization expires
vendor bid window closes at 17:00
manual approval must finish in 24 hours

A scheduler or delayed event can transition timed-out Sagas into compensation or manual review.

Production states

I prefer explicit states such as:

plaintext
STARTED
FLIGHT_RESERVED
HOTEL_RESERVED
PAYMENT_PENDING
PAYMENT_STATUS_UNKNOWN
CONFIRMED
COMPENSATING
COMPENSATION_FAILED
FAILED
MANUAL_REVIEW

These states are operationally valuable. Support teams can see what actually happened.

Failure handling

Compensation itself fails

Persist the failure, retry safely and alert operations. A Saga without manual recovery is incomplete.

Duplicate command

Use a stable Saga/business request ID.

Supplier sends late callback

Validate callback against the current Saga state and make state transitions conditional.

Process restarts halfway

Recover from persisted Saga state. Never rely only on in-memory workflow state.

Production checklist

  • Persist every meaningful Saga state.
  • Give each Saga a stable operation ID.
  • Make steps and compensations idempotent.
  • Model uncertain outcomes.
  • Define business deadlines.
  • Use reconciliation for external providers.
  • Add a manual-recovery state.
  • Trace the Saga across services.
  • Combine state transitions with Outbox where required.
  • Test process crash between every two steps.

FAQ

Orchestration or choreography?

For complex workflows I prefer orchestration because progression and recovery are visible. Choreography is useful for simpler independent reactions.

Is compensation guaranteed?

No. Compensation can fail and may require retry or manual intervention.

Can a Saga last days?

Yes. Procurement and approval workflows are often long-running.

Is Saga only for microservices?

No. It is useful whenever one business workflow spans independently committed systems.

- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects) - [Idempotency in Payments and Bookings](/articles/idempotency-payments-bookings-bids-event-consumers)
  • Transactional Outbox

  • Adapter and Anti-Corruption Layer

    Architect's final take

    Saga is valuable because it admits the truth: distributed business transactions are sequences of independently committed actions. Recovery must be designed as carefully as the happy path.

Was this article useful?

Your feedback helps prioritize deeper technical content.

Romharshan Singh
ABOUT THE AUTHOR

Romharshan Singh

Senior Solution Architect and Full Stack Technology Leader with 20+ years of enterprise engineering experience across AI, cloud, distributed systems, Java, Node.js, React, Angular, Kafka and Kubernetes.