Event Sourcing
Ein Architektur-Pattern, bei dem der Zustand einer Anwendung nicht direkt gespeichert wird, sondern aus einer Sequenz von Events rekonstruiert wird.
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 | Geschwindigkeit |
| Schema | Normalisiert | Denormalisiert |
| Skalierung | Weniger Last | Mehr Last |
| Komplexität | Business-Logik | Einfache Abfragen |
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 (einfach, aber 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 (entkoppelt):
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
(t=0) Published (t=100ms)
(t=10ms)
└────── Inconsistency Window ──────┘
Umgang damit:
| Szenario | CQRS sinnvoll? |
|---|---|
| Einfaches CRUD | ❌ Overkill |
| Read >> Write (10:1+) | ✅ Read separat skalieren |
| Komplexe Reports | ✅ Denormalisierte Views |
| Event Sourcing | ✅ Natürliche Ergänzung |
| Echtzeit-Anforderungen | ⚠️ Eventual Consistency beachten |
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, hohen Skalierungsanforderungen. Für einfache CRUD-Apps ist es Overkill.
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 werden asynchron aktualisiert. Nach einem Write ist das Read-Modell kurzzeitig veraltet. Für viele Anwendungen akzeptabel, für manche nicht.
Ja, deutlich. Zwei Modelle, Synchronisation, Eventual Consistency. Nur einsetzen wenn die Vorteile die Komplexität rechtfertigen.