Friday, 7 June 2024

Event Sourcing with Java: A Comprehensive Guide with Practical Examples

Event sourcing is a design pattern in which the system state is captured as a sequence of events. Every change to the system state is captured as an event. This sequence of events can be used to reconstruct the state of the entity/entire system at any point in time.

 


Key Concepts of Event Sourcing

1.   Event: An event is a record of something that has happened in the system. For example, in an e-commerce system, events could be "Order Placed", "Order Shipped", "Payment Received", etc.

2.   Event Store: A storage mechanism that persists events. This could be a database designed to store events or a specialized event store like EventStoreDB.

3.   Command: A request to perform an action. Commands result in events if they successfully execute.

 

Traditional Persistence vs Event Souring

In Traditional Persistence, we store the current state directly (e.g., a row in a database table), whereas in Event Sourcing, we store the history of state changes as events.

 

Let us try to understand event sourcing pattern with a simple example of Bank Account. At high level, we can have three events.

1.   Account Creation Event: This event triggered when a new bank account is created.

2.   Money Deposited Event: This event triggered when money deposited to a bank account.

3.   Money Withdraw Event: This event triggered when money withdraw from a bank account.


 

 

Find the below working application for the same.

 

Event.java

package com.sample.app.events;

import java.time.LocalDateTime;

public abstract class Event {
	private final LocalDateTime timestamp;

	protected Event(LocalDateTime timestamp) {
		this.timestamp = timestamp;
	}

	public LocalDateTime getTimestamp() {
		return timestamp;
	}

	@Override
	public String toString() {
		return "timestamp=" + timestamp ;
	}

}

AccountCreatedEvent.java

package com.sample.app.events;

import java.time.LocalDateTime;

public class AccountCreatedEvent extends Event {
	private final String accountId;
	private final String owner;

	public AccountCreatedEvent(LocalDateTime timestamp, String accountId, String owner) {
		super(timestamp);
		this.accountId = accountId;
		this.owner = owner;
	}

	public String getAccountId() {
		return accountId;
	}

	public String getOwner() {
		return owner;
	}

	@Override
	public String toString() {
		return "AccountCreated [accountId=" + accountId + ", owner=" + owner + " " + super.toString() + "]";
	}

}

MoneyDepositedEvent.java

package com.sample.app.events;

import java.time.LocalDateTime;

public class MoneyDepositedEvent extends Event {
	private final String accountId;
	private final double amount;

	public MoneyDepositedEvent(LocalDateTime timestamp, String accountId, double amount) {
		super(timestamp);
		this.accountId = accountId;
		this.amount = amount;
	}

	public String getAccountId() {
		return accountId;
	}

	public double getAmount() {
		return amount;
	}

	@Override
	public String toString() {
		return "MoneyDeposited [accountId=" + accountId + ", amount=" + amount + " " + super.toString() +"]";
	}

}

MoneyWithdrawnEvent.java

package com.sample.app.events;

import java.time.LocalDateTime;

public class MoneyWithdrawnEvent extends Event {
	private final String accountId;
	private final double amount;

	public MoneyWithdrawnEvent(LocalDateTime timestamp, String accountId, double amount) {
		super(timestamp);
		this.accountId = accountId;
		this.amount = amount;
	}

	public String getAccountId() {
		return accountId;
	}

	public double getAmount() {
		return amount;
	}

	@Override
	public String toString() {
		return "MoneyWithdrawn [accountId=" + accountId + ", amount=" + amount + " " + super.toString() +"]";
	}

}

EventStore.java

package com.sample.app.events.util;

import java.util.ArrayList;
import java.util.List;

import com.sample.app.events.Event;

public class EventStore {
	private final List<Event> events = new ArrayList<>();

	public void saveEvent(Event event) {
		events.add(event);
	}

	public List<Event> getEvents() {
		return new ArrayList<>(events);
	}
}

BankAccount.java

package com.sample.app.model;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import com.sample.app.events.AccountCreatedEvent;
import com.sample.app.events.Event;
import com.sample.app.events.MoneyDepositedEvent;
import com.sample.app.events.MoneyWithdrawnEvent;
import com.sample.app.events.util.EventStore;

public class BankAccount {
	private final String accountId;
	private final String owner;
	private double balance;
	private final EventStore eventStore;

	public BankAccount(String accountId, String owner) {
		this.accountId = accountId;
		this.owner = owner;
		this.balance = 0.0;
		this.eventStore = new EventStore();
	}

	public Event createAccount() {
		Event event = new AccountCreatedEvent(LocalDateTime.now(), accountId, owner);
		apply(event);
		return event;
	}

	public Event depositMoney(double amount) {
		Event event = new MoneyDepositedEvent(LocalDateTime.now(), accountId, amount);
		apply(event);
		return event;
	}

	public Event withdrawMoney(double amount) {
		if (balance >= amount) {
			Event event = new MoneyWithdrawnEvent(LocalDateTime.now(), accountId, amount);
			apply(event);
			return event;
		} else {
			throw new IllegalArgumentException("Insufficient funds");
		}
	}

	private void apply(Event event) {
		if (event instanceof AccountCreatedEvent) {
			this.balance = 0.0;
		} else if (event instanceof MoneyDepositedEvent) {
			this.balance += ((MoneyDepositedEvent) event).getAmount();
		} else if (event instanceof MoneyWithdrawnEvent) {
			this.balance -= ((MoneyWithdrawnEvent) event).getAmount();
		}
		this.eventStore.saveEvent(event);
	}

	public double getBalance() {
		return balance;
	}

	public List<Event> getEvents() {
		return new ArrayList<>(eventStore.getEvents());
	}

	public void printDetailedSummary() {
		for (Event event : eventStore.getEvents()) {
			System.out.println(event);
		}

		System.out.println("Total Balance : " + balance);
	}
}

App.java

package com.sample.app;

import com.sample.app.model.BankAccount;

public class App {

	public static void main(String args[]) {
		BankAccount bankAccount = new BankAccount("HARI123", "Harikrishna");
		bankAccount.createAccount();

		bankAccount.depositMoney(1000);
		bankAccount.depositMoney(2000);
		bankAccount.withdrawMoney(500);

		bankAccount.printDetailedSummary();
	}

}

Output

AccountCreated [accountId=HARI123, owner=Harikrishna timestamp=2024-06-08T11:17:13.882]
MoneyDeposited [accountId=HARI123, amount=1000.0 timestamp=2024-06-08T11:17:13.882]
MoneyDeposited [accountId=HARI123, amount=2000.0 timestamp=2024-06-08T11:17:13.882]
MoneyWithdrawn [accountId=HARI123, amount=500.0 timestamp=2024-06-08T11:17:13.883]
Total Balance : 2500.0

How These Events Help?

1.   Audit Trail: Every change to the state is recorded as an event, which in turn provides a complete audit trail of all actions performed on the bank account.

2.   Reproducibility: Since the state is derived from the events, you can reproduce the current state by replaying the events from the beginning.

3.   Powerful Debugging Tool: It greatly aids in efficiently debugging the system. If the system starts showing strange behavior, you can replay events up to a consistent state and then apply subsequent events one by one to identify the cause of the abnormal behavior.

4.   Historic State: We can go to any state that we want to go

5.   Dynamic Views: Event log can be consumed by any system and they can build their own view.

 

Replaying the events from the beginning kills performance

Event sourcing involves storing the state changes of an entity as a sequence of events. While this approach provides many benefits, it can lead to performance issues when the number of events grows over time. Snapshots helps to mitigate these performance issues by providing a more efficient way to reconstruct the current state of an entity.

 

In the context of event sourcing, snapshots can be taken at the level of individual aggregates, such as a bank account, or at the entire system level. The decision depends on the system's use case and requirements. For example, by implementing snapshots at the aggregate (account) level, each bank account can be efficiently reconstructed without replaying every event since the account's creation. Only the events that occurred after the latest snapshot need to be replayed.

 

References

https://www.youtube.com/watch?v=ck7t592bvBg

 

https://martinfowler.com/eaaDev/EventSourcing.html

 

https://martinfowler.com/bliki/MemoryImage.html

 

https://microservices.io/patterns/data/event-sourcing.html

 

https://netflixtechblog.com/scaling-event-sourcing-for-netflix-downloads-episode-2-ce1b54d46eec

 

https://microservices.io/

 

https://microservices.io/patterns/data/cqrs.html

 

https://www.eventstore.com/



                                                                                System Design Questions

No comments:

Post a Comment