×

Event Sourcing – Ein Einblick abseits des Status Quo

In vielen Softwareprojekten werden relationale Datenbanken verwendet, weil sie sich über viele Jahre hinweg bewährt haben. Mit Frameworks wie Hibernate lassen sie sich auf recht einfache Weise in Anwendungen nutzen und sind für viele Projekte schlicht von Beginn an als Lösungsansatz gesetzt. Dagegen ist Event Sourcing unter Verwendung eines Event Stores recht unbekannt. Als ich diesen Ansatz im Rahmen meiner Bachelorarbeit für eine Schlüsselverwaltung ausprobierte, merkte ich schnell, dass er intuitiv dazu führte, ein viel fachlicheres Domänenmodell in Richtung Domain-driven Design zu entwickeln. Nachfolgend gehe ich genauer darauf ein, was Event Sourcing ist und warum es solche Auswirkungen hatte.

Was ist Event Sourcing?

Bei Event Sourcing handelt es sich um ein spezielles Persistenzverfahren. Der Grundgedanke ist hierbei, nicht den aktuellen Zustand des Datenmodells einer Anwendung zu speichern, sondern jede Änderung an diesem. Werden Daten abgerufen, werden dafür alle vorgenommenen Änderungen geladen und erneut abgespielt.

Die vorgenommenen Änderungen werden als Events bezeichnet. Ein einmal aufgetretenes Event wird gespeichert und kann dann nicht wieder gelöscht oder verändert werden. War eine Änderung ungewollt, ist ein weiteres Event nötig, um dies rückgängig zu machen.

Es bildet sich ein sogenannter Event Stream für das Datenmodell, der zwangsläufig immer eine vollständige Historie aller Änderungen darstellt. Verwendet man hierfür ausschließlich Domänenevents, wie sie z.B. Domain-driven Design vorsieht, dann beschreibt der Event Stream, im Gegensatz z.B. zu Transaktionslogs einer relationalen Datenbank, alle Änderungen anhand der durchgeführten Prozessschritte innerhalb der Domäne.

Welche Vorteile ergeben sich?

Im Vergleich zur Verwendung eines relationalen Datenbanksystems bietet Event Sourcing einige Vorteile, von denen ich hier insbesondere den Zwang zu fachlichem Code und die einfache Testbarkeit betonen möchte und unten erklären werde. Natürlich gibt es auch auf der technischen Seite viele Vorteile, z.B. dass es eine vollständige, unveränderbare Historie gibt, die man für Audits oder zur Datenanalyse verwenden kann, dass die Events sich auch sehr gut für Messaging mit einem Message-Bus eignen und natürlich dass sich mit Event Sourcing in Kombination mit dem Messaging gut skalierbare Anwendungen bauen lassen, was z.B. für Microservices interessant sein kann.

Fachlicher Code

Entwickelt man Software unter Verwendung von Event Sourcing, dann zieht das oft nach sich, dass auch Domain-driven Design oder zumindest einige Konzepte aus dem taktischen Design angewandt werden. Die Domainevents von DDD eignen sich perfekt dafür, mittels Event Sourcing persistiert zu werden. Da sich diese an den Prozessen orientieren, die die Software abbildet, lassen sie sich z.B. sehr gut mit Event Stormings erarbeiten. Das Datenmodell kann dann aus Aggregaten aufgebaut werden, die Commands ausführen können. Das kann z.B. wie folgt aussehen:

class BankAccount {
  private AmountOfMoney balance;
  //…

  @CommandHandler
  public List depositAmount(DepositAmountCommand cmd) {
    return List.of(AmountDepositedEvent.of(this.id, cmd.getAmount());
  }

  @EventHandler
  public void amountDeposited(AmountDepositedEvent event) {
    this.balance.add(event.getAmount());
  }

  @CommandHandler
  public List withdrawAmount(WithdrawAmountCommand cmd) {
    assertEnoughMoneyInAccount(cmd.getAmount());
    return List.of(AmountWithdrawnEvent.of(this.id, cmd.getAmount());
  }

  @EventHandler
  public void amountdeposited(AmountWithdrawnEvent event) {
    this.balance = this.balance.plus(event.getAmount());
  }
  //…
}

Die Besonderheit und auch ein Unterschied zu DDD ist, dass die Commandhandler zwar alle Geschäftsregeln prüfen, aber den Zustand des Aggregats nicht verändern. Stattdessen werden Events erzeugt und herausgegeben, welche wiederum von Eventhandlern verarbeitet werden können. Erst in diesen werden letztlich die Zustandsänderungen durchgeführt. Dies ermöglicht, ein Aggregat beim Laden aus dem Event Store durch das Einspielen der Events wiederherzustellen, ohne die Geschäftsregeln erneut prüfen lassen zu müssen.

Alle Aggregate können nach dieser einheitlichen Struktur aufgebaut werden, was zu sehr fachlich orientiertem Code führt, insofern man keine sehr grobgranularen Events verwendet. Dies kann man natürlich auch ohne Event Sourcing erreichen, aber dann steigt die Gefahr, ein anemisches Modell, also ein Modell bestehend nur aus Gettern und Settern, zu implementieren. Leider sieht man viel zu oft, dass Hibernate-Entitys als Domänenmodell herhalten müssen und die Geschäftslogik dann im gesamten System verstreut wird. Das mag zwar Code und Tipparbeit sparen, erzeugt aber langfristig gesehen eine sehr schwer zu wartende und zu testende Anwendung. Erstellt man ein separates Domänenmodell und verwendet Hibernate stattdessen ausschließlich in den Repositories für diese, lässt sich die Geschäftslogik gut gekapselt zusammenfassen, ohne Kompromisse für die Persistierung und gegen die Testbarkeit eingehen zu müssen. Das führt auch dazu, dass Aggregate wie in DDD möglich werden, also kleine Einheiten, die immer zusammen geladen und persistiert werden und eine in sich immer konsistente Einheit bilden. Bei Event Sourcing besitzt jedes Aggregat dann seinen eigenen unabhängigen Event Stream. Warum sich so ein Domänenmodell besser zum Testen eignet, zeigt der nächste Abschnitt.

Testbarkeit

Wie schon erwähnt wurde, ergibt sich der Zustand eines Aggregats aus den Events, die in seinem Event Stream sind. Dies lässt sich wunderbar für Akzeptanztests verwenden. Man kann einen Event Stream vorgeben, um einen Ausgangszustand für den Test zu erhalten und dann versuchen, einen Command auszuführen. Es kann dann überprüft werden, ob z.B. bestimmte Exceptions geworfen wurden, ob die Events im Erfolgsfall genau das enthalten, was sie sollten und ob sie das Aggregat so modifizieren, wie es gedacht war. Dies ermöglicht sogar intuitiv Test-driven Development. Für meine Bachelorarbeit habe ich hierfür eine kleine Fluent-API geschrieben. Dies sieht dann wie folgt aus:

@Test
public void testWithdraw() {
  AcceptanceTest.init(new BankAccount())
    .given(AmountDepositedEvent.of(accountId, AmountOfMoney.of(100)))
    .when(WithDrawAmountCommand.of(accountId, AmountOfMoney.of(50)))
    .then(AmountWithdrawnEvent.of(accountId, AmountOfMoney.of(50)))
    .thenAssert(account -> assertEquals(AmountOfMoney.of(50), account.getBalance()));
}
@Test
public void testWithdrawWithoutEnoughMoney() {
  AcceptanceTest.init(new BankAccount())
    .given(AmountDepositedEvent.of(accountId, AmountOfMoney.of(10)))
    .when(WithDrawAmountCommand.of(accountId, AmountOfMoney.of(50)))
    .thenThrow (AccountNotCoveredException.class);
}

Es handelt sich hierbei um reine Unit-Tests. Es wird also nichts persistiert oder per Messaging verschickt. Hinter der Fluent-API steckt lediglich ein bisschen Reflection, um die passenden Command- und Eventhandler zu finden und aufzurufen. Das Testen ist also sehr einfach und nachdem die von uns entwickelte Anwendung produktiv ging und die ersten Bugs auftauchten, konnten diese sogar ganz einfach nachgestellt werden, indem ein neuer Akzeptanztest mit dem Event Stream des entsprechenden Aggregats aus dem Produktivsystem erstellt wurde. Über das Testen hinaus hilft Event Sourcing in vielen Fällen auch beim Debugging, weil sich jede Änderung nachvollziehen lässt. Selbst im Produktivsystem kann man im Livebetrieb mit dem sogenannten Time Traveling einen Blick in die vorherigen Zustände des Systems werfen. Dafür muss nur angegeben werden, bis zu welchem Zeitpunkt die Event Streams geladen werden sollen. Dies ist vergleichbar mit den Revisionen in Git, nur das bei Event Sourcing Time Traveling üblicherweise nur lesend stattfindet, während in Git dafür ein neuer Branch erstellt werden kann.

Nachteile

In der Softwareentwicklung gibt es keine Silver Bullets und so ist auch Event Sourcing keine. Es gibt sogar einige große Nachteile, die einem vor der Verwendung bewusst sein sollten. Das häufigste Problem stellt wahrscheinlich die notwendige Abwärtskompatibilität dar. Die Struktur der Events in der Software kann nicht einfach verändert werden, da es für die Wiederherstellung von Aggregaten notwendig ist, alle gespeicherten Events wieder in das System laden zu können. Statt ein bestehendes Event anzupassen, muss deshalb für solche Zwecke immer ein neues Event, wie z.B. AmountDepositedEventV2, erstellt werden. Die Erstellung neuer Events des ersten Typs kann dann zwar abgeschaltet werden, aber nichtsdestotrotz braucht es weiterhin die Eventhandler für die bestehenden Events.

Besonders schwierig wird es, wenn ein Refactoring des Domänenmodells vorgenommen werden soll und Events dann von anderen oder auch neuen Aggregaten gehandhabt werden sollen. Dafür ist dann eine Migration nötig, in der die gesamte betroffene Historie eingelesen, in das neue Modell übersetzt und neu gespeichert werden muss.

Oft wird auch angeführt, dass die Performance von Anwendung mit Event Sourcing schlechter ist, weil immer ein ganzer Event Stream je Aggregat geladen werden muss. Hierfür gibt es mit Snapshots, also Events, die den gesamten Zustand des Aggregats enthalten und damit direkt wiederherstellbar machen, eine gute Optimierungsmöglichkeit, wodurch dieser Nachteil meistens eher nicht sonderlich gravierend ist. Dabei geht die Historie vor dem Snapshot nicht verloren, sondern muss durch den Snapshot für die Wiederherstellung des Aggregats nur nicht mehr mit geladen werden. Natürlich ist es aber auch ein Mehraufwand entsprechende Eventhandler für Snapshot-Events zu erstellen und zu testen.

Nicht zuletzt kann die Verwendung von Event Sourcing aber auch aus rechtlichen Gründen problematisch sein, wenn z.B. Daten gemäß der DSGVO gelöscht werden müssten. Für diesen Fall müssten eben doch alte Events gelöscht werden können. Mit Snapshots wäre es trotzdem möglich, Aggregate wiederherzustellen, obwohl nicht mehr die vollständige Historie vorhanden ist. Wenn der gesamte Event Stream eines Aggregates gelöscht werden soll, dann braucht es zwar keine Snapshots mehr, aber es muss dann darauf geachtet werden, dass kein anderes Aggregat in seinem aktuellen Zustand mehr eine Referenz auf das gelöschte Aggregat hält.

Fazit

Event Sourcing ist ein Ansatz, der deutlich vom klassischen Vorgehen abweicht, wodurch man selbst dazu gezwungen ist, das eigene Vorgehen grundlegend zu überdenken. Plant man eine Anwendung von vornherein mit Event Stormings oder ähnlichen Ansätzen, ist es intuitiv einfach, dies unter Verwendung von Event Sourcing in ein rein fachliches Domänenmodell zu übertragen und mit Akzeptanztests zu testen. Jedoch muss man damit rechnen, dass Refactorings am Domänenmodell aufwendiger sind als bei Systemen, in denen nur der aktuelle Zustand des Modells geladen wird. Event Sourcing ist deshalb keine Patentlösung, sondern sollte nur dort eingesetzt werden, wo es seine Vorteile ausspielen kann. Als Beispiele können Anwendungen aufgeführt werden, die auditiert werden, die viele Berichte erzeugen oder die eine Änderungshistorie oder ein Protokoll benötigen.

Auch, wenn man Event Sourcing nicht produktiv verwendet, lohnt sich das Ausprobieren definitiv. Es gibt Projekte, in denen nicht so sehr darauf geachtet wurde, Technisches von Fachlichem zu trennen. Hier kann man den Status Quo in Frage stellen und die positiven Seiten von Event Sourcing einfließen lassen.

Quellen:
V. Vernon, Implementing Domain-Driven Design, Boston: Addison Wesley, 2013
E. Wolff, Microservices – Grundlagen flexibler Softwarearchitekturen (2. Auflage), Heidelberg: dpunkt.verlag, 2018
https://eventstore.com/

Von Alexander Dammeier | 6.10.2020
Alexander Dammeier

Softwareentwicklung