44 Event Sourcing: Prinzip, Vorteile, Herausforderungen

Event Sourcing stellt einen fundamentalen Paradigmenwechsel in der Art dar, wie wir Daten in Systemen persistieren und verwalten. Statt den aktuellen Zustand zu speichern, werden alle Änderungen als unveränderliche Events aufgezeichnet – der aktuelle Zustand ergibt sich durch Replay dieser Events.

44.1 Das Grundprinzip von Event Sourcing

Event Sourcing bedeutet, dass jede Änderung an einem Geschäftsobjekt als Event gespeichert wird. Diese Events bilden eine chronologische Abfolge aller Aktivitäten und ermöglichen es, jeden beliebigen Zustand in der Vergangenheit zu rekonstruieren.

Betrachten wir eine Bestellung in unserem E-Commerce-System:

// Spring Boot: Traditionelle Speicherung
@Entity
public class Order {
    private String orderId;
    private OrderStatus status;
    private BigDecimal totalAmount;
    private LocalDateTime lastModified;
    // Zustand wird überschrieben
}

// Event Sourcing: Event-basierte Speicherung  
public class OrderEvent {
    private String eventId;
    private String orderId;
    private String eventType;
    private LocalDateTime timestamp;
    private Map<String, Object> eventData;
    // Events werden angehängt, nie geändert
}
# Python: Traditionelle Speicherung
class Order:
    def __init__(self):
        self.order_id = None
        self.status = None
        self.total_amount = None
        self.last_modified = None
        # Zustand wird überschrieben

# Event Sourcing: Event-basierte Speicherung
class OrderEvent:
    def __init__(self, event_id, order_id, event_type, event_data):
        self.event_id = event_id
        self.order_id = order_id
        self.event_type = event_type
        self.timestamp = datetime.now()
        self.event_data = event_data
        # Events werden nur hinzugefügt

44.2 Event Store als Single Source of Truth

Der Event Store fungiert als zentrale Datenquelle, die alle geschäftsrelevanten Events unveränderlich speichert. Im Gegensatz zu traditionellen Datenbanken werden hier keine Updates oder Deletes durchgeführt.

44.2.1 Struktur eines Event Stores

Eigenschaft Traditionelle DB Event Store
Operationen CREATE, READ, UPDATE, DELETE CREATE, READ (nur Append)
Datenintegrität Aktuelle Zustandskonsistenz Eventsequenz-Konsistenz
Historisierung Externe Audit-Tabellen Inhärent durch Event-Sequenz
Skalierung Complex aufgrund Updates Linear (nur Appends)

44.2.2 Implementierung eines einfachen Event Stores

Spring Boot Implementierung:

@Repository
public class OrderEventStore {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // Event anhängen (nie ändern)
    public void appendEvent(OrderEvent event) {
        String sql = """
            INSERT INTO order_events 
            (event_id, order_id, event_type, event_data, timestamp)
            VALUES (?, ?, ?, ?, ?)
            """;
        
        jdbcTemplate.update(sql,
            event.getEventId(),
            event.getOrderId(), 
            event.getEventType(),
            event.getEventDataAsJson(),
            event.getTimestamp()
        );
    }
    
    // Events für eine Bestellung laden
    public List<OrderEvent> getEventsForOrder(String orderId) {
        String sql = """
            SELECT * FROM order_events 
            WHERE order_id = ? 
            ORDER BY timestamp ASC
            """;
        
        return jdbcTemplate.query(sql, 
            new Object[]{orderId}, 
            new OrderEventRowMapper()
        );
    }
}

Python Implementierung:

class OrderEventStore:
    def __init__(self, connection):
        self.connection = connection
    
    def append_event(self, event):
        """Event anhängen (nie ändern)"""
        cursor = self.connection.cursor()
        cursor.execute("""
            INSERT INTO order_events 
            (event_id, order_id, event_type, event_data, timestamp)
            VALUES (?, ?, ?, ?, ?)
        """, (
            event.event_id,
            event.order_id,
            event.event_type,
            json.dumps(event.event_data),
            event.timestamp
        ))
        self.connection.commit()
    
    def get_events_for_order(self, order_id):
        """Events für eine Bestellung laden"""
        cursor = self.connection.cursor()
        cursor.execute("""
            SELECT * FROM order_events 
            WHERE order_id = ? 
            ORDER BY timestamp ASC
        """, (order_id,))
        
        return [self._map_to_event(row) for row in cursor.fetchall()]

44.3 Audit Trail und Temporal Queries

Event Sourcing bietet von Natur aus ein vollständiges Audit Trail. Jede Änderung ist dokumentiert und nachvollziehbar, was sowohl für Compliance als auch für Debugging wertvoll ist.

44.3.1 Temporal Queries: Zeitreisen im System

// Spring Boot: Zustand zu einem bestimmten Zeitpunkt rekonstruieren
@Service
public class OrderProjectionService {
    
    public Order getOrderAtTime(String orderId, LocalDateTime pointInTime) {
        List<OrderEvent> events = eventStore.getEventsForOrder(orderId);
        
        return events.stream()
            .filter(event -> event.getTimestamp().isBefore(pointInTime))
            .collect(Order::new, this::applyEvent, Order::merge);
    }
    
    private void applyEvent(Order order, OrderEvent event) {
        switch (event.getEventType()) {
            case "OrderPlaced":
                order.setStatus(OrderStatus.PLACED);
                order.setTotalAmount(event.getTotalAmount());
                break;
            case "PaymentProcessed":
                order.setStatus(OrderStatus.PAID);
                break;
            case "OrderShipped":
                order.setStatus(OrderStatus.SHIPPED);
                break;
        }
    }
}
# Python: Zustand zu einem bestimmten Zeitpunkt rekonstruieren
class OrderProjectionService:
    def __init__(self, event_store):
        self.event_store = event_store
    
    def get_order_at_time(self, order_id, point_in_time):
        events = self.event_store.get_events_for_order(order_id)
        
        # Events bis zum Zeitpunkt filtern
        filtered_events = [
            event for event in events 
            if event.timestamp <= point_in_time
        ]
        
        # Zustand rekonstruieren
        order = Order()
        for event in filtered_events:
            self._apply_event(order, event)
        
        return order
    
    def _apply_event(self, order, event):
        if event.event_type == "OrderPlaced":
            order.status = "PLACED"
            order.total_amount = event.event_data["totalAmount"]
        elif event.event_type == "PaymentProcessed":
            order.status = "PAID"
        elif event.event_type == "OrderShipped":
            order.status = "SHIPPED"

44.3.2 Praktische Anwendungen von Temporal Queries

// Beispiel: Monatlicher Umsatzbericht
public BigDecimal getMonthlyRevenue(int year, int month) {
    LocalDateTime monthStart = LocalDateTime.of(year, month, 1, 0, 0);
    LocalDateTime monthEnd = monthStart.plusMonths(1);
    
    return eventStore.getEventsBetween(monthStart, monthEnd)
        .stream()
        .filter(event -> "PaymentProcessed".equals(event.getEventType()))
        .map(event -> event.getAmount())
        .reduce(BigDecimal.ZERO, BigDecimal::add);
}

44.4 Complexity vs. Benefits

Event Sourcing bringt erhebliche Vorteile, aber auch zusätzliche Komplexität mit sich. Eine ehrliche Bewertung hilft bei der Entscheidung, wann dieser Ansatz sinnvoll ist.

44.4.1 Vorteile von Event Sourcing

Bereich Vorteil Praktischer Nutzen
Auditierbarkeit Vollständige Historie Compliance, Debugging, Geschäftsanalyse
Flexibilität Neue Projektionen möglich Retroaktive Geschäftsberichte
Debugging Exakte Nachstellung Reproduzierbare Fehlerszenarien
Skalierung Nur Appends Hohe Schreibperformance
Integration Events als API Lose Kopplung zwischen Services

44.4.2 Herausforderungen und Komplexität

Herausforderung Impact Lösungsansatz
Eventual Consistency Verzögerte Konsistenz Geschäftsprozesse anpassen
Event Schema Evolution Breaking Changes Versionierte Events
Storage Growth Unbegrenzte Größe Snapshots, Archivierung
Query Complexity Keine Ad-hoc Queries Vorberechnete Projektionen
Team Learning Curve Paradigmenwechsel Training, schrittweise Einführung

44.4.3 Wann Event Sourcing verwenden?

Gute Kandidaten: - Systeme mit hohen Audit-Anforderungen (Finanzwesen, Gesundheitswesen) - Analytische Anwendungen mit historischen Abfragen - Systeme mit komplexen Geschäftsregeln - Microservices mit hoher Entkopplung

Schlechte Kandidaten: - Einfache CRUD-Anwendungen - Systeme mit strengen Konsistenzanforderungen - Teams ohne Event-Sourcing-Erfahrung - Legacy-Systeme mit bestehenden Constraints

44.4.4 Performance-Überlegungen

// Problem: Event-Replay kann bei vielen Events langsam werden
public Order getCurrentOrder(String orderId) {
    List<OrderEvent> events = eventStore.getEventsForOrder(orderId);
    // 10.000 Events = 10.000 Operationen für aktuellen Zustand
    return replayEvents(events);
}

// Lösung: Snapshots für bessere Performance
public Order getCurrentOrderWithSnapshot(String orderId) {
    OrderSnapshot snapshot = snapshotStore.getLatestSnapshot(orderId);
    List<OrderEvent> recentEvents = eventStore.getEventsAfter(
        orderId, 
        snapshot.getTimestamp()
    );
    // Nur wenige Events seit Snapshot
    return applyEventsToSnapshot(snapshot, recentEvents);
}

Die Entscheidung für Event Sourcing sollte bewusst getroffen werden, basierend auf den spezifischen Anforderungen und der Bereitschaft des Teams, die zusätzliche Komplexität zu meistern. In den nächsten Abschnitten werden wir sehen, wie Projektionen und CQRS diese Komplexität handhabbar machen.