Event Sourcing
Ein Architektur-Pattern, bei dem Zustand nicht nur als aktueller Snapshot gespeichert wird, sondern aus einer Sequenz von Events rekonstruiert werden kann.
Ein Architektur-Pattern, das Lese- und Schreiboperationen trennt – unterschiedliche Modelle für Commands (Änderungen) und Queries (Abfragen).
CQRS trennt deine Anwendung in zwei Teile: Einen für Änderungen (Commands) und einen für Abfragen (Queries). Jeder Teil kann unabhängig optimiert werden.
Traditionell (ein Modell für alles):
┌─────────────────────────────────┐
│ Application │
│ ┌─────────────────────────┐ │
│ │ Single Model │ │
│ │ (Read + Write) │ │
│ └─────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────┐ │
│ │ Database │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
CQRS (getrennte Modelle):
┌─────────────────────────────────────────────┐
│ Application │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Commands │ │ Queries │ │
│ │ (Write) │ │ (Read) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Write DB │ ──→ │ Read DB │ │
│ │ (normalized)│ │(denormalized)│ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────┘
Warum trennen?
| Aspekt | Write-Seite | Read-Seite |
|---|---|---|
| Optimierung | Konsistenz, Validierung | schnelle und passende Abfragen |
| Schema | oft stärker normalisiert | oft denormalisiert oder view-orientiert |
| Skalierung | abhängig von Schreiblast | abhängig von Leselast |
| Komplexität | Business-Logik | UI-, Reporting- oder API-nahe Views |
Command (ändert Zustand):
@dataclass
class CreateOrderCommand:
customer_id: str
items: List[OrderItem]
@dataclass
class AddItemCommand:
order_id: str
sku: str
quantity: int
class CommandHandler:
def handle_create_order(self, cmd: CreateOrderCommand):
order = Order.create(cmd.customer_id, cmd.items)
self.repository.save(order)
# Kein Return-Wert (oder nur ID)
Query (liest Zustand):
@dataclass
class GetOrderQuery:
order_id: str
@dataclass
class ListOrdersQuery:
customer_id: str
status: Optional[str] = None
class QueryHandler:
def handle_get_order(self, query: GetOrderQuery) -> OrderDTO:
return self.read_db.get_order(query.order_id)
def handle_list_orders(self, query: ListOrdersQuery) -> List[OrderDTO]:
return self.read_db.find_orders(
customer_id=query.customer_id,
status=query.status
)
Option 1: Synchron (einfacher, aber stärker gekoppelt):
class OrderService:
def create_order(self, cmd: CreateOrderCommand):
# Write
order = Order.create(cmd.customer_id, cmd.items)
self.write_db.save(order)
# Sync to Read (im selben Request)
self.read_db.upsert_order_view(order.to_view())
Option 2: Asynchron via Events (entkoppelter, aber eventual consistent):
class OrderService:
def create_order(self, cmd: CreateOrderCommand):
order = Order.create(cmd.customer_id, cmd.items)
self.write_db.save(order)
# Event publizieren
self.event_bus.publish(OrderCreatedEvent(order))
class OrderViewUpdater:
@subscribe(OrderCreatedEvent)
def handle(self, event: OrderCreatedEvent):
# Read-Modell asynchron aktualisieren
self.read_db.upsert_order_view(event.order.to_view())
Write-Modell (normalisiert):
-- Mehrere Tabellen, Joins nötig
orders (id, customer_id, status, created_at)
order_items (id, order_id, product_id, quantity, price)
products (id, name, description)
customers (id, name, email)
Read-Modell (denormalisiert):
-- Eine Tabelle, alles drin
order_views (
id,
customer_name,
customer_email,
status,
items_json, -- [{name, quantity, price}, ...]
total,
created_at
)
Timeline:
─────────────────────────────────────────────────→
│ │ │
▼ ▼ ▼
Write Event Read Updated
Published
└────── mögliche Inconsistency Window ──────┘
Umgang damit:
| Szenario | CQRS sinnvoll? |
|---|---|
| Einfaches CRUD | meist nicht nötig |
| Deutlich andere Read-/Write-Last | kann separate Skalierung ermöglichen |
| Komplexe Reports | kann denormalisierte Views vereinfachen |
| Event Sourcing | häufig passende Ergänzung |
| Strenge Echtzeit- oder Konsistenzanforderungen | Eventual Consistency sorgfältig prüfen |
CQRS ist wie ein Restaurant mit getrennter Küche und Ausgabe: Die Küche (Write) ist optimiert für Zubereitung, die Ausgabe (Read) für schnelles Servieren. Beide haben unterschiedliche Anforderungen und Optimierungen.
Trennt Commands (Schreiben) von Queries (Lesen)
Ermöglicht unterschiedliche Optimierungen für Read und Write
Oft kombiniert mit Event Sourcing
High-Read-Systeme
Viele Leser, wenige Schreiber – Read-Seite separat skalieren
Komplexe Domains
Write-Modell für Business-Logik, Read-Modell für UI
Reporting
Denormalisierte Views für schnelle Reports
Event Sourcing
Events schreiben, Projections für Queries
Bei stark unterschiedlichen Read/Write-Anforderungen, komplexer Domain-Logik, Reporting-Bedarf oder gezielter Skalierung. Für einfache CRUD-Apps ist der zusätzliche Aufwand meist nicht gerechtfertigt.
Nein, aber sie passen gut zusammen. CQRS trennt Read/Write. Event Sourcing speichert Events statt Zustand. Man kann CQRS ohne Event Sourcing nutzen und umgekehrt.
Read-Modelle können asynchron aktualisiert werden. Nach einem Write kann das Read-Modell daher kurzzeitig veraltet sein. Ob das akzeptabel ist, hängt vom Use Case und Konsistenzbedarf ab.
Ja, meist. Zwei Modelle, Synchronisation und mögliche Eventual Consistency erhöhen Architektur- und Betriebsaufwand. CQRS sollte nur eingesetzt werden, wenn die Vorteile diesen Aufwand rechtfertigen.