System-Design
Event-Driven Architecture in der Praxis: Wann sie funktioniert – und wann nicht
Event-Driven Architecture ist der Liebling der Konferenz-Slides: lose Kopplung, unendliche Skalierbarkeit, Echtzeit-Reaktionen. In der Realität sehe ich Teams, die nach einem Jahr im Event-Chaos versinken – weil niemand mehr weiß, welcher Service auf welches Event reagiert, und Debugging zum Detektivspiel wird. EDA ist mächtig. Aber sie ist kein Allheilmittel.
Wenn Sie nicht bereit sind, Observability und Event-Governance wie Produktfeatures zu behandeln, lassen Sie EDA.
Was ist Event-Driven Architecture?
In einer Event-Driven Architecture kommunizieren Services nicht direkt miteinander, sondern über Events – Nachrichten, die beschreiben, dass etwas passiert ist:
// Synchron (Request/Response)
OrderService → UserService.getUser(userId)
← User
// Event-Driven (Asynchron)
OrderService → publishes: OrderCreated { orderId, userId, items }
UserService ← subscribes: OrderCreated
InventoryService ← subscribes: OrderCreated
NotificationService ← subscribes: OrderCreated
Der entscheidende Unterschied: Der OrderService weiß nicht, wer auf seine Events reagiert. Er feuert und vergisst.
Event-Typen
| Typ | Beschreibung | Beispiel |
|---|---|---|
| Domain Event | Geschäftlich relevantes Ereignis | OrderPlaced, PaymentReceived |
| Integration Event | Kommunikation zwischen Bounded Contexts | CustomerCreated (für andere Services) |
| Event Notification | Signal ohne viele Daten | InventoryLow { productId } |
| Event-Carried State Transfer | Event mit vollständigem State | CustomerUpdated { ...alle Felder } |
Jedes Event ist eine öffentliche API. Wenn Sie Events ohne Versionierung und Contracts publishen, bauen Sie Abhängigkeiten, die Sie später nicht mehr brechen können.
Wann EDA sinnvoll ist
Gute Use Cases
- Lose Kopplung zwischen Teams: Team A muss nicht auf Team B warten
- Unterschiedliche Skalierungsanforderungen: Order-Ingestion vs. Reporting
- Asynchrone Workflows: Bestellung → Zahlung → Versand → Benachrichtigung
- Audit-Trail: Events als unveränderliche Historie
- Event Sourcing: State aus Event-Historie rekonstruierbar
- Real-time Reactions: Fraud Detection, Recommendations
Schlechte Use Cases
- Synchrone Abhängigkeiten: „Zeige Bestellung mit Kundendaten" braucht beides jetzt
- Starke Konsistenzanforderungen: Banktransaktionen, Inventar-Reservierung. Wenn Sie starke Konsistenz brauchen, ist EDA nicht „schwierig" – dann ist es das falsche Default. Planen Sie explizit Sagas + Kompensation oder bleiben Sie synchron.
- Einfache CRUD-Anwendungen: Blog, Kontaktformular – Events sind Overkill
- Kleine Teams: EDA-Overhead lohnt sich erst ab gewisser Größe
Architektur-Patterns
Event Broker
Der zentrale Baustein: Ein Message Broker, der Events entgegennimmt und an Subscriber verteilt:
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Order │────▶│ │────▶│ Inventory │
│ Service │ │ Kafka/ │ │ Service │
└─────────────┘ │ RabbitMQ/ │ └─────────────┘
│ AWS SNS+SQS │
┌─────────────┐ │ │ ┌─────────────┐
│ Payment │────▶│ │────▶│ Notification│
│ Service │ └─────────────────┘ │ Service │
└─────────────┘ └─────────────┘
| Broker | Stärken | Wann einsetzen |
|---|---|---|
| Apache Kafka | Hoher Durchsatz, Event-Log, Replay | High-Volume, Event Sourcing |
| RabbitMQ | Flexible Routing, einfaches Setup | Klassische Message Queues |
| AWS SNS/SQS | Managed, Pay-per-Use | AWS-Umgebung, Serverless |
| Redis Streams | Schnell, einfach | Einfache Use Cases, bereits Redis im Stack |
Event Sourcing
Statt den aktuellen State zu speichern, speichern Sie alle Events, die zum State geführt haben:
// Klassisch: State speichern
UPDATE orders SET status = 'shipped' WHERE id = 123;
// Event Sourcing: Events speichern
INSERT INTO events (aggregate_id, type, data) VALUES
(123, 'OrderCreated', '{"items": [...]}'),
(123, 'PaymentReceived', '{"amount": 99.00}'),
(123, 'OrderShipped', '{"trackingId": "DHL123"}');
// State wird durch Replay rekonstruiert
function getOrderState(orderId) {
const events = getEventsForOrder(orderId);
return events.reduce(applyEvent, initialState);
}
CQRS (Command Query Responsibility Segregation)
Trennung von Schreib- und Lesemodell:
// Command Side (Schreiben)
POST /orders → OrderService → Events → Event Store
// Query Side (Lesen)
GET /orders → Read Model (optimiert für Queries)
↑
Projections (aus Events aufgebaut)
Das ermöglicht unterschiedliche Optimierungen: Schreibseite normalisiert für Konsistenz, Leseseite denormalisiert für Performance.
Der Elefant im Raum: Eventual Consistency
In verteilten Event-Driven Systemen gibt es keine sofortige Konsistenz. Events brauchen Zeit, bis sie verarbeitet sind. Das führt zu Situationen, die in synchronen Systemen nicht existieren:
// User erstellt Bestellung
POST /orders → 201 Created
// User ruft sofort Bestellübersicht auf
GET /orders → Bestellung fehlt noch (Event noch nicht verarbeitet)
// 500ms später
GET /orders → Bestellung ist da
Strategien für Eventual Consistency
- Optimistic UI: Zeige sofort lokalen State, aktualisiere bei Event-Bestätigung
- Polling/WebSocket: Client wartet auf Bestätigung
- Read-your-writes: Nach Schreibvorgang aus Write-Modell lesen
- Akzeptieren: Manche Inkonsistenzen sind geschäftlich akzeptabel
Die häufigsten Fallstricke
1. Event-Chaos
Nach zwei Jahren hat niemand mehr den Überblick: 200 Event-Typen, keine Dokumentation, Services, die auf Events reagieren, von denen keiner weiß.
Lösung: Event Catalog von Tag 1. Jedes Event dokumentiert: Schema, Producer, Consumer, Business-Zweck. Tools wie AsyncAPI oder Event Catalog helfen.
2. Distributed Monolith
Services, die synchron aufeinander warten oder feste Reihenfolgen erwarten – die Nachteile von Microservices ohne die Vorteile.
// Anti-Pattern: Synchrone Kette über Events
OrderService → OrderCreated
→ InventoryService prüft → InventoryReserved
→ PaymentService wartet auf InventoryReserved → PaymentProcessed
→ OrderService wartet auf PaymentProcessed → OrderConfirmed
// Ergebnis: Latenz addiert sich, ein Ausfall blockiert alles
Das passiert fast immer, wenn Teams Sequenzen über Events modellieren, die eigentlich ein synchrones Geschäfts-Transaktionsthema sind.
Lösung: Choreographie statt Orchestrierung – oder explizite Saga mit Kompensation.
3. Fehlende Idempotenz
Events können doppelt ankommen (at-least-once delivery). Ohne Idempotenz führt das zu Chaos:
// Event kommt zweimal an
PaymentReceived { orderId: 123, amount: 99.00 }
PaymentReceived { orderId: 123, amount: 99.00 }
// Ohne Idempotenz: Kunde zahlt doppelt
// Mit Idempotenz: Zweites Event wird ignoriert
Lösung: Event-IDs speichern, bereits verarbeitete Events überspringen. Idempotente Operationen designen. Praxis: Outbox (Producer) + Inbox/Processed-Events (Consumer) ist der Standard, wenn Sie at-least-once sauber beherrschen wollen.
4. Schema-Evolution
Events ändern sich. Neue Felder, geänderte Strukturen, deprecated Fields. Alte Consumer müssen mit neuen Events umgehen können – und umgekehrt.
// Version 1
OrderCreated { orderId, items }
// Version 2 (neues Feld)
OrderCreated { orderId, items, customerId }
// Version 3 (breaking change)
OrderCreated { orderId, lineItems, customer: { id, name } }
Breaking Changes in Events sind Produktionsausfälle mit Ansage. Wenn Sie sie brauchen, müssen Sie parallel Versionen fahren oder eine neue Event-Name-Generation publishen.
Lösung: Schema Registry (Confluent, AWS Glue), Versionierung, Forward/Backward Compatibility als Regel.
5. Debugging-Hölle
Etwas geht schief. Wo? Ein Request wird zu Events, Events werden zu mehr Events, verteilt über 10 Services. Viel Spaß beim Debuggen.
Observability ist nicht optional
In Event-Driven Systemen brauchen Sie mehr als Logs:
Must-haves
- Distributed Tracing: Request über alle Services verfolgen
- Event Flow Visualization: Welches Event triggert was?
- Consumer Lag: Wie weit hinkt ein Consumer hinterher?
- Dead Letter Queues: Wohin gehen fehlgeschlagene Events?
- Schema Registry: Welche Event-Versionen sind im Umlauf?
// Jedes Event braucht:
{
"eventId": "uuid", // Einzigartig
"correlationId": "uuid", // Request-übergreifend
"causationId": "uuid", // Welches Event hat dieses ausgelöst?
"timestamp": "ISO8601",
"version": "1.2.0",
"source": "order-service",
"type": "OrderCreated",
"data": { ... }
}
Migration zu EDA
Sie haben einen Monolithen und wollen Event-Driven werden? Nicht alles auf einmal.
Strangler Fig mit Events
- Event-Emission im Monolith: Beginnen Sie, Events zu publishen – auch wenn noch niemand zuhört
- Neue Features Event-Driven: Alles Neue als separate Services, die auf Events reagieren
- Schrittweise Extraktion: Bestehende Funktionalität in Services migrieren
- Anti-Corruption Layer: Übersetzer zwischen alter und neuer Welt
Fazit
Event-Driven Architecture ist kein Upgrade – sie ist ein Trade-off. Sie tauschen synchrone Komplexität gegen asynchrone Komplexität. Manchmal ist das der richtige Trade. Oft nicht.
EDA funktioniert, wenn:
- Sie echte Entkopplung brauchen (Teams, Skalierung, Deployment)
- Eventual Consistency für Ihr Business akzeptabel ist
- Sie in Observability investieren
- Sie Event-Design als Architektur-Disziplin behandeln
EDA scheitert, wenn:
- Sie sie einführen, weil Kafka cool ist
- Sie synchrone Probleme mit asynchronen Tools lösen wollen
- Niemand für Event-Governance verantwortlich ist
- Debugging und Operations nachgedacht werden
Häufige Fragen
Kafka oder RabbitMQ?
Kafka für High-Volume, Event Sourcing, Replay-Anforderungen. RabbitMQ für klassische Message-Queues, einfachere Setups, flexible Routing-Patterns. Wenn Sie fragen müssen, starten Sie mit RabbitMQ – es ist einfacher zu betreiben.
Wie viele Events sind zu viele?
Keine feste Zahl, aber eine Faustregel: Wenn niemand im Team alle Event-Typen kennt, haben Sie zu viele. 50-100 Events sind bei größeren Systemen normal, aber jedes sollte dokumentiert und begründet sein.
Event Sourcing – ja oder nein?
Nein, außer Sie haben einen klaren Grund (Audit-Anforderungen, Zeitreisen, komplexe Aggregate). Event Sourcing ist eine Architektur-Entscheidung mit weitreichenden Konsequenzen. Starten Sie ohne, führen Sie es später ein, wenn nötig.
Wie teste ich Event-Driven Systeme?
Unit-Tests für Event-Handler, Contract-Tests für Event-Schemas, Integration-Tests mit embedded Broker, End-to-End-Tests für kritische Flows. Chaos Engineering für Resilience. Testbarkeit muss von Anfang an eingeplant werden.
Architektur-Review für Ihr System?
Ich bewerte, ob Event-Driven Architecture für Ihren Use Case passt – und wie Sie sie ohne Chaos einführen.
Architektur-Review anfragen