Bevor wir in die Implementierung von Event Sourcing eintauchen: Wie speichern Sie normalerweise den aktuellen Zustand einer Bestellung in Ihrem System? Die meisten Entwickler würden spontan an eine Datenbanktabelle denken – und genau hier liegt der fundamentale Unterschied zu Event Sourcing.
In traditionellen Systemen modellieren wir Geschäftsobjekte als aktuelle Zustände. Eine Bestellung existiert als Datensatz mit dem neuesten Status:
-- Klassische Order-Tabelle
CREATE TABLE orders (
id UUID PRIMARY KEY,
customer_id UUID,
status VARCHAR(20), -- 'placed', 'paid', 'shipped', 'delivered'
total_amount DECIMAL(10,2),
created_at TIMESTAMP,
last_modified TIMESTAMP
);Was passiert, wenn sich der Status einer Bestellung ändert? Der alte Zustand wird überschrieben:
-- UPDATE überschreibt den alten Zustand
UPDATE orders
SET status = 'paid', last_modified = NOW()
WHERE id = 'order-123';Event Sourcing kehrt diesen Ansatz um. Statt den aktuellen Zustand zu speichern, bewahren wir die vollständige Geschichte aller Änderungen:
| CRUD-Denkweise | Event Sourcing-Denkweise |
|---|---|
| “Eine Bestellung ist bezahlt” | “Eine Bestellung wurde bezahlt” |
| Aktueller Zustand ist wichtig | Veränderungshistorie ist wichtig |
| Überschreibung von Daten | Anhängen von Events |
// Event Store statt Zustandstabelle
public class OrderEvent {
private UUID eventId;
private UUID orderId;
private String eventType; // OrderPlaced, PaymentReceived, etc.
private LocalDateTime timestamp;
private String eventData; // JSON mit spezifischen Daten
}Denken Sie einen Moment nach: Welche Informationen gehen in einem CRUD-System verloren, die in Event Sourcing erhalten bleiben?
In Event Sourcing rekonstruieren wir den aktuellen Zustand durch Replay aller Events:
// Spring Boot: Zustand aus Events rekonstruieren
@Service
public class OrderProjectionService {
public OrderView buildCurrentState(UUID orderId) {
List<OrderEvent> events = eventStore.getEventsForOrder(orderId);
OrderView order = new OrderView();
for (OrderEvent event : events) {
order = applyEvent(order, event);
}
return order;
}
private OrderView applyEvent(OrderView order, OrderEvent event) {
switch (event.getEventType()) {
case "OrderPlaced":
return order.withStatus("placed")
.withAmount(event.getTotalAmount());
case "PaymentReceived":
return order.withStatus("paid");
case "OrderShipped":
return order.withStatus("shipped")
.withShippingDate(event.getTimestamp());
default:
return order;
}
}
}# Python: Event Replay
class OrderProjection:
def __init__(self):
self.event_store = EventStore()
def build_current_state(self, order_id):
events = self.event_store.get_events_for_order(order_id)
order_state = OrderView()
for event in events:
order_state = self._apply_event(order_state, event)
return order_state
def _apply_event(self, order, event):
if event.event_type == "OrderPlaced":
return order.with_status("placed").with_amount(event.data["amount"])
elif event.event_type == "PaymentReceived":
return order.with_status("paid")
elif event.event_type == "OrderShipped":
return order.with_status("shipped").with_shipping_date(event.timestamp)
return orderAngenommen Sie müssten herausfinden, wie eine Bestellung am 15. Januar um 14:30 Uhr aussah. In CRUD-Systemen: unmöglich. In Event Sourcing: einfach den Replay bis zu diesem Zeitpunkt laufen lassen.
// Zustand zu einem bestimmten Zeitpunkt
public OrderView getStateAtTime(UUID orderId, LocalDateTime pointInTime) {
List<OrderEvent> events = eventStore.getEventsForOrder(orderId)
.stream()
.filter(event -> event.getTimestamp().isBefore(pointInTime))
.collect(toList());
return buildStateFromEvents(events);
}Event Sourcing bringt sowohl Vorteile als auch Herausforderungen mit sich:
| Aspekt | CRUD | Event Sourcing |
|---|---|---|
| Write Performance | Schnell (ein UPDATE) | Schnell (ein INSERT) |
| Read Performance | Sehr schnell (direkter Zugriff) | Langsam ohne Optimierung |
| Speicherverbrauch | Minimal (nur aktueller Zustand) | Hoch (alle Events) |
| Komplexität | Niedrig | Mittel bis Hoch |
Wie würden Sie das Performance-Problem beim Lesen lösen? Event Sourcing nutzt mehrere Ansätze:
@Service
public class OrderSnapshotService {
// Snapshot alle 100 Events erstellen
@EventListener
public void handleOrderEvent(OrderEvent event) {
long eventCount = eventStore.getEventCount(event.getOrderId());
if (eventCount % 100 == 0) {
OrderView currentState = projectionService.buildCurrentState(event.getOrderId());
snapshotStore.saveSnapshot(event.getOrderId(), currentState, eventCount);
}
}
// Schnelle Zustandsrekonstruktion mit Snapshot
public OrderView buildStateFromSnapshot(UUID orderId) {
Snapshot snapshot = snapshotStore.getLatestSnapshot(orderId);
List<OrderEvent> eventsAfterSnapshot = eventStore.getEventsAfter(orderId, snapshot.getEventNumber());
OrderView state = snapshot.getState();
for (OrderEvent event : eventsAfterSnapshot) {
state = applyEvent(state, event);
}
return state;
}
}# Asynchrone Aktualisierung von Read Models
class OrderReadModelUpdater:
def __init__(self):
self.read_model_store = ReadModelStore()
async def handle_order_event(self, event):
# Read Model sofort aktualisieren
current_view = await self.read_model_store.get_order_view(event.order_id)
updated_view = self._apply_event_to_view(current_view, event)
await self.read_model_store.save_order_view(event.order_id, updated_view)
def _apply_event_to_view(self, view, event):
# Gleiche Logik wie bei Event Replay, aber auf materialisierte View
if event.event_type == "OrderPlaced":
return view.with_status("placed")
# ... weitere Event-BehandlungÜberlegen Sie: In welchen Szenarien würden die Vorteile von Event Sourcing die Performance-Kosten rechtfertigen?
Viele Systeme kombinieren beide Ansätze:
// Hybrid: Aktueller Zustand + Event History
@Entity
public class Order {
private UUID id;
private String currentStatus; // Für schnelle Abfragen
private BigDecimal totalAmount;
// Zusätzlich: Event History für Audit
@OneToMany(mappedBy = "orderId")
private List<OrderEvent> eventHistory;
}Welche Teile Ihres Systems würden von Event Sourcing profitieren, und welche sollten bei CRUD bleiben? Diese Entscheidung hängt von den spezifischen Anforderungen ab – nicht jedes Domain-Objekt muss event-sourced sein.
Die Kunst liegt darin, Event Sourcing gezielt dort einzusetzen, wo die Vorteile die Komplexität rechtfertigen. Im nächsten Abschnitt schauen wir uns an, wie Projections und Snapshots diese Komplexität handhabbar machen.