Softwareentwicklung 5 Min. Lesezeit

Geschäftslogik mit Ereignisströmen verwalten: Flexible und nachvollziehbare Anwendungen mit Event Sourcing

PROFSCODE Ekibi 08 Jul 2026
Paylaş:
Event Sourcing Pattern: Robuste und auditierbare Systeme bauen

Einleitung: Warum Ereignisse und nicht nur den Zustand aufzeichnen?

Traditionelle Datenbankansätze speichern in der Regel den aktuellen Zustand eines Systems. Wenn ein Benutzer eine Aktion ausführt, werden die Daten in der entsprechenden Tabelle aktualisiert, und der alte Zustand geht verloren. In vielen Szenarien ist es jedoch wertvoller zu wissen, warum und wie sich etwas geändert hat, als nur den aktuellen Zustand zu kennen. Hier kommt das Event Sourcing-Muster ins Spiel. Dieses Muster zeichnet jede Zustandsänderung als Ereignis auf und rekonstruiert den aktuellen Zustand des Systems als Akkumulation dieser Ereignisse.

Kernprinzipien des Event Sourcing

Das Wesen von Event Sourcing besteht darin, dass jede Geschäftstransaktion als 'Ereignis' aufgezeichnet wird. Diese Ereignisse sind unveränderliche Aufzeichnungen, die eine Änderung im System darstellen, und werden einem Event Store hinzugefügt. Der aktuelle Zustand wird durch die sequentielle Anwendung dieser Ereignisse abgeleitet.

  • Unveränderliche Ereignisse: Sobald ein Ereignis erstellt wurde, kann es niemals geändert oder gelöscht werden.
  • Event Store: Eine Datenbank, in der alle Ereignisse in chronologischer Reihenfolge gespeichert werden.
  • Zustandsrekonstruktion: Der aktuelle Zustand eines Objekts kann jederzeit durch das sequentielle Wiederabspielen aller relevanten Ereignisse rekonstruiert werden.

Vorteile und Anwendungsfälle

  • Vollständige Auditierbarkeit: Bietet eine vollständige Historie jeder Änderung im System.
  • Temporale Abfragen: Einfaches Abfragen des Systemzustands zu jedem Zeitpunkt (z.B. Kontostand vor drei Tagen).
  • Fehlerbehebung und Rückgängigmachen: Ereignisströme können untersucht werden, um Probleme zu identifizieren oder unerwünschte Änderungen rückgängig zu machen.
  • Integration mit CQRS: Passt sich natürlich dem Command Query Responsibility Segregation (CQRS)-Muster an, indem es Lese- und Schreibmodelle trennt.
  • Reichhaltigere Geschäftsanalyse: Ermöglicht ein besseres Verständnis der Ursachen und Auswirkungen von Änderungen in Geschäftsprozessen.

Ein praktisches Beispiel: Ein einfaches Bankkonto

Modellieren wir eine Bankkonto-Anwendung mit Event Sourcing. Kontoerstellung, Geldeinzahlungen und Abhebungen werden als Ereignisse aufgezeichnet.

1. Ereignisdefinitionen

Jedes Ereignis ist eine Klasse, die eine spezifische Aktion und relevante Daten enthält.

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. Aggregat und Anwenden von Ereignissen

Das BankAccount-Aggregat empfängt Befehle, generiert Ereignisse und wendet diese Ereignisse dann auf seinen eigenen Zustand an.

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("Der Einzahlungsbetrag muss größer als Null sein.");        ApplyChange(new MoneyDepositedEvent(Id, amount, DateTime.UtcNow));    }    public void Withdraw(decimal amount)    {        if (amount <= 0) throw new ArgumentException("Der Abhebungsbetrag muss größer als Null sein.");        if (Balance < amount) throw new InvalidOperationException("Unzureichendes Guthaben.");        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

Der Event Store wird in der Lage sein, Ereignisse zu speichern und Ereignisse für ein Aggregat wiederzugeben.

public interface IEventStore{    void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion);    List<IEvent> GetEventsForAggregate(Guid aggregateId);}// Eine einfache In-Memory-Event Store Implementierungpublic 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];        // Optimistische Parallelitätsprüfung        if (expectedVersion != -1 && existingEvents.Count != expectedVersion)        {            throw new InvalidOperationException("Parallelitätskonflikt. Erwartete Version: " + expectedVersion + ", Aktuelle Version: " + existingEvents.Count);        }        existingEvents.AddRange(events);    }    public List<IEvent> GetEventsForAggregate(Guid aggregateId)    {        return _storage.ContainsKey(aggregateId) ? _storage[aggregateId] : new List<IEvent>();    }}

4. Ausführen von Ereignissen und Rekonstruieren des Zustands

// Anwendungslogik (Beispielverwendung)var eventStore = new InMemoryEventStore();var accountId = Guid.NewGuid();var newAccount = BankAccount.CreateNew(accountId, "Max Mustermann");newAccount.Deposit(100);newAccount.Withdraw(30);eventStore.SaveEvents(accountId, newAccount.GetUncommittedChanges(), -1);newAccount.MarkChangesAsCommitted();// Konto von anderswo laden und Operationen durchführenvar events = eventStore.GetEventsForAggregate(accountId);var existingAccount = new BankAccount();existingAccount.LoadFromHistory(events);Console.WriteLine($"Kontoinhaber: {existingAccount.Owner}, Saldo: {existingAccount.Balance}"); // Ausgabe: Kontoinhaber: Max Mustermann, Saldo: 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($"Aktualisierter Kontoinhaber: {updatedAccount.Owner}, Aktualisierter Saldo: {updatedAccount.Balance}"); // Ausgabe: Aktualisierter Kontoinhaber: Max Mustermann, Aktualisierter Saldo: 120

Fazit

Event Sourcing bietet eine völlig neue Perspektive auf Softwarearchitekturen. Indem wir eine Sequenz von Ereignissen beibehalten, die jeden Zustand konstituieren, anstatt nur Snapshots von Zuständen, können wir unsere Systeme transparenter, auditierbarer und flexibler gestalten. Obwohl es eine anfängliche Lernkurve gibt, bietet Event Sourcing ein leistungsstarkes Werkzeug für Entwickler, insbesondere in Szenarien mit komplexer Geschäftslogik, in denen historische Daten entscheidend sind, und in Mikroservices-Architekturen. Dieses Muster ermöglicht es Ihnen, nicht nur zu verstehen, 'wie' Ihre Systeme aussehen, sondern auch 'warum' sie so aussehen.

Zurück zum Blog

Kommentare (0)

Noch keine Kommentare. Seien Sie der Erste!

Kommentar absenden