Wenn gewöhnliches CRUD nicht mehr ausreicht: Einführung in CQRS und Event Sourcing
\nStellen Sie sich vor, Sie bauen ein Ticketbuchungssystem. Alles ist einfach, solange es zehn Benutzer gibt. Aber wenn es tausend werden und dann eine Million, treten Probleme auf: Datenbanken werden blockiert, Abfragen geraten in Konflikt, und der Änderungsverlauf kann nicht wiederhergestellt werden. Eine vertraute Situation?
\nIn der Welt der Go-Entwicklung, wo Leistung und Parallelität das A und O sind, helfen zwei Architekturmuster, diese Probleme zu lösen: CQRS (Command Query Responsibility Segregation) und Event Sourcing. Heute werden wir sie in der Praxis durchgehen – mit Code, Beispielen und realen Anwendungsszenarien.
\n\nWas ist CQRS in einfachen Worten?
\nCQRS ist die Trennung der Verantwortlichkeiten zwischen Befehlen (Schreiben) und Abfragen (Lesen). Im klassischen CRUD ist ein Datenmodell sowohl für das Erstellen als auch für das Lesen zuständig. In CQRS erstellen wir zwei separate Modelle:
\n- \n
- Command-Modell – zuständig für Zustandsänderungen (Erstellen, Aktualisieren, Löschen) \n
- Query-Modell – zuständig nur für das Lesen von Daten \n
Dies ermöglicht es, jede Seite unabhängig zu optimieren: Das Schreiben kann streng validiert sein, das Lesen denormalisiert und schnell. In Go ist dies dank Goroutinen und Kanälen besonders effektiv.
\n\nEinfaches CQRS-Beispiel in Go
\nLassen Sie uns ein einfaches System zur Auftragsverwaltung erstellen. Beginnen wir mit den Befehlen:
\n\ntype CreateOrderCommand struct {\n OrderID string\n UserID string\n ProductID string\n Amount float64\n}\n\ntype CommandHandler struct {\n repo *OrderRepository\n}\n\nfunc (h *CommandHandler) HandleCreateOrder(ctx context.Context, cmd CreateOrderCommand) error {\n order := Order{\n ID: cmd.OrderID,\n UserID: cmd.UserID,\n ProductID: cmd.ProductID,\n Amount: cmd.Amount,\n Status: "pending",\n CreatedAt: time.Now(),\n }\n return h.repo.Save(ctx, order)\n}\n\nJetzt erstellen wir ein separates Modell für das Lesen:
\n\ntype OrderQuery struct {\n repo *ReadRepository\n}\n\ntype OrderView struct {\n OrderID string\n UserName string\n ProductName string\n Total float64\n Status string\n}\n\nfunc (q *OrderQuery) GetOrderSummary(ctx context.Context, orderID string) (*OrderView, error) {\n return q.repo.GetOrderView(ctx, orderID)\n}\n\nBeachten Sie: In OrderView sind bereits denormalisierte Daten (Benutzername, Produktname) enthalten, die aus verschiedenen Quellen zusammengestellt werden. Dies beschleunigt das Lesen um ein Vielfaches.
Event Sourcing: Nicht den Zustand, sondern Ereignisse aufzeichnen
\nEvent Sourcing ändert den Ansatz zur Datenspeicherung: Anstatt den aktuellen Zustand eines Objekts zu speichern, speichern wir die Abfolge von Ereignissen, die zu diesem Zustand geführt haben. Dies verleiht eine Superkraft: den vollständigen Änderungsverlauf und die Möglichkeit, den Zustand „zurückzuspulen“.
\n\nImplementierung von Event Sourcing in Go
\nBeginnen wir mit den grundlegenden Ereignistypen:
\n\ntype Event struct {\n ID string\n AggregateID string\n Type string\n Data []byte\n Timestamp time.Time\n Version int\n}\n\ntype EventStore interface {\n Save(ctx context.Context, events []Event) error\n Load(ctx context.Context, aggregateID string) ([]Event, error)\n}\n\nJetzt implementieren wir das Aggregat – ein Objekt, das seinen Zustand aus Ereignissen wiederherstellt:
\n\ntype OrderAggregate struct {\n ID string\n Status string\n Items []OrderItem\n Version int\n}\n\nfunc (a *OrderAggregate) Apply(event Event) {\n switch event.Type {\n case "OrderCreated":\n var data OrderCreatedData\n json.Unmarshal(event.Data, &data)\n a.ID = data.OrderID\n a.Status = "created"\n case "OrderConfirmed":\n a.Status = "confirmed"\n case "OrderShipped":\n a.Stat