5320 Mal verschoben: Was Event-Sourcing rückwirkend möglich macht
Eine interne To-do-App bekam einen Verlauf und eine Statistik, rückwirkend über zweieinhalb Jahre. Das verdankt sie nicht Weitsicht, sondern Event-Sourcing.
(Bild: KI / iX)
- Golo Roden
Seit Mai 2024 habe ich in meiner To-do-Liste 5320 Mal ein Fälligkeitsdatum verschoben und 2009 Mal ein To-do als erledigt markiert. Ein einzelnes To-do ist seit Oktober 2024 offen und wurde inzwischen 123 Mal verschoben. Diese Zahlen kenne ich erst seit Anfang Oktober dieses Jahres, obwohl sie bis in den Mai 2024 zurückreichen. Erhoben hat sie damals niemand, und trotzdem sind sie vollständig.
Die To-do-Liste heißt „Makani To-Dos“. Die Anwendung ist bei the native web Ende 2023 im Team für den eigenen Bedarf entstanden und dort seit April 2024 intern im Einsatz. Sie sortiert To-dos danach, ob sie überfällig sind, heute, morgen oder später anstehen, und jedes To-do lässt sich verschieben, vorziehen, erledigen oder verwerfen (siehe Abbildung 1). Öffentlich verfügbar ist sie nicht, es handelt sich um ein rein internes Werkzeug.
Die Hauptansicht von Makani To-Dos sortiert die offenen Beispiel-To-dos nach überfällig, heute, morgen und später (Abb. 1).
Gebaut ist Makani von Anfang an mit Event-Sourcing : Gespeichert wird nicht der aktuelle Stand eines To-dos, sondern jede einzelne Änderung daran. Genau das hat es möglich gemacht, die Anwendung in den vergangenen Wochen gleich zweimal im Nachhinein weiterzuentwickeln, zuerst durch einen vollständigen Neubau und dann um einen Verlauf und eine Statistik, die es vorher nicht gab. Beides geschah auf denselben Daten, und beides war nur möglich, weil diese Daten nie überschrieben wurden.
Nur Änderungen, kein Zustand
Die meisten Anwendungen speichern den aktuellen Zustand. In einer klassischen CRUD-Anwendung (Create, Read, Update, Delete) wäre ein To-do eine Zeile in einer Tabelle, mit Spalten für den Text, das Fälligkeitsdatum und den Status. Wird das To-do verschoben, überschreibt ein Update das Datum, und der alte Wert ist verloren. Nach 5320 Verschiebungen stünde in der Tabelle genau das, was jetzt gilt, und nichts darüber, wie es dazu kam.
Event-Sourcing dreht das um. Statt des Zustands werden die Ereignisse gespeichert, die zu ihm geführt haben, sogenannte Events, als unveränderliche Fakten in der Reihenfolge, in der sie geschehen sind. Makani kennt neun solcher Events, darunter „vorgemerkt“, „verschoben“, „vorgezogen“, „erledigt“ und „verworfen“.
Für ein einzelnes To-do ergibt sich daraus eine kleine Geschichte: vorgemerkt für Montag, verschoben auf Mittwoch, Text geändert, verschoben auf Freitag, erledigt. Spielt die Anwendung diese Events der Reihe nach ab, kennt sie den aktuellen Stand. Eine solche Auswertung von Events heißt Projektion, und eine Anwendung darf beliebig viele davon haben, für die Listen auf dem Bildschirm ebenso wie für alles, was später noch hinzukommt.
Ein verschobenes To-do sieht als Event, leicht gekürzt, so aus:
{
"type": "io.thenativeweb.todo.postponed",
"subject": "/5b2c8e41/to-dos/9f1d2a7c-3e4b-4c5d-8a6f-1b2c3d4e5f60",
"time": "2025-03-14T16:42:07.118Z",
"data": {
"dueDate": "2025-03-17"
}
}
Das Event hält fest, welches To-do betroffen ist, wann es verschoben wurde und auf welches Datum. Gespeichert sind die Events seit dem ersten Tag in EventSourcingDB, einer auf Event-Sourcing spezialisierten Datenbank, die mein Unternehmen the native web entwickelt. Wie weit der Unterschied zwischen beiden Ansätzen reicht, habe ich vor einiger Zeit an einem Märchen in CRUD gezeigt. Bei Makani lässt er sich nun an echten Daten beobachten.
Neu gebaut, nichts migriert
Im September habe ich Makani von Grund auf neu geschrieben, als Referenzanwendung für ArchitectureKit, einen Baukasten für Go von the native web. Von der alten Anwendung blieb dabei kaum etwas übrig. Der Umbau änderte 403 Dateien, knapp 19.500 Zeilen verschwanden und gut 19.000 kamen hinzu, aus 132 Go-Dateien wurden 47.
Auch das Verhalten änderte sich grundlegend. Die alte Fassung fragte den Server alle fünf Sekunden, ob sich etwas getan hatte, und wartete nach jedem Befehl 200 Millisekunden in der Hoffnung, dass die Daten bis dahin aktuell waren. Die neue erhält Änderungen über einen Stream und fragt gezielt nach dem Stand, den ein Befehl gerade geschrieben hat.
Videos by heise
Unverändert blieben nur die Events: dieselben neun Typen, dieselben Felder, dieselbe Bedeutung. Dafür sorgte eine einzige harte Regel, die als Kommentar direkt über den Event-Definitionen im Code steht: Kein Event-Typ und kein Feld darf umbenannt werden. Alles andere, vom Aufbau der Oberfläche bis zur Struktur des Servers, war frei. Die eigentliche Schnittstelle der Anwendung sind in diesem Sinn nicht ihre Endpunkte, sondern ihre Events.
Die neue Anwendung las beim ersten Start die gesamte Geschichte seit April 2024 und baute daraus ihren Zustand auf, ohne ein einziges Migrationsskript. Bei einer CRUD-Anwendung hätte ein solcher Neubau mit der Frage begonnen, wie sich das alte Tabellenschema in das neue überführen lässt. Hier stellte sich diese Frage nicht, weil der Zustand ohnehin nur abgeleitet ist.
Eine Spur der Vergangenheit gibt es allerdings. Bis Juni 2024 schrieb die erste Fassung das Datum beim Erledigen und Verwerfen unter einem großgeschriebenen Feldnamen, danach unter einem kleingeschriebenen. 277 Events tragen noch die alte Schreibweise, und weil sich Events nicht ändern lassen, bleibt das so. Die Datenbank übernimmt ein Schema für einen Event-Typ nur, wenn alle bereits gespeicherten Events dazu passen, also akzeptiert das Schema der neuen Anwendung beide Schreibweisen. Das ist der Preis der Unveränderlichkeit, und er fällt erstaunlich klein aus.
Der Verlauf, den es schon immer gab
Die erste Funktion, die nach dem Neubau hinzukam, ist der Verlauf. Jedes To-do zeigt nun auf Wunsch seine Geschichte: wann es angelegt wurde, wann und wohin es verschoben wurde und wann sich sein Text geändert hat. Wer einen früheren Schritt auswählt, sieht das To-do so, wie es zu diesem Zeitpunkt aussah, und kann einen früheren Text übernehmen.
Abbildung 2 zeigt ein solches To-do, das innerhalb von drei Wochen siebenmal verschoben und einmal umformuliert wurde. Die Zeilen, die bei der Änderung hinzukamen, sind hervorgehoben, und jeder Schritt trägt den Zeitpunkt, zu dem er geschah.
Der Verlauf (mit Beispieldaten) zeigt jede Verschiebung und jede Textänderung eines To-dos (Abb. 2).
Bemerkenswert ist dabei nicht die Funktion selbst, sondern ihre Reichweite. Der Verlauf gilt nicht erst ab dem Tag, an dem er eingebaut wurde, sondern für jedes To-do seit April 2024. Allein in meinem Konto umfasst er 1122 Textänderungen, von denen keine einzige mit dem Gedanken gespeichert wurde, sie später einmal anzuzeigen. Die Informationen waren einfach da, weil Event-Sourcing nichts wegwirft.
In einer CRUD-Anwendung hätte ein Verlauf ein eigenes Projekt bedeutet: eine Historientabelle, Trigger oder zusätzlichen Code bei jedem Update, und am Ende einen Verlauf, der erst ab seiner Einführung etwas zu erzählen hat. Bei Makani bestand die Arbeit darin, eine neue Sicht auf vorhandene Events zu schreiben. An den Event-Definitionen änderte sich keine einzige Zeile.
Was schiebe ich eigentlich vor mir her?
Ähnlich verhält es sich mit der zweiten Funktion, der Statistik. Sie beantwortet Fragen danach, wie jemand mit seinen To-dos arbeitet: wie viele davon pünktlich erledigt werden, wie lange ein To-do im Mittel offen bleibt, an welchen Wochentagen und zu welchen Uhrzeiten erledigt wird und ob der Berg offener To-dos über die Zeit wächst oder schrumpft. Dazu kommt eine Liste der Dauerbrenner, also der To-dos, die noch offen sind und schon mindestens fünfmal verschoben wurden.
Die Bilanz in Abbildung 3 stellt für jedes Quartal nebeneinander dar, wie viele To-dos angelegt, erledigt und verschoben wurden. In den Beispieldaten überragen die Verschiebungen die beiden anderen Werte deutlich, und das entspricht dem Verhältnis, das sich bei mir über die gesamte Zeit ergibt.
Die Statistik wertet die gesamte Geschichte aus, hier mit beispielhaft ausgewählten Dauerbrennern und der Bilanz pro Quartal (Abb. 3).
Für mein eigenes Konto fällt das Ergebnis lehrreich aus. Den 5320 Verschiebungen stehen 2009 Erledigungen gegenüber, und 1116 To-dos habe ich erledigt, ohne sie je zu verschieben. 924 To-dos habe ich mindestens einmal verschoben, 277 davon fünfmal oder öfter.
Von diesen 277 habe ich am Ende immerhin 248 doch erledigt: Verschieben heißt bei mir also meistens nicht, dass etwas liegen bleibt, sondern dass es später passiert. Wie viel später zeigt die Dauer bis zur Erledigung. To-dos, die ich nie verschoben habe, waren im Median nach etwa sieben Stunden erledigt, verschobene nach gut sechs Tagen. Das To-do mit den 123 Verschiebungen ist allerdings noch offen.
Auch die Statistik reicht bis zum ersten Tag zurück, und sie vergleicht jeden Zeitraum mit dem gleich langen davor, sofern die Geschichte so weit zurückreicht. Technisch ist sie wiederum nur eine neue Projektion, also eine weitere Auswertung, die die vorhandenen Events von vorn bis hinten durchgeht und daraus Zahlen ableitet. Nichts musste nachträglich erfasst, geschätzt oder rekonstruiert werden. Die Daten für eine Auswertung, an die im Mai 2024 niemand gedacht hatte, lagen seit Mai 2024 vollständig vor.
Entscheidungen, die sich vertagen lassen
Neubau, Verlauf und Statistik haben eines gemeinsam: Für keines davon musste die ursprüngliche Anwendung vorbereitet sein.
Darin liegt für mich der eigentliche Unterschied zwischen beiden Ansätzen. CRUD verlangt beim ersten Speichern eine Entscheidung darüber, was sich zu merken lohnt. Event-Sourcing erlaubt, diese Entscheidung aufzuschieben: welche Auswertung, welche Darstellung, sogar welches Programm.
In der vergangenen Woche ging es hier im zweiten Teil der Serie „Werkzeugwahl“ um Teams, die ihren Stack festlegen, bevor sie das Problem kennen. Ein Datenmodell, das nur den aktuellen Zustand speichert, ist eine verwandte, wenn auch subtilere Festlegung. Es bestimmt, welche Fragen sich später beantworten lassen, und zwar zu einem Zeitpunkt, an dem diese Fragen noch niemand gestellt hat. Wer am Anfang festlegt, dass das Fälligkeitsdatum eine Spalte ist, entscheidet damit stillschweigend, dass die Frage nach dem Aufschieben nie beantwortet werden kann. Diese Festlegung wiegt schwerer als die Wahl einer Sprache oder eines Frameworks, denn Code lässt sich neu schreiben, überschriebene Daten dagegen nicht.
Ein naheliegender Einwand lautet, dass sich das alles auch mit CRUD lösen ließe, wenn nur rechtzeitig jemand daran denkt, etwa mit einer Historientabelle, einem Änderungsprotokoll oder einer zusätzlichen Spalte. Das stimmt, setzt aber voraus, dass die Frage vorher bekannt ist. Bei Makani war sie das nicht. Weder der Verlauf noch die Statistik standen 2024 auf irgendeiner Liste, beide sind erst aus der Nutzung entstanden, nach zweieinhalb Jahren im Alltag.
Hinzu kommt, dass ein Änderungsprotokoll neben einer CRUD-Tabelle eine zweite Wahrheit ist, die mit der ersten übereinstimmen muss und das nicht immer tut. Beim Event-Sourcing gibt es nur eine: Die Events sind die Daten, und alles andere wird aus ihnen abgeleitet. Eine neue Auswertung kann deshalb nicht in Widerspruch zu einer alten geraten, sie blickt nur anders auf dieselben Fakten.
Wie weit das trägt, zeigt eine Frage, die mir erst beim Schreiben dieses Beitrags eingefallen ist: Behandle ich To-dos, deren Text ich nachträglich ändere, anders als die übrigen? Makani selbst beantwortet diese Frage nicht, die Events tun es trotzdem, nach wenigen Minuten Auswertung. Die 325 To-dos mit geändertem Text habe ich im Schnitt siebenmal verschoben, alle anderen nicht einmal zweimal.
Was sich nicht vertagen lässt
Ob ich unklare Aufgaben häufiger umformuliere oder lange liegende To-dos eher neu fasse, verraten die Events allerdings nicht. Damit ist die Grenze des Ansatzes erreicht, denn auch mit Event-Sourcing lässt sich nur die Auswertung aufschieben, nicht das Erfassen. Makani weiß, dass ein To-do 123 Mal verschoben wurde, aber kein einziges Mal, warum. Das Event hält das neue Datum fest und sonst nichts. Diese Frage bleibt unbeantwortet, egal wie viele Auswertungen noch dazukommen.
Ein Feld für den Grund ließe sich morgen ergänzen, und von da an wäre die Information vorhanden. Die 5320 Verschiebungen davor blieben trotzdem stumm. Dasselbe gilt für alles, was Makani gar nicht erst als Event kennt, etwa wie lange ich an einem To-do tatsächlich gearbeitet habe. Was die Anwendung nie als Event erfahren hat, lässt sich auch rückwirkend nicht auswerten.
Welche Fakten ein Event trägt, ist deshalb die eine Entscheidung, die sich nicht vertagen lässt, und sie verdient mehr Sorgfalt als jede Tabellenspalte. Die bessere Frage beim Entwurf eines Events lautet nicht, was die Anwendung heute anzeigen muss, sondern was in der Fachlichkeit tatsächlich geschehen ist.
Aufschieben mit gutem Gewissen
Als Nutzer von Makani schiebe ich mehr vor mir her, als mir lieb ist, und das weiß ich jetzt sehr genau. Als Entwickler habe ich daraus etwas anderes gelernt. Dass sich Verlauf und Statistik nach zweieinhalb Jahren rückwirkend einbauen ließen, verdankt sich keiner Weitsicht, denn die hatte niemand. Es verdankt sich einer einzigen Entscheidung am Anfang, nämlich Änderungen festzuhalten statt Zustände. Die nächste Auswertung, die mir einfällt, beginnt deshalb nicht mit einer Datenerhebung, sondern mit einer Projektion über zweieinhalb Jahre Geschichte.
Diese Entscheidung hat Makani durch einen vollständigen Neubau getragen und zwei Funktionen ermöglicht, an die beim ersten Speichern niemand gedacht hatte. Event-Sourcing ist damit vor allem eine Absicherung gegen die Grenzen der eigenen Voraussicht. Welche Fragen eine Anwendung in drei Jahren beantworten soll, weiß heute niemand. Mit Event-Sourcing muss das auch niemand wissen.
(mro)