Event Sourcing Pattern in Java – Immutable State Management with Events

Illustration for Event Sourcing Pattern in Java – Immutable State Management with Events
By Last updated:

Introduction

Traditional applications store only the current state of data. For example, an account_balance field shows how much money you have—but not how you got there.

Event Sourcing flips this model.

Instead of saving the final state, Event Sourcing saves a sequence of events that lead to that state. The current state is a projection derived by replaying all historical events.

This pattern is crucial for systems needing auditability, reliability, and state traceability.


🧠 What Is the Event Sourcing Pattern?

Event Sourcing is a design pattern that persists the state of a system as a sequence of immutable events. Instead of updating and storing the current state, you append events describing changes, and replay them to reconstruct the current state.


UML Structure (Text-Based)

[Command Handler]
     |
     v
[Domain Logic] ---> [Event Store] ---> [Event Stream] ---> [Projections / Read Models]

👥 Core Participants

  • Command: Triggers an intent to change state.
  • Aggregate: Applies domain logic and produces events.
  • Event: Immutable representation of a state change.
  • Event Store: Append-only log of domain events.
  • Projection: Read model built by replaying events.

🌍 Real-World Use Cases

  • Financial systems: Transactions and audit logs
  • E-commerce: Order, payment, shipping flows
  • IoT systems: Sensor state transitions
  • Game engines: Player actions over time

🧰 Java Implementation Strategies

Using Axon Framework for Event Sourcing

Maven Dependency

<dependency>
  <groupId>org.axonframework</groupId>
  <artifactId>axon-spring-boot-starter</artifactId>
  <version>4.8.0</version>
</dependency>

Define Commands and Events

public record CreateAccountCommand(String accountId, int initialBalance) {}
public record AccountCreatedEvent(String accountId, int initialBalance) {}

Create Aggregate

@Aggregate
public class Account {

    @AggregateIdentifier
    private String accountId;
    private int balance;

    @CommandHandler
    public Account(CreateAccountCommand command) {
        AggregateLifecycle.apply(new AccountCreatedEvent(command.accountId(), command.initialBalance()));
    }

    @EventSourcingHandler
    public void on(AccountCreatedEvent event) {
        this.accountId = event.accountId();
        this.balance = event.initialBalance();
    }
}

Projection (Query Side)

@Component
public class AccountProjection {

    private final Map<String, Integer> store = new HashMap<>();

    @EventHandler
    public void on(AccountCreatedEvent event) {
        store.put(event.accountId(), event.initialBalance());
    }

    public int getBalance(String accountId) {
        return store.getOrDefault(accountId, 0);
    }
}

✅ Pros and Cons

Pros Cons
Full audit trail of state changes Complex to implement and debug
Easily supports undo or rollback Event schema evolution is hard
High-performance reads with projections Requires more storage (event + read model)
Aligns well with CQRS and DDD Event replay can be slow for large histories

❌ Anti-Patterns and Misuse Cases

  • Storing full state in events instead of changes
  • Mutating events after they’ve been published
  • Using event sourcing when CRUD would suffice
  • Not designing for event versioning

🔁 Comparison with Similar Patterns

Pattern Purpose
Event Sourcing Reconstruct state from events
CQRS Separate read and write models
CRUD Direct state persistence
Change Data Capture Track changes post-write

💻 Java Class Overview (Event Flow)

// Command triggers domain action
CreateAccountCommand cmd = new CreateAccountCommand("123", 1000);

// Command handler applies event
AggregateLifecycle.apply(new AccountCreatedEvent("123", 1000));

// Event store saves the event
// Projection listens and updates read model

🔧 Refactoring Legacy Code

Before

account.setBalance(account.getBalance() + 100);
repository.save(account);

After (Event Sourced)

AggregateLifecycle.apply(new BalanceUpdatedEvent(accountId, 100));

🌟 Best Practices

  • Design events as facts, not intentions.
  • Use event versioning from day one.
  • Keep events small and immutable.
  • Use projections for efficient querying.
  • Store correlation IDs for traceability.

🧠 Real-World Analogy

Think of a bank passbook. It doesn’t show your current balance as a number in a database. It shows every transaction line by line. You derive your current balance by adding them up.

That’s event sourcing.


☕ Java Feature Relevance

  • Java Records: Ideal for immutable events and commands.
  • Sealed Interfaces: Group and validate event types.
  • Streams: Efficiently replay events into projections.

🔚 Conclusion & Key Takeaways

The Event Sourcing Pattern is powerful for systems that require a full history of changes, high reliability, and strong auditability. While it adds complexity, its benefits are massive in the right context.

✅ Summary

  • Store all changes as immutable events.
  • Rebuild state by replaying events.
  • Use projections for optimized read access.
  • Combine with CQRS for full power.
  • Ideal for regulated, financial, and critical systems.

❓ FAQ – Event Sourcing Pattern in Java

1. What is Event Sourcing?

A pattern where all changes are stored as events, not current state.

2. Is Event Sourcing the same as event-driven architecture?

No. Event sourcing is about persistence. EDA is about communication.

3. Can I use a relational database for events?

Yes, with append-only tables or tools like Axon, EventStoreDB.

4. Is Event Sourcing suitable for all apps?

No. It’s best for systems needing audit, traceability, or rollback.

5. How do I handle event versioning?

Use upcasters or separate event versions in code.

6. Can I mutate events later?

No. Events must remain immutable forever.

7. What happens if an event handler fails?

Retry, log, or implement dead-letter queues depending on context.

8. How to deal with performance issues?

Snapshotting and partitioning event streams.

9. Does it work with CQRS?

Yes, Event Sourcing is often paired with CQRS.

10. Which tools support event sourcing in Java?

Axon Framework, Eventuate, Kafka, EventStoreDB, custom implementations.