- Java Microservices Tutorial
- Java Microservices - Home
- Microservices - Introduction
- Microservices vs Monolith vs SOA
- Java Microservices - Environment Setup
- Java Microservices - Advantages of Spring Boot
- Java Microservices - Design Patterns
- Java Microservices - Domain Driven Design
- Java Microservices - Decomposition by Business Capability
- Java Microservices - Decomposition by Subdomain
- Java Microservices - Backend for Frontend
- Java Microservices - The Strangler Pattern
- Java Microservices - Synchronous Communication
- Java Microservices - Asynchronous Communication
- Java Microservices - Saga Pattern
- Java Microservices - Centralized Logging (ELK Stack)
- Java Microservices - Event Sourcing
- Java Microservices - CQRS Pattern
- Java Microservices - Sidecar Pattern
- Java Microservices - Service Mesh Pattern
- Java Microservices - Circuit Breaker Pattern
- Java Microservices - Distributed Tracing
- Java Microservices - Control Loop Pattern
- Java Microservices - Database Per Service
- Java Microservices - Bulkhead Pattern
- Java Microservices - Health Check API
- Java Microservices - Retry Pattern
- Java Microservices - Fallback Pattern
- Java Microservices Useful Resources
- Java Microservices Quick Guide
- Java Microservices Useful Resources
- Java Microservices Discussion
Java Microservices - Event Sourcing
Introduction
In traditional systems, application state is stored as the current value of entities. For example, if a user updates their address, the database reflects only the latest address.
But in microservices, sometimes it's necessary to store a full history of changes - not just the final state.
Event Sourcing is a powerful design pattern that solves this by storing changes to application state as a sequence of events. Instead of only saving the current state, you store all events that led to it.
What is Event Sourcing?
Definition
Event Sourcing is a pattern in which every change to the state of an application is captured in an event object, and those events are persisted. The current state is then rebuilt by replaying the sequence of past events.
Example
Instead of storing:
{ "accountBalance": 1000 }
You store events like:
[
{ "type": "AccountCreated", "amount": 0 },
{ "type": "DepositMade", "amount": 1000 }
]
Replaying these events leads to the current balance.
Key Concepts of Event Sourcing
| Sr.No. | Concept | Description |
|---|---|---|
| 1 | Event | Immutable fact describing what happened |
| 2 | Aggregate | Entity that applies events to rebuild state |
| 3 | Event Store | Database or broker where events are saved |
| 4 | Projection | Read model built from event stream |
| 5 | Replay | Rebuilding state by applying past events |
Benefits of Event Sourcing
| Sr.No. | Benefit | Explanation |
|---|---|---|
| 1 | Auditability | Full history of what happened and when |
| 2 | Debugging & Replayability | Reconstruct bugs by replaying events |
| 3 | Temporal Queries | View system state at any point in time |
| 4 | Decoupling | Services can react to events asynchronously |
| 5 | Event-Driven Integration | Pairs naturally with messaging patterns |
Use Cases in Microservices
| Sr.No. | Domain | Event Sourcing Use Case |
|---|---|---|
| 1 | Banking | Transactions, audit trails |
| 2 | eCommerce | Orders, inventory changes |
| 3 | Healthcare | Patient record changes |
| 4 | Logistics | Shipment events and delivery status |
Spring Boot Example: Simple Event Sourcing for Account
We will build a simple Account microservice that −
Accepts commands like CreateAccount, DepositMoney
Persists events to an in-memory list (simulating event store)
Applies events to rebuild account balance
Technologies
Java 17+
Spring Boot 3.x
Model: Domain Event Base Class
public interface DomainEvent {
LocalDateTime occurredAt();
}
Account Events
public class AccountCreatedEvent implements DomainEvent {
private final String accountId;
private final LocalDateTime occurredAt = LocalDateTime.now();
public AccountCreatedEvent(String accountId) {
this.accountId = accountId;
}
public String getAccountId() { return accountId; }
public LocalDateTime occurredAt() { return occurredAt; }
}
public class MoneyDepositedEvent implements DomainEvent {
private final String accountId;
private final double amount;
private final LocalDateTime occurredAt = LocalDateTime.now();
public MoneyDepositedEvent(String accountId, double amount) {
this.accountId = accountId;
this.amount = amount;
}
public String getAccountId() { return accountId; }
public double getAmount() { return amount; }
public LocalDateTime occurredAt() { return occurredAt; }
}
Event Store (In-Memory)
@Service
public class EventStore {
private final List<DomainEvent> events = new ArrayList<>();
public void save(DomainEvent event) {
events.add(event);
}
public List<DomainEvent> getEventsForAccount(String accountId) {
return events.stream()
.filter(e -> {
if (e instanceof AccountCreatedEvent ac) {
return ac.getAccountId().equals(accountId);
} else if (e instanceof MoneyDepositedEvent md) {
return md.getAccountId().equals(accountId);
}
return false;
})
.toList();
}
}
Aggregate: Account
public class Account {
private final String accountId;
private double balance = 0;
public Account(String accountId) {
this.accountId = accountId;
}
public void apply(DomainEvent event) {
if (event instanceof AccountCreatedEvent) {
// no-op
} else if (event instanceof MoneyDepositedEvent e) {
this.balance += e.getAmount();
}
}
public double getBalance() {
return balance;
}
}
Command Controller
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private EventStore store;
@PostMapping("/{id}/create")
public ResponseEntity<String> createAccount(@PathVariable String id) {
AccountCreatedEvent event = new AccountCreatedEvent(id);
store.save(event);
return ResponseEntity.ok("Account created: " + id);
}
@PostMapping("/{id}/deposit")
public ResponseEntity<String> deposit(@PathVariable String id, @RequestParam double amount) {
MoneyDepositedEvent event = new MoneyDepositedEvent(id, amount);
store.save(event);
return ResponseEntity.ok("Deposited " + amount);
}
@GetMapping("/{id}")
public ResponseEntity<String> getBalance(@PathVariable String id) {
List<DomainEvent> events = store.getEventsForAccount(id);
Account account = new Account(id);
events.forEach(account::apply);
return+ ResponseEntity.ok("Balance: " + account.getBalance());
}
}
Combining with CQRS
Event Sourcing works beautifully with CQRS −
Command model modifies state via events
Query model uses projections of those events
Can use different databases for read/write
This enables high scalability and responsiveness for read-heavy systems.
Tools and Frameworks
| Sr.No. | Tool / Library | Description |
|---|---|---|
| 1 | Axon Framework | Java framework for CQRS + Event Sourcing |
| 2 | Eventuate | Platform for event-driven microservices |
| 3 | Kafka | Durable distributed event store |
| 4 | PostgreSQL | Can be used as event store with event tables |
| 5 | Debezium | CDC (Change Data Capture) tool for generating events from DB changes |
Summary
| Sr.No. | Topic | Key Takeaway |
|---|---|---|
| 1 | What is Event Sourcing | Store state as events |
| 2 | Benefits | Audit, scalability, debugging |
| 3 | Implementation | Events + Aggregates + Event Store |
| 4 | Best Fit | Complex domains, financial logs |
| 5 | Tools | Axon, Kafka, Spring Boot |
Conclusion
Event Sourcing is a powerful pattern that provides traceability, scalability, and flexibility. When combined with microservices and messaging tools like Kafka, it enables robust, event-driven architectures.
While it introduces complexity, especially around modeling and querying, the long-term benefits−especially in systems requiring audit, replay, and high scalability-are significant.
Start small with in-memory event logs or lightweight projections, and grow into full-fledged event-sourced systems as your microservices mature.