This page describes how the FTGO application publishes and consumes messages reliably across microservices. The core challenge is that a service must persist a state change and publish a message in the same atomic operation — if these two steps are not atomic, a crash between them leaves the system inconsistent. FTGO solves this using the transactional outbox pattern implemented by the Eventuate Tram framework.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.
The Problem: Dual Writes
A service that writes to its database and then publishes to a message broker has two independent I/O operations. A process crash or network failure between them produces a state where the database update succeeded but the message was never sent (or vice versa). Downstream services never learn about the change, or they process a phantom event.The Outbox Pattern
Instead of publishing directly to Kafka, the service writes the event into anOUTBOX table in the same local database transaction as the business entity change. A separate CDC process reads new rows from that table (via the MySQL binary log) and forwards them to Kafka. Because the outbox write and the business write share a transaction, they are atomic.
Publishing Domain Events with Eventuate Tram
The Order Service publishes events throughOrderDomainEventPublisher, a thin wrapper over the Eventuate Tram DomainEventPublisher:
Order.createOrder() returns a ResultWithDomainEvents, the service layer passes the events to this publisher, which writes them transactionally:
Domain Events Published by the Order Service
| Event | Published when |
|---|---|
OrderCreatedEvent | A new order is placed (APPROVAL_PENDING) |
OrderAuthorized | The CreateOrderSaga completes successfully |
OrderRejected | The CreateOrderSaga compensation runs |
OrderCancelled | The CancelOrderSaga completes |
Subscribing to Domain Events
Services that need to react to events implement aDomainEventHandlers bean using DomainEventHandlersBuilder.
Order History Service
Accounting Service
The Accounting Service subscribes toConsumerCreated events from the Consumer Service to create a corresponding account for each new consumer:
Saga Command Messages
The saga orchestrator sends commands usingCommandWithDestinationBuilder. These are also written to the outbox transactionally and forwarded to the target service’s Kafka topic.
Participant Command Handlers
Each participating service declares aCommandHandlers bean using SagaCommandHandlersBuilder. The framework routes incoming messages from the named channel to the correct handler method.
Full Messaging Flow
Business logic runs inside a local transaction
The service writes its entity (e.g., creates an
Order row) and Eventuate Tram appends an event or command record to the outbox table — both in the same MySQL transaction.CDC service reads the MySQL binlog
The Eventuate CDC service (deployed as a separate container) tails the MySQL binary log and reads committed outbox rows as soon as they appear.
Messages are published to Kafka
The CDC service produces each outbox row as a Kafka message to the appropriate topic (e.g.,
net.chrisrichardson.ftgo.orderservice.domain.Order for Order domain events, accountingService for accounting commands).Subscriber consumes the Kafka message
Each subscribing service’s Eventuate Tram consumer polls its Kafka topic, deserializes the event or command, and calls the registered handler method.
Infrastructure Components
| Component | Technology | Role |
|---|---|---|
| Outbox table | MySQL | Stores events/commands atomically with business data |
| CDC service | Eventuate CDC | Reads MySQL binlog, publishes to Kafka |
| Message broker | Apache Kafka | Durable, ordered delivery between services |
| Consumer offset tracking | Kafka consumer groups | At-least-once delivery; handlers must be idempotent |
Why at-least-once delivery requires idempotent handlers
Why at-least-once delivery requires idempotent handlers
The CDC service can publish the same message more than once if it crashes after publishing but before recording its position in the binlog. Handlers must therefore be idempotent.The Order History Service handles this explicitly: every DynamoDB write includes the source event’s
aggregateType, aggregateId, and eventId as a condition expression. If the same event arrives a second time, ConditionalCheckFailedException is caught and the write is skipped.