Sunday, 4 February 2024

Time Travel Through Your Data: A Deep Dive into Event Sourcing

Event sourcing is a way of organizing data in a computer system. Instead of keeping track of the current state directly, it stores a series of events that show how the system changed over time. Each event represents a specific change that occurred. To figure out the current state, you just need to go through all the events in the order they happened, like playing a story from the beginning to see how things developed. This approach helps in understanding the full history of what happened in the system.

 

Let me explain it with an example to illustrates the concept of event sourcing in the context of a simple bank account scenario. In event sourcing, instead of maintaining the current state (account balance) directly, the system records a series of events that represent changes over time. 

public class BankAccount {
	private UUID accountId;
	private List<TransactionEvent> transactionHistory;
	.....
	.....
}

The BankAccount class, equipped with a unique identifier (accountId) and a transaction history (transactionHistory), adheres to this paradigm.

 

public class TransactionEvent {
	private UUID transactionId;
	private EventType type;
	private double amount;
        private Date date;
	......
	......
}

Transaction events, such as deposits and withdrawals, are encapsulated in the TransactionEvent class.

 

We can have a getBalanace method, that calculate the current account balance by processing series of events.

public synchronized double getBalance() {
	double balance = 0.0;
	for (TransactionEvent event : transactionHistory) {
		if (event.getType() == EventType.DEPOSIT) {
			balance += event.getAmount();
		} else if (event.getType() == EventType.WITHDRAWL) {
			balance -= event.getAmount();
		}
	}
	return balance;
}

We can have an applyEvent method which is responsible for recording these events in the transaction history.

 


public synchronized void applyEvent(TransactionEvent event) {
	double balance = getBalance();

	if (event.getType() == EventType.DEPOSIT) {
		if (event.getAmount() <= 0) {
			throw new IllegalArgumentException("Deposit amount must be positive");
		}
		balance += event.getAmount();
	} else if (event.getType() == EventType.WITHDRAWL) {
		if (balance < event.getAmount()) {
			throw new IllegalArgumentException("Balance is less than withdrawl amount");
		}
		balance -= event.getAmount();
	}

	transactionHistory.add(event);
}

Crucially, the balance is not explicitly stored; rather, it is dynamically reconstructed through the getBalance method by replaying the recorded events. This ensures that the current state is derived from the historical sequence of events, reflecting the essence of event sourcing. The example demonstrates how event sourcing allows the system to maintain a comprehensive transaction history, providing transparency and facilitating the reconstruction of the system's state at any point in time.

 

Find the below working application.

 

EventType.java

package com.sample.app.events;

public enum EventType {
	DEPOSIT, WITHDRAWL
}

TransactionEvent.java

package com.sample.app.events;

import java.util.Date;
import java.util.UUID;

public class TransactionEvent {
	private UUID transactionId;
	private EventType type;
	private double amount;
	private Date date;

	public TransactionEvent(UUID transactionId, EventType type, double amount) {
		this.transactionId = transactionId;
		this.type = type;
		this.amount = amount;
		this.date = new Date();
	}

	public UUID getTransactionId() {
		return transactionId;
	}

	public EventType getType() {
		return type;
	}

	public double getAmount() {
		return amount;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("|");
		builder.append(transactionId).append("|");
		builder.append(type).append("|");
		builder.append(amount).append("|");
		builder.append(date).append("|");

		return builder.toString();
	}

}

BankAccount.java

package com.sample.app.model;

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

import com.sample.app.events.EventType;
import com.sample.app.events.TransactionEvent;

public class BankAccount {
	private UUID accountId;
	private List<TransactionEvent> transactionHistory;

	public BankAccount(UUID accountId) {
		this.accountId = accountId;
		this.transactionHistory = new ArrayList<>();
	}

	// Apply a transaction event to update the state
	public synchronized void applyEvent(TransactionEvent event) {
		double balance = getBalance();

		if (event.getType() == EventType.DEPOSIT) {
			if (event.getAmount() <= 0) {
				throw new IllegalArgumentException("Deposit amount must be positive");
			}
			balance += event.getAmount();
		} else if (event.getType() == EventType.WITHDRAWL) {
			if (balance < event.getAmount()) {
				throw new IllegalArgumentException("Balance is less than withdrawl amount");
			}
			balance -= event.getAmount();
		}

		transactionHistory.add(event);
	}

	public UUID getAccountId() {
		return accountId;
	}

	public synchronized double getBalance() {
		double balance = 0.0;
		for (TransactionEvent event : transactionHistory) {
			if (event.getType() == EventType.DEPOSIT) {
				balance += event.getAmount();
			} else if (event.getType() == EventType.WITHDRAWL) {
				balance -= event.getAmount();
			}
		}
		return balance;
	}

	public List<TransactionEvent> getTransactionHistory() {
		return new ArrayList<>(transactionHistory);
	}
}

App.java

package com.sample.app;

import java.util.UUID;

import com.sample.app.events.EventType;
import com.sample.app.events.TransactionEvent;
import com.sample.app.model.BankAccount;

public class App {

	public static void main(String[] args) {
		UUID accountId = UUID.randomUUID();
		BankAccount bankAccount = new BankAccount(accountId);

		// Simulate transactions
		TransactionEvent depositEvent = new TransactionEvent(UUID.randomUUID(), EventType.DEPOSIT, 100.0);
		bankAccount.applyEvent(depositEvent);

		TransactionEvent withdrawalEvent = new TransactionEvent(UUID.randomUUID(), EventType.WITHDRAWL, 50.0);
		bankAccount.applyEvent(withdrawalEvent);

		TransactionEvent anotherDepositEvent = new TransactionEvent(UUID.randomUUID(), EventType.DEPOSIT, 30.0);
		bankAccount.applyEvent(anotherDepositEvent);

		// Print final state and transaction history
		System.out.println("Final Balance: " + bankAccount.getBalance());
		System.out.println("Transaction History: ");
		bankAccount.getTransactionHistory().stream().forEach(System.out::println);

	}
}

Output

Final Balance: 80.0
Transaction History: 
|20eaac02-13c1-47b8-a60a-d23e44729cad|DEPOSIT|100.0|Mon Feb 05 11:59:52 IST 2024
|f5095139-eb2b-47d0-88aa-108c6fb7176b|WITHDRAWL|50.0|Mon Feb 05 11:59:52 IST 2024
|78192cab-4079-430c-a650-2ac36bd6c906|DEPOSIT|30.0|Mon Feb 05 11:59:52 IST 2024

Event sourcing presents numerous benefits when contrasted with conventional data storage approaches. The operational mechanism involves the following steps:

 

1.   Events: Whenever a modification occurs in the system, a corresponding event is generated to articulate the nature of the change. This event encompasses essential details such as the specific alteration, the timestamp of the occurrence, and the entity responsible for the change.

2.   Event Store: These events find their place in an append-only log known as an event store. This strategic storage approach ensures that the historical record of alterations remains immutable and impervious to tampering.

3.   State Reconstruction: To ascertain the current state of the system, the events are systematically replayed in chronological order, commencing from the initial state. This iterative process progressively constructs the present state by incorporating the effects of each event, thereby providing a comprehensive and accurate representation of the system's current state.

 

Advantages of Event Sourcing:

1.   Audit Trail: The event store keeps a complete and trustworthy record of all changes made to the system. This is very helpful for debugging problems, and keeping things secure.

2.   Versioning: It's simple to keep track of different versions of the data by replaying the events up to the time you want to check. This makes it easy to undo changes and look at the history of what happened.

3.   Scalability: Events are small and unchangeable, so it is is easy to model a scalable application.

4.   Event-Driven Architecture: Event sourcing works well with event-driven setups, where events are used to start other processes in the system.

 

Challenges with Event Sourcing

1.   Complexity: Introducing and managing event sourcing can be trickier compared to regular methods. Developers must grasp the concepts and patterns associated with it.

2.   Performance: Building the current state from events can take up a lot of computer power, especially with big sets of data. This might slow things down.

3.   Queries: Getting information about the current state quickly can be tough because it involves replaying events. Using materialized views can make this easier and more efficient (A materialized view is a database object that stores the result of a precomputed query, essentially a snapshot of the data at a specific point in time.).

 

Event sourcing can be used for systems that

1.   Experience frequent changes and demand a precise audit trail.

2.   Gain advantages from versioning and a thorough historical analysis.

3.   Align well with an event-driven architecture.

 

However, it might not be the most suitable option for performance-sensitive queries.

 

References

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

https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing

 

You can download this application from this link.





 

                                                                          System Design Questions

No comments:

Post a Comment