Event Sourcing vs. klassisches CRUD
A klassische CRUD-Tabelle sogt dir den aktuellen Zustand von ana Zeiln und nix darüber, wia sie dorthin kummen is. Event Sourcing dreht des um: Es speichert jede Änderung, de jemois passiert is, und der aktuelle Zustand wird zu wos, des du berechnst, ned speicherst. Diese Umkehrung löst echte Probleme und schafft a poar neiche - es lohnt si, beides zu vastehn, bevor ma danach greift.
CRUD: Zustand vor Ort, Historie nirgends
A normales Update auf a orders-Tabelle überschreibt die Zeiln:
UPDATE orders SET status = 'shipped', shipped_at = now() WHERE id = 4821;
Nachdem des gelaufen is, is der vorherige Status weg, außer irgendwos ondres - a Audit-Log-Tabelle, Logging auf Anwendungsebene, a Trigger - hot ihn separat festghoitn, ois Nachgedanke aufs eigentliche Datenmodell draufgschraubt. Wenn a Kunde fragt "warum zeigt meine Bestellung ois versandt an, obwohl i nie a Versandbenachrichtigung kriagt hob", und die Antwort verlangt zu wissn, dass sie wegn ana Bug vor drei Tagen kurz auf shipped und wieda zruck gsprungen is, hot a CRUD-Tabelle diese Information einfach nimma.
Event Sourcing: Des Log is die Wahrheitsquelle
Statt den aktuellen Zustand zu speichern, speicherst a Append-only-Folge von Events, und der aktuelle Zustand wird durch Nachspielen abgleitet:
// events, in order, for order 4821
[
{ type: 'OrderPlaced', orderId: 4821, items: [...], at: '2026-03-20T10:00:00Z' },
{ type: 'PaymentReceived', orderId: 4821, amount: 4999, at: '2026-03-20T10:01:12Z' },
{ type: 'OrderShipped', orderId: 4821, carrier: 'ups', at: '2026-03-22T14:30:00Z' },
]
Der aktuelle Zustand is a Fold über diese Liste:
function projectOrder(events) {
return events.reduce((state, event) => {
switch (event.type) {
case 'OrderPlaced':
return { ...state, id: event.orderId, items: event.items, status: 'placed' };
case 'PaymentReceived':
return { ...state, status: 'paid', paidAmount: event.amount };
case 'OrderShipped':
return { ...state, status: 'shipped', carrier: event.carrier };
default:
return state;
}
}, {});
}
const currentOrder = projectOrder(eventsForOrder(4821));
Nix wird do glöscht oda überschrieben. Die vollständige Historie - jeden Zustand, den die Bestellung durchlaufen hot, und genau wann - steckt im Modell drin, ned in am separaten System, das du zum Pflegen ned vergessn darfst.
Wos du tatsächlich gewinnst
- A echter Audit-Trail, gratis. "Wos is passiert, und in wöcher Reihenfolge" is die Primärdaten, ka abgleitetes Log, des du mit da "echten" Tabelle synchron hoitn muasst.
- Zeitreisen. Du kannst den Zustand zu jedem Zeitpunkt in da Vergangenheit projiziern, indem du Events nur bis zu dem Zeitstempel nochspielst - wirklich nützlich zum Debuggen ("wia hot des ausgschaut, kurz bevor der Bug-Report einkummen is") und für Compliance-Anforderungen, de genau diese Frage stöllen.
- Neiche Lesemodelle, ohne die Schreiblogik anzufassn. Du brauchst a neiche Sicht auf die Daten - Gesamtumsatz pro Tag, Bestellungen pro Spediteur - für a Dashboard, des es beim Bau vom System no ned gegeben hot? Schreib a neiche Projektionsfunktion über den bestehenden Event-Strom. Die Events söba ändern si nie.
- Entkoppelte Schreib- und Lesepfade. Die Events sand das Schreibmodell; Projektionen sand Lesemodelle, de daraus gebaut wern, oft asynchron. Des führt natürlich zu CQRS (Command Query Responsibility Segregation), wo Schreibvorgänge und Lesezugriffe unabhängig skalieren und si weiterentwickeln kinnan.
Wos es tatsächlich kost
- Jede Query braucht a Projektion. "Wos is der aktuelle Status von Bestellung 4821" is nimma a Zeilen-Lookup - es is das Nachspielen von so vül Events, wia die Bestellung angesammelt hot, außer du cachst den projizierten Zustand (an "Snapshot") und spielst nur Events seit dem letztn Snapshot nach.
// snapshot every 50 events instead of replaying from the start each time
function projectWithSnapshot(orderId, snapshot, eventsSinceSnapshot) {
return eventsSinceSnapshot.reduce(applyEvent, snapshot ?? initialState());
}
- Schema-Evolution is schwerer, ned leichter. Du kannst nie ändern, wos a alter Event bedeutet hot - Code, der heit gschrieben wird, muass an
OrderPlaced-Event von vor drei Joar immer no korrekt interpretiern, in da Form, de er damals hatte. An neichn Event-Typ hinzuzufügen is einfach; die Struktur von am bestehenden zu ändern heißt, ihn zu versionieren (OrderPlacedV2) und den oidn Handler unbegrenzt mitzuschleppen. - Löschen is absichtlich umständlich. "Lösch die Daten von dem Kunden" (a echte Anforderung unter da DSGVO) steht direkt gegn "Events sand unveränderlich und werdn nie glöscht". Echte Systeme lösen des mit Crypto-Shredding (personenbezogene Felder mit am Pro-Kunden-Schlüssel verschlüsseln, dann den Schlüssel löschn, um die Daten unwiederbringlich zu machen), statt Events tatsächlich aus dem Log zu entfernen.
- Debuggen verlangt, in Sequenzen zu denken, ned in Snapshots. "Wos is mit dera Zeiln falsch" wird zu "wos is mit dera Sequenz von 40 Events und der Projektionslogik falsch, de sie faltet" - a wirklich ondere Debugging-Denkweise, an die si a Team erst gewöhnen muass.
Wo es sich lohnt
Event Sourcing lohnt si, wo die Historie für sich echten Geschäftswert hot - Finanz-Hauptbücher, Bestell-Lebenszyklen, olles mit Audit- oda Compliance-Anforderungen, oda Systeme, wo "wos hot si gändert und warum" a erstklassige Frage is, de Nutzer wirklich stölln. A users-Tabelle, de display_name und avatar_url trackt, braucht des fast nie; niemand fragt nach dem Audit-Trail vom Profilbild von wem.
A Mittelweg: Outbox-/Audit-Tabellen ohne vollständiges Event Sourcing
Du musst ned alles oda nix wählen. A gängiges praktisches Muster is a normale CRUD-Tabelle für den aktuellen Zustand, plus a Append-only-Tabelle order_events, de in derselben Transaktion wia jeder Schreibvorgang befüllt wird:
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 4821;
INSERT INTO order_events (order_id, type, payload, at)
VALUES (4821, 'OrderShipped', '{"carrier":"ups"}', now());
COMMIT;
Damit kriagst den Audit-Trail und die Zeitreise-fürs-Debuggen-Vorteile, ohne jeden Lesezugriff zu ana Wiederholung zu machn - der aktuelle Zustand bleibt a direkter Zeilen-Lookup. Du verlierst die "Lesemodelle sand einfach neiche Projektionen"-Flexibilität, aber für die meisten Anwendungen is des a fairer Tausch gegn die Einfachheit zum Bauen und Durchdenkn.
Fazit
CRUD speichert, wo du bist; Event Sourcing speichert, wia du hinkemman bist, und leitet davon ab, wo du bist. Audit-Trail, Zeitreisen und flexible Lesemodelle sand echt und wertvoll - aber sie kommen mit schwererer Schema-Evolution, umständlichem Löschen und ana grundlegend ondern Debugging-Denkweise. Greif zu vollem Event Sourcing, wenn die Historie der Sinn des Systems is, ned nur a nice-to-have; sonst bringt a Audit-Tabelle neben normalem CRUD meist den Großteil vom Nutzen für a Bruchteil der Komplexität.