This page covers how the FTGO application implements the event sourcing pattern in the Accounting Service. Instead of persisting the current state of anDocumentation 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.
Account record in a relational table, every state change is stored as an immutable domain event. The current account state is reconstructed by replaying those events.
What is Event Sourcing
In a conventional application anUPDATE statement overwrites the previous state of a row. Event sourcing replaces that mutable row with an append-only sequence of events. The current state is derived by folding all past events:
FTGO uses the Eventuate Client framework for event sourcing. Aggregates extend
ReflectiveMutableCommandProcessingAggregate, which routes command messages to process() methods and state-change events to apply() methods.The Account Aggregate
Account is the event-sourced aggregate in the Accounting Service. It extends ReflectiveMutableCommandProcessingAggregate<Account, AccountCommand>, meaning the framework automatically dispatches:
- Incoming commands to the matching
process(SomeCommand)method, which returns a list of events to persist. - Persisted events back to the matching
apply(SomeEvent)method, which mutates the in-memory aggregate state.
Events
| Event | Produced by | Meaning |
|---|---|---|
AccountCreatedEvent | process(CreateAccountCommand) | A new consumer account was opened |
AccountAuthorizedEvent | process(AuthorizeCommandInternal) | A payment was successfully authorized |
ReverseAuthorizationCommandInternal and ReviseAuthorizationCommandInternal currently return an empty list, meaning they have side effects tracked elsewhere but do not produce new domain events in this implementation.
Creating an Account
AccountingService uses AggregateRepository.save() to persist the initial CreateAccountCommand. The framework calls process(CreateAccountCommand), persists the returned AccountCreatedEvent, then calls apply(AccountCreatedEvent) to hydrate the new aggregate instance.
ConsumerCreated event is observed by AccountingEventConsumer:
aggregateId passed to save() matches the consumer’s ID, so the account and consumer share the same identifier.
Processing Saga Commands
AccountingServiceCommandHandler handles commands that arrive from the saga orchestrator. Each handler calls accountRepository.update(), which loads the aggregate (by replaying its events), calls the appropriate process() method, persists any new events, and sends a reply back to the saga.
replyingTo(cm) option tells Eventuate to send a success or failure reply back to the saga reply channel once the event has been durably persisted.
How Eventuate Client Works
Command arrives on the messaging channel
The saga orchestrator sends an
AuthorizeCommand (or similar) to the accountingService Kafka topic.Command handler loads the aggregate
AggregateRepository.update() queries the Eventuate event store for all events with the given aggregate ID and replays them to reconstruct the current Account state.process() returns new events
The framework calls
account.process(AuthorizeCommandInternal), which returns [AccountAuthorizedEvent].Events are persisted atomically
The new events are appended to the event store in a single transaction. Because the event store is a database, this write is atomic and forms the basis of transactional messaging.
apply() updates in-memory state
account.apply(AccountAuthorizedEvent) is called to bring the in-memory aggregate up to date.Event Store vs Relational Store
How does this differ from a conventional JPA entity?
How does this differ from a conventional JPA entity?
A conventional JPA entity would have an
Each row is immutable. Reconstructing the account always starts from the first event and applies each in sequence. This guarantees a complete and auditable history of every authorization, reversal, and revision.
accounts table with columns for balance, status, etc. An UPDATE would overwrite the previous row, and the history would be lost.With Eventuate event sourcing, the events table stores rows like:| aggregate_type | aggregate_id | event_type | event_data |
|---|---|---|---|
Account | 42 | AccountCreatedEvent | {} |
Account | 42 | AccountAuthorizedEvent | {"orderId":"101","amount":"25.00"} |