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.
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ügtDer 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.
| 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) |
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()]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.
// 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"// 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);
}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.
| 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 |
| 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 |
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
// 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.