Software Development 5 min read

Managing Business Logic with Event Streams: Flexible and Traceable Applications with Event Sourcing

PROFSCODE Team 08 Jul 2026
Share:
Event Sourcing Pattern: Building Resilient and Auditable Systems

Introduction: Why Record Events, Not Just State?

Traditional database approaches typically store the system's current state. When a user performs an action, data in the relevant table is updated, and the old state is lost. However, in many scenarios, knowing why and how something changed is more valuable than just the current state. This is where the Event Sourcing pattern comes in. This pattern records every state change as an event and reconstructs the system's current state as the accumulation of these events.

Core Principles of Event Sourcing

The essence of Event Sourcing is that every business transaction is recorded as an 'event'. These events are immutable records representing a change in the system and are appended to an Event Store. The current state is derived by applying these events sequentially.

  • Immutable Events: Once an event is created, it can never be changed or deleted.
  • Event Store: A database where all events are stored in chronological order.
  • State Reconstruction: The current state of an object can always be rebuilt by replaying all relevant events in sequence.

Advantages and Use Cases

  • Full Auditability: Provides a complete history of every change in the system.
  • Temporal Querying: Easily query the system's state at any point in time (e.g., bank balance three days ago).
  • Debugging and Reversion: Event streams can be examined to pinpoint issues or revert unwanted changes.
  • Integration with CQRS: Naturally aligns with the Command Query Responsibility Segregation (CQRS) pattern, separating read and write models.
  • Richer Business Analysis: Enables a better understanding of the causes and effects of changes in business processes.

A Practical Example: A Simple Bank Account

Let's model a bank account application using Event Sourcing. Account creation, money deposits, and withdrawals will be recorded as events.

1. Event Definitions

Each event will be a class containing a specific action and relevant data.

public interface IEvent {}public class AccountCreatedEvent : IEvent{    public Guid AccountId { get; }    public string Owner { get; }    public DateTime CreatedAt { get; }    public AccountCreatedEvent(Guid accountId, string owner, DateTime createdAt)    {        AccountId = accountId;        Owner = owner;        CreatedAt = createdAt;    }}public class MoneyDepositedEvent : IEvent{    public Guid AccountId { get; }    public decimal Amount { get; }    public DateTime DepositedAt { get; }    public MoneyDepositedEvent(Guid accountId, decimal amount, DateTime depositedAt)    {        AccountId = accountId;        Amount = amount;        DepositedAt = depositedAt;    }}public class MoneyWithdrawnEvent : IEvent{    public Guid AccountId { get; }    public decimal Amount { get; }    public DateTime WithdrawnAt { get; }    public MoneyWithdrawnEvent(Guid accountId, decimal amount, DateTime withdrawnAt)    {        AccountId = accountId;        Amount = amount;        WithdrawnAt = withdrawnAt;    }}

2. Aggregate and Applying Events

The BankAccount aggregate receives commands, generates events, and then applies these events to its own state.

public class BankAccount{    public Guid Id { get; private set; }    public string Owner { get; private set; }    public decimal Balance { get; private set; }    private List<IEvent> _changes = new List<IEvent>();    public BankAccount()    {        Balance = 0;    }    public static BankAccount CreateNew(Guid accountId, string owner)    {        var account = new BankAccount();        account.ApplyChange(new AccountCreatedEvent(accountId, owner, DateTime.UtcNow));        return account;    }    public void Deposit(decimal amount)    {        if (amount <= 0) throw new ArgumentException("Deposit amount must be greater than zero.");        ApplyChange(new MoneyDepositedEvent(Id, amount, DateTime.UtcNow));    }    public void Withdraw(decimal amount)    {        if (amount <= 0) throw new ArgumentException("Withdrawal amount must be greater than zero.");        if (Balance < amount) throw new InvalidOperationException("Insufficient balance.");        ApplyChange(new MoneyWithdrawnEvent(Id, amount, DateTime.UtcNow));    }    public IEnumerable<IEvent> GetUncommittedChanges() => _changes;    public void MarkChangesAsCommitted() => _changes.Clear();    public void LoadFromHistory(IEnumerable<IEvent> history)    {        foreach (var e in history)        {            ApplyChange(e, isNew: false);        }    }    private void ApplyChange(IEvent e, bool isNew = true)    {        When((dynamic)e);        if (isNew)        {            _changes.Add(e);        }    }    private void When(AccountCreatedEvent e)    {        Id = e.AccountId;        Owner = e.Owner;        Balance = 0;    }    private void When(MoneyDepositedEvent e)    {        Balance += e.Amount;    }    private void When(MoneyWithdrawnEvent e)    {        Balance -= e.Amount;    }}

3. Event Store

The Event Store will be capable of saving events and replaying events for an aggregate.

public interface IEventStore{    void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion);    List<IEvent> GetEventsForAggregate(Guid aggregateId);}// A simple in-memory Event Store implementationpublic class InMemoryEventStore : IEventStore{    private readonly Dictionary<Guid, List<IEvent>> _storage = new Dictionary<Guid, List<IEvent>>();    public void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion)    {        if (!_storage.ContainsKey(aggregateId))        {            _storage.Add(aggregateId, new List<IEvent>());        }        var existingEvents = _storage[aggregateId];        // Optimistic concurrency check        if (expectedVersion != -1 && existingEvents.Count != expectedVersion)        {            throw new InvalidOperationException("Concurrency conflict. Expected version: " + expectedVersion + ", Current version: " + existingEvents.Count);        }        existingEvents.AddRange(events);    }    public List<IEvent> GetEventsForAggregate(Guid aggregateId)    {        return _storage.ContainsKey(aggregateId) ? _storage[aggregateId] : new List<IEvent>();    }}

4. Executing Events and Reconstructing State

// Application Logic (Example Usage)var eventStore = new InMemoryEventStore();var accountId = Guid.NewGuid();var newAccount = BankAccount.CreateNew(accountId, "John Doe");newAccount.Deposit(100);newAccount.Withdraw(30);eventStore.SaveEvents(accountId, newAccount.GetUncommittedChanges(), -1);newAccount.MarkChangesAsCommitted();// Load the account from elsewhere and perform operationsvar events = eventStore.GetEventsForAggregate(accountId);var existingAccount = new BankAccount();existingAccount.LoadFromHistory(events);Console.WriteLine($"Account Owner: {existingAccount.Owner}, Balance: {existingAccount.Balance}"); // Output: Account Owner: John Doe, Balance: 70existingAccount.Deposit(50);eventStore.SaveEvents(accountId, existingAccount.GetUncommittedChanges(), events.Count);existingAccount.MarkChangesAsCommitted();var updatedEvents = eventStore.GetEventsForAggregate(accountId);var updatedAccount = new BankAccount();updatedAccount.LoadFromHistory(updatedEvents);Console.WriteLine($"Updated Account Owner: {updatedAccount.Owner}, Updated Balance: {updatedAccount.Balance}"); // Output: Updated Account Owner: John Doe, Updated Balance: 120

Conclusion

Event Sourcing provides a completely different perspective on software architectures. By maintaining a sequence of events that constitute every state, rather than just snapshots of states, we can make our systems more transparent, auditable, and flexible. Although there is an initial learning curve, Event Sourcing offers a powerful tool for developers, especially in complex business logic scenarios, where historical data is crucial, and in microservices architectures. This pattern enables you to understand not only 'how' your systems appear, but also 'why' they appear that way.

Back to Blog

Comments (0)

No comments yet. Be the first to comment!

Submit Comment