Sofortantwort
CQRS einfach erklärt
Trennung von Lese- und Schreibmodellen für gezielte Optimierung.
- Kurz gesagt
- Trennt Commands (Schreiben) von Queries (Lesen)
- Typischer Einsatz
- High-Read-Systeme, Komplexe Domains, Reporting
- Wichtig zu wissen
- Oft kombiniert mit Event Sourcing
CQRS im Überblick
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 |
Technisch betrachtet
Commands und Queries
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
)
Synchronisation Write → Read
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())
Read-Modell optimieren
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
)
Eventual Consistency
Timeline:
─────────────────────────────────────────────────→
│ │ │
▼ ▼ ▼
Write Event Read Updated
Published
└────── mögliche Inconsistency Window ──────┘
Umgang damit:
- UI zeigt optimistisch den erwarteten Zustand
- Polling oder WebSocket für Updates
- Für kritische Reads: Direkt aus Write-DB lesen
Wann CQRS?
| 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 |
Schritt für Schritt
CQRS nur dort einsetzen, wo die Trennung Nutzen stiftet
CQRS lohnt sich, wenn Schreiben und Lesen wirklich unterschiedliche Anforderungen haben. Die zusätzliche Komplexität muss sichtbar gerechtfertigt sein.
Read- und Write-Bedarf vergleichen
01
Prüfe, ob Fachregeln, Skalierung, Datenform oder Zugriffsprofile deutlich voneinander abweichen.
Write: Regeln & Konsistenz | Read: Ansicht & GeschwindigkeitCommands fachlich definieren
02
Ein Command beschreibt eine beabsichtigte Änderung; seine Validierung und mögliche Ablehnung gehören zur Schreibseite.
Command → Validierung → Zustandsänderung / EventRead-Modelle für echte Ansichten bauen
03
Die Leseseite wird auf konkrete Oberflächen, Reports oder APIs zugeschnitten statt ein Datenbankspiegel zu sein.
Ereignis / Änderung → Projektion → passende ViewAktualität transparent machen
04
Wenn Ansichten asynchron aktualisiert werden, braucht die Produktlogik eine verständliche Regel für Zwischenzustände.
Write bestätigt → View wird aktualisiert → Status sichtbarBetrieb und Wiederaufbau testen
05
Monitoring, Fehlertoleranz und Rebuilds der Leseansichten werden als Teil der Architektur geprüft.
Lag → Alert → Reprocess → konsistente View
Konkretes Beispiel
Beispiel: Auftragsübersicht mit hoher Leselast
Mitarbeitende lesen ständig Statusansichten, während Änderungen strengen Freigabe- und Fachregeln unterliegen.
Unterschiedliche Anforderungen
Die Schreibseite muss valide Auftragsänderungen garantieren; das Dashboard braucht schnell filterbare, verständliche Übersichten.
Getrennte Modelle
Commands und Fachlogik schützen die Änderungen; eine Projektion baut eine optimierte Auftragsansicht mit einem klaren Aktualitätsstatus.
CQRS trennt Verantwortlichkeiten, nicht automatisch Teams oder Infrastruktur – die Trennung sollte dem Fachproblem folgen.
Einordnung
Vorteile und Grenzen
Das spricht dafür
- Gezielte Optimierung: Fachregeln beim Schreiben und nutzernahe Ansichten beim Lesen können unabhängig gestaltet werden.
- Klare Verantwortlichkeit: Änderungen und Abfragen haben unterschiedliche, leichter erklärbare Pfade.
- Skalierbare Ansichten: Read-Modelle lassen sich auf bestimmte Zugriffs- und Reporting-Bedürfnisse zuschneiden.
Das solltest du beachten
- Mehr bewegliche Teile: Synchronisation, Monitoring und Fehlerbehandlung erhöhen Architektur- und Betriebsaufwand.
- Zwischenzustände möglich: Asynchrone Ansichten können nach einer Änderung kurz hinterherhinken.
- Zu viel für Einfaches: Bei überschaubaren CRUD-Anwendungen ist ein gemeinsames Modell meist leichter zu warten.
Vertiefung · für FortgeschritteneDie getrennten Pfade von CQRS
Die Schreibseite schützt Fachregeln, die Leseseite liefert für Nutzer optimierte Datenansichten.
Eine beabsichtigte Zustandsänderung wird vor ihrer Annahme geprüft.