Warum CQRS und Event Sourcing kein „Overkill“, sondern eine Rettung für komplexe Systeme sind
\n\nHaben Sie jemals die Situation erlebt, dass Ihr in Rust geschriebenes System Leseabfragen perfekt verarbeitet, aber beim Schreiben „in die Knie geht“? Oder umgekehrt – Sie opfern die Leseleistung, um die vollständige Änderungshistorie speichern zu können? Kommt Ihnen das bekannt vor? Dann willkommen in der Welt von CQRS und Event Sourcing.
\n\nViele Entwickler betrachten diese Muster als „schweres Geschütz“, das nur in Enterprise-Projekten mit C# oder Java anwendbar ist. Aber mit der wachsenden Beliebtheit von Rust in hochbelasteten Systemen (von Spieleservern bis zu Finanzplattformen) werden CQRS und Event Sourcing nicht nur nützlich – sie werden notwendig. Rust mit seinen kostenlosen Abstraktionen und strengen Typprüfungen ist die ideale Sprache für die Implementierung dieser Muster.
\n\nIn diesem Artikel werden wir nicht nur über die Theorie sprechen. Wir werden funktionierenden Code in Rust schreiben, den Sie an Ihre Projekte anpassen können. Sie erfahren, wie Sie die Verantwortung zwischen Befehlen und Abfragen aufteilen, wie Sie Ereignisse anstelle des aktuellen Zustands speichern und wie all dies Ihr System um ein Vielfaches beschleunigt.
\n\nWas sind CQRS und Event Sourcing? Einfach erklärt für Rust-Entwickler
\n\nStellen Sie sich vor, Sie verwalten ein Bankkonto. Im traditionellen CRUD-Modell haben Sie eine Tabelle „Konten“, in der der aktuelle Saldo gespeichert ist. Wenn Sie den Verlauf einsehen möchten, erstellen Sie eine weitere Tabelle „Transaktionen“. Aber was ist, wenn Sie nicht nur den Saldo kennen, sondern verstehen müssen, warum er sich geändert hat? Oder den Zustand zu einem beliebigen Datum wiederherstellen möchten?
\n\nCQRS: Teile und herrsche
\n\nCQRS (Command Query Responsibility Segregation) ist ein Prinzip, das Operationen in zwei Gruppen unterteilt:
\n- \n
- Befehle (Commands) – ändern den Zustand. Sie geben keine Daten zurück, nur eine Bestätigung oder einen Fehler. \n
- Abfragen (Queries) – lesen nur Daten. Sie ändern den Zustand nicht. \n
In Rust wirkt diese Trennung natürlich: Die Typen für Befehle und Abfragen sind unterschiedliche Strukturen, und ihre Handler sind unterschiedliche Funktionen oder Module. Dies ist nicht nur eine „gute Praxis“, sondern eine Möglichkeit, versehentliche Mutationen beim Lesen zu vermeiden.
\n\n// Befehl – ändert den Zustand\npub struct CreateOrder {\n pub user_id: Uuid,\n pub items: Vec<OrderItem>,\n}\n\n// Abfrage – liest nur\npub struct GetOrderHistory {\n pub user_id: Uuid,\n pub limit: usize,\n}\n\nEvent Sourcing: Speichern Sie die Geschichte, nicht die Momentaufnahme
\n\nEvent Sourcing ist ein Ansatz, bei dem wir nicht den aktuellen Zustand eines Objekts speichern, sondern die Abfolge von Ereignissen, die zu diesem Zustand geführt haben. Möchten Sie den Kontostand zum 1. Januar 2023 wissen? „Spielen“ Sie einfach alle Ereignisse von Anfang bis zu diesem Datum ab.
\n\nDie Vorteile liegen auf der Hand:
\n- \n
- Prüfung (Audit) – jede Änderung ist dokumentiert. \n
- Zeitliche Schnappschüsse – jeder Zustand kann wiederhergestellt werden. \n
- Fehlertoleranz – wenn ein Fehler im Code vorliegt, haben Sie die Daten nicht verloren, sondern sie nur falsch „abgespielt“. \n
Praktisches Beispiel: Auftragsverwaltungssystem in Rust
\n\nLassen Sie uns ein Minisystem für einen Online-Shop erstellen. Es wird Befehle (Auftragserstellung, Artikel hinzufügen) und Abfragen (Warenkorb abrufen, Auftragshistorie) verarbeiten. Die gesamte Logik basiert auf Ereignissen.
\n\nSchritt 1: Ereignisse und Befehle definieren
\n\nDas Erste, was zu tun ist, ist die Domäne zu entwerfen. In Rust verwenden wir Aufzählungen (enum) für Ereignisse und strenge Strukturen für Befehle.
use serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\n// Ereignisse – unveränderliche Fakten\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum OrderEvent {\n Created { order_id: Uuid, user_id: Uuid, timestamp: i64 },\n ItemAdded { order_id: Uuid, item: Str