45 Unterschied zu klassischen Write Models

45.1 CRUD vs. Event-based Persistence

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.

45.1.1 Das klassische CRUD-Modell

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';

45.1.2 Event-based Persistence: Die Geschichte bewahren

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?

45.2 State Reconstruction

45.2.1 Wie entsteht der aktuelle Zustand?

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 order

45.2.2 Zeitreisen möglich

Angenommen 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);
}

45.3 Performance Implications

45.3.1 Die Trade-offs verstehen

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

45.3.2 Optimierungsstrategien

Wie würden Sie das Performance-Problem beim Lesen lösen? Event Sourcing nutzt mehrere Ansätze:

45.3.2.1 1. Snapshots für häufig abgefragte Zustände

@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;
    }
}

45.3.2.2 2. Materialized Views (Read Models)

# 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

45.3.3 Wann macht Event Sourcing Sinn?

Überlegen Sie: In welchen Szenarien würden die Vorteile von Event Sourcing die Performance-Kosten rechtfertigen?

45.3.4 Hybrid-Ansätze

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.