Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/microservices-patterns/ftgo-application/llms.txt

Use this file to discover all available pages before exploring further.

This page explains how the FTGO application implements the Saga pattern to coordinate multi-step business transactions that span multiple microservices. Without a distributed transaction coordinator, each step is a local database transaction; the saga orchestrator sequences them and triggers compensating transactions if any step fails.

Why Sagas Instead of 2PC

Two-phase commit (2PC) requires all participating services to hold locks until the coordinator commits. In a microservices architecture this creates tight coupling, blocks availability, and is incompatible with NoSQL stores. Sagas replace a single distributed ACID transaction with a sequence of local transactions, each publishing a message or event that triggers the next step.
FTGO uses orchestration-based sagas via the Eventuate Tram Sagas library. A central orchestrator (the saga class) sends commands to participants over messaging channels and reacts to their replies.

The Three Sagas in FTGO

SagaTriggerParticipating Services
CreateOrderSagaNew order placedOrder, Consumer, Kitchen, Accounting
CancelOrderSagaOrder cancellation requestedOrder, Kitchen, Accounting
ReviseOrderSagaOrder revision requestedOrder, Kitchen, Accounting

CreateOrderSaga

CreateOrderSaga is the most detailed saga in FTGO. It validates the consumer, reserves a kitchen ticket, authorizes payment, then confirms both the ticket and the order.

Saga Definition

// ftgo-order-service/.../sagas/createorder/CreateOrderSaga.java
public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaState> {

  public CreateOrderSaga(OrderServiceProxy orderService,
                         ConsumerServiceProxy consumerService,
                         KitchenServiceProxy kitchenService,
                         AccountingServiceProxy accountingService) {
    this.sagaDefinition =
         step()
           .withCompensation(orderService.reject,
               CreateOrderSagaState::makeRejectOrderCommand)
         .step()
           .invokeParticipant(consumerService.validateOrder,
               CreateOrderSagaState::makeValidateOrderByConsumerCommand)
         .step()
           .invokeParticipant(kitchenService.create,
               CreateOrderSagaState::makeCreateTicketCommand)
           .onReply(CreateTicketReply.class,
               CreateOrderSagaState::handleCreateTicketReply)
           .withCompensation(kitchenService.cancel,
               CreateOrderSagaState::makeCancelCreateTicketCommand)
         .step()
           .invokeParticipant(accountingService.authorize,
               CreateOrderSagaState::makeAuthorizeCommand)
         .step()
           .invokeParticipant(kitchenService.confirmCreate,
               CreateOrderSagaState::makeConfirmCreateTicketCommand)
         .step()
           .invokeParticipant(orderService.approve,
               CreateOrderSagaState::makeApproveOrderCommand)
         .build();
  }
}

Saga Flow

1

Reject Order (compensation only)

The first step has no forward action — it registers a compensation command so that if any later step fails, the Order Service receives a RejectOrderCommand and marks the order REJECTED.
// CreateOrderSagaState.java
RejectOrderCommand makeRejectOrderCommand() {
  return new RejectOrderCommand(getOrderId());
}
2

Validate Consumer

The Consumer Service checks whether the consumer is allowed to place orders of this total value. The orchestrator sends a ValidateOrderByConsumer command to the consumerService channel.
ValidateOrderByConsumer makeValidateOrderByConsumerCommand() {
  ValidateOrderByConsumer x = new ValidateOrderByConsumer();
  x.setConsumerId(getOrderDetails().getConsumerId());
  x.setOrderId(getOrderId());
  x.setOrderTotal(getOrderDetails().getOrderTotal().asString());
  return x;
}
3

Create Kitchen Ticket

A CreateTicket command is sent to Kitchen Service. The reply carries the new ticketId, which the saga state stores for later compensation.
CreateTicket makeCreateTicketCommand() {
  return new CreateTicket(
      getOrderDetails().getRestaurantId(),
      getOrderId(),
      makeTicketDetails(getOrderDetails()));
}

void handleCreateTicketReply(CreateTicketReply reply) {
  setTicketId(reply.getTicketId());
}

// Compensation — sent if a later step fails
CancelCreateTicket makeCancelCreateTicketCommand() {
  return new CancelCreateTicket(getOrderId());
}
4

Authorize Payment

An AuthorizeCommand is sent to Accounting Service. This step has no compensation: if authorization fails the saga rolls back the kitchen ticket via the compensation registered in step 3.
AuthorizeCommand makeAuthorizeCommand() {
  return new AuthorizeCommand()
      .withConsumerId(getOrderDetails().getConsumerId())
      .withOrderId(getOrderId())
      .withOrderTotal(getOrderDetails().getOrderTotal().asString());
}
5

Confirm Kitchen Ticket

Once payment is authorized, Kitchen Service is told to confirm the ticket using the ticketId stored from step 3.
ConfirmCreateTicket makeConfirmCreateTicketCommand() {
  return new ConfirmCreateTicket(getTicketId());
}
6

Approve Order

The final step transitions the order from APPROVAL_PENDING to APPROVED.
ApproveOrderCommand makeApproveOrderCommand() {
  return new ApproveOrderCommand(getOrderId());
}

CancelOrderSaga

CancelOrderSaga coordinates cancellation across the Order, Kitchen, and Accounting services. Steps 1 and 2 each have compensations to undo a partial cancel if the saga fails mid-way.
// ftgo-order-service/.../sagas/cancelorder/CancelOrderSaga.java
sagaDefinition = step()
    .invokeParticipant(this::beginCancel)
    .withCompensation(this::undoBeginCancel)
    .step()
    .invokeParticipant(this::beginCancelTicket)
    .withCompensation(this::undoBeginCancelTicket)
    .step()
    .invokeParticipant(this::reverseAuthorization)
    .step()
    .invokeParticipant(this::confirmTicketCancel)
    .step()
    .invokeParticipant(this::confirmOrderCancel)
    .build();
Each step sends a command to a named channel built with CommandWithDestinationBuilder.send(...):
private CommandWithDestination reverseAuthorization(CancelOrderSagaData data) {
  return send(new ReverseAuthorizationCommand(
                  data.getConsumerId(),
                  data.getOrderId(),
                  data.getOrderTotal()))
      .to(AccountingServiceChannels.accountingServiceChannel)
      .build();
}

ReviseOrderSaga

ReviseOrderSaga follows the same five-step structure. The first step replies with the revised order total so the saga can pass an accurate amount to Accounting Service in the authorization revision step.
// ftgo-order-service/.../sagas/reviseorder/ReviseOrderSaga.java
sagaDefinition = step()
    .invokeParticipant(this::beginReviseOrder)
    .onReply(BeginReviseOrderReply.class, this::handleBeginReviseOrderReply)
    .withCompensation(this::undoBeginReviseOrder)
    .step()
    .invokeParticipant(this::beginReviseTicket)
    .withCompensation(this::undoBeginReviseTicket)
    .step()
    .invokeParticipant(this::reviseAuthorization)
    .step()
    .invokeParticipant(this::confirmTicketRevision)
    .step()
    .invokeParticipant(this::confirmOrderRevision)
    .build();
The reply handler updates the saga state before the authorization step runs:
private void handleBeginReviseOrderReply(ReviseOrderSagaData data,
                                         BeginReviseOrderReply reply) {
  data.setRevisedOrderTotal(reply.getRevisedOrderTotal());
}

Command Handlers in Participating Services

Each participating service exposes a commandHandlers() method that maps incoming command message types to handler methods.

Consumer Service

// ConsumerServiceCommandHandlers.java
public CommandHandlers commandHandlers() {
  return SagaCommandHandlersBuilder
      .fromChannel("consumerService")
      .onMessage(ValidateOrderByConsumer.class, this::validateOrderForConsumer)
      .build();
}

private Message validateOrderForConsumer(
    CommandMessage<ValidateOrderByConsumer> cm) {
  try {
    consumerService.validateOrderForConsumer(
        cm.getCommand().getConsumerId(),
        cm.getCommand().getOrderTotal());
    return withSuccess();
  } catch (ConsumerVerificationFailedException e) {
    return withFailure();
  }
}

Kitchen Service

// KitchenServiceCommandHandler.java
public CommandHandlers commandHandlers() {
  return SagaCommandHandlersBuilder
      .fromChannel(KitchenServiceChannels.COMMAND_CHANNEL)
      .onMessage(CreateTicket.class,          this::createTicket)
      .onMessage(ConfirmCreateTicket.class,   this::confirmCreateTicket)
      .onMessage(CancelCreateTicket.class,    this::cancelCreateTicket)
      .onMessage(BeginCancelTicketCommand.class,   this::beginCancelTicket)
      .onMessage(ConfirmCancelTicketCommand.class, this::confirmCancelTicket)
      .onMessage(UndoBeginCancelTicketCommand.class, this::undoBeginCancelTicket)
      .onMessage(BeginReviseTicketCommand.class,   this::beginReviseTicket)
      .onMessage(UndoBeginReviseTicketCommand.class, this::undoBeginReviseTicket)
      .onMessage(ConfirmReviseTicketCommand.class, this::confirmReviseTicket)
      .build();
}

Accounting Service

// AccountingServiceCommandHandler.java
public CommandHandlers commandHandlers() {
  return SagaCommandHandlersBuilder
      .fromChannel("accountingService")
      .onMessage(AuthorizeCommand.class,        this::authorize)
      .onMessage(ReverseAuthorizationCommand.class, this::reverseAuthorization)
      .onMessage(ReviseAuthorization.class,     this::reviseAuthorization)
      .build();
}
If the Accounting Service finds the account disabled it replies with AccountDisabledReply, which the saga treats as a failure and triggers compensation for all preceding steps.

Compensation Summary

SagaStepCompensation
CreateOrderSagaOrder (step 1)RejectOrderCommand
CreateOrderSagaKitchen ticket (step 3)CancelCreateTicket
CancelOrderSagaBegin cancel (step 1)UndoBeginCancelCommand
CancelOrderSagaBegin cancel ticket (step 2)UndoBeginCancelTicketCommand
ReviseOrderSagaBegin revise order (step 1)UndoBeginReviseOrderCommand
ReviseOrderSagaBegin revise ticket (step 2)UndoBeginReviseTicketCommand
Compensation transactions run in reverse order when a step fails. Steps that do not have a compensation (e.g., authorizePayment) are considered pivot transactions — once they succeed, the saga is committed forward.

Build docs developers (and LLMs) love