48 Choreographie: Dezentrale Workflow-Koordination

48.1 Grundprinzip der Choreographie

Choreographie in Event-Driven Architecture beschreibt ein Koordinationsmuster, bei dem komplexe Geschäftsprozesse durch die autonome Reaktion verteilter Services auf Events entstehen. Anstatt einer zentralen Steuerungsinstanz folgt jeder Service nur seinen eigenen Geschäftsregeln und reagiert auf relevante Events im System.

Es ist wie ein Tanzabend: Anstatt eines Choreografen, der jeden Schritt dirigiert, kennt jeder Tänzer seine Rolle und reagiert auf die Bewegungen der anderen. Das Gesamtbild entsteht durch das Zusammenspiel autonomer Akteure.

48.2 Emergent Behavior in Microservices

48.2.1 Wie komplexe Workflows aus einfachen Regeln entstehen

In unserem E-Commerce-Szenario entsteht der Bestellprozess durch das Zusammenspiel mehrerer Services, ohne dass ein zentraler Koordinator existiert:

// OrderService - Startet den Workflow
@Service
public class OrderService {
    
    @EventListener
    public void handleOrderPlacement(PlaceOrderCommand command) {
        Order order = new Order(command);
        orderRepository.save(order);
        
        // Event publizieren - andere Services reagieren autonom
        eventPublisher.publish(new OrderPlacedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getItems(),
            order.getTotalAmount()
        ));
    }
}

// PaymentService - Reagiert autonom auf OrderPlaced
@Service
public class PaymentService {
    
    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        PaymentRequest payment = createPaymentRequest(event);
        PaymentResult result = processPayment(payment);
        
        if (result.isSuccessful()) {
            eventPublisher.publish(new PaymentProcessedEvent(
                event.getOrderId(),
                result.getTransactionId(),
                event.getTotalAmount()
            ));
        } else {
            eventPublisher.publish(new PaymentFailedEvent(
                event.getOrderId(),
                result.getErrorCode()
            ));
        }
    }
}

// InventoryService - Reagiert parallel auf OrderPlaced
@Service
public class InventoryService {
    
    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        boolean available = checkAvailability(event.getItems());
        
        if (available) {
            reserveItems(event.getItems());
            eventPublisher.publish(new InventoryReservedEvent(
                event.getOrderId(),
                event.getItems()
            ));
        } else {
            eventPublisher.publish(new InventoryInsufficientEvent(
                event.getOrderId(),
                findUnavailableItems(event.getItems())
            ));
        }
    }
}

48.2.2 Python-Implementierung der autonomen Services

# order_service.py
class OrderService:
    def __init__(self, event_publisher, order_repository):
        self.event_publisher = event_publisher
        self.order_repository = order_repository
    
    async def handle_place_order(self, command):
        order = Order(command)
        await self.order_repository.save(order)
        
        # Workflow startet durch Event-Publikation
        await self.event_publisher.publish(OrderPlacedEvent(
            order_id=order.id,
            customer_id=order.customer_id,
            items=order.items,
            total_amount=order.total_amount
        ))

# payment_service.py  
class PaymentService:
    def __init__(self, event_publisher, payment_processor):
        self.event_publisher = event_publisher
        self.payment_processor = payment_processor
    
    async def handle_order_placed(self, event: OrderPlacedEvent):
        result = await self.payment_processor.process(
            customer_id=event.customer_id,
            amount=event.total_amount
        )
        
        if result.successful:
            await self.event_publisher.publish(PaymentProcessedEvent(
                order_id=event.order_id,
                transaction_id=result.transaction_id
            ))
        else:
            await self.event_publisher.publish(PaymentFailedEvent(
                order_id=event.order_id,
                error_code=result.error_code
            ))

Der komplexe Bestellprozess entsteht durch das emergente Verhalten dieser autonomen Services. Keiner “weiß” von dem Gesamtworkflow - jeder folgt nur seinen eigenen Geschäftsregeln.

48.3 Event-driven Collaboration

48.3.1 Kollaboration ohne direkte Kopplung

Die Services kommunizieren ausschließlich über Events, wodurch eine lose Kopplung entsteht. Ein Service kennt nicht die Implementierung anderer Services, sondern nur die Events, auf die er reagieren muss.

Kollaborationsmuster Beschreibung Beispiel
Parallel Processing Mehrere Services reagieren gleichzeitig auf dasselbe Event PaymentService und InventoryService verarbeiten OrderPlaced parallel
Sequential Chains Ein Event löst weitere Events aus, die eine Kette bilden OrderPlaced → PaymentProcessed → ShippingInitiated
Conditional Flows Events werden nur unter bestimmten Bedingungen ausgelöst PaymentFailed → OrderCancelled (nur bei Zahlungsfehlern)
Fan-out/Fan-in Ein Event löst mehrere aus, deren Ergebnisse später zusammenfließen OrderPlaced → mehrere Validierungen → OrderConfirmed

48.3.2 Implementierung von Kollaborationsmustern

// ShippingService - Wartet auf multiple Events
@Service
public class ShippingService {
    private final Map<String, OrderShippingState> pendingOrders = new ConcurrentHashMap<>();
    
    @EventListener
    public void handlePaymentProcessed(PaymentProcessedEvent event) {
        OrderShippingState state = getOrCreateState(event.getOrderId());
        state.setPaymentConfirmed(true);
        checkReadyForShipping(state);
    }
    
    @EventListener  
    public void handleInventoryReserved(InventoryReservedEvent event) {
        OrderShippingState state = getOrCreateState(event.getOrderId());
        state.setInventoryConfirmed(true);
        checkReadyForShipping(state);
    }
    
    private void checkReadyForShipping(OrderShippingState state) {
        if (state.isPaymentConfirmed() && state.isInventoryConfirmed()) {
            eventPublisher.publish(new ShippingInitiatedEvent(
                state.getOrderId(),
                state.getShippingAddress()
            ));
        }
    }
}

48.4 Monitoring Choreographed Workflows

48.4.1 Herausforderungen der Observability

Da bei Choreographie kein zentraler Koordinator existiert, ist die Nachverfolgung von Workflows komplexer. Der Zustand eines Geschäftsprozesses ist über mehrere Services verteilt.

// Event Correlation für Workflow-Monitoring
@Component
public class WorkflowTracker {
    
    @EventListener
    public void trackEvent(DomainEvent event) {
        WorkflowInstance workflow = findOrCreateWorkflow(event.getCorrelationId());
        workflow.addEvent(event);
        
        // Workflow-Status bestimmen
        WorkflowStatus status = determineWorkflowStatus(workflow);
        
        // Monitoring-Event senden
        monitoringPublisher.publish(new WorkflowStatusChangedEvent(
            workflow.getId(),
            status,
            event.getTimestamp()
        ));
    }
    
    private WorkflowStatus determineWorkflowStatus(WorkflowInstance workflow) {
        boolean hasOrderPlaced = workflow.hasEvent(OrderPlacedEvent.class);
        boolean hasPaymentProcessed = workflow.hasEvent(PaymentProcessedEvent.class);
        boolean hasInventoryReserved = workflow.hasEvent(InventoryReservedEvent.class);
        boolean hasShippingInitiated = workflow.hasEvent(ShippingInitiatedEvent.class);
        
        if (hasShippingInitiated) return WorkflowStatus.COMPLETED;
        if (hasPaymentProcessed && hasInventoryReserved) return WorkflowStatus.READY_FOR_SHIPPING;
        if (hasOrderPlaced) return WorkflowStatus.PROCESSING;
        
        return WorkflowStatus.UNKNOWN;
    }
}

48.4.2 Python-Monitoring mit Correlation IDs

# workflow_tracker.py
class WorkflowTracker:
    def __init__(self):
        self.workflows = {}
    
    async def track_event(self, event):
        correlation_id = event.correlation_id
        
        if correlation_id not in self.workflows:
            self.workflows[correlation_id] = WorkflowInstance(correlation_id)
        
        workflow = self.workflows[correlation_id]
        workflow.add_event(event)
        
        # Status-Berechnung
        status = self._determine_status(workflow)
        
        await self._publish_status_update(workflow.id, status)
    
    def _determine_status(self, workflow):
        events = workflow.events
        event_types = [type(e).__name__ for e in events]
        
        if 'ShippingInitiatedEvent' in event_types:
            return 'COMPLETED'
        elif 'PaymentProcessedEvent' in event_types and 'InventoryReservedEvent' in event_types:
            return 'READY_FOR_SHIPPING'
        elif 'OrderPlacedEvent' in event_types:
            return 'PROCESSING'
        
        return 'UNKNOWN'

48.5 Benefits und Challenges

48.5.1 Vorteile der Choreographie

Autonomie und Skalierbarkeit Services können unabhängig entwickelt, deployed und skaliert werden. Jeder Service ist für seine eigene Geschäftslogik verantwortlich und kann diese ohne Rücksicht auf andere Services ändern.

Resilience durch Entkopplung Der Ausfall eines Service beeinträchtigt nicht die Funktion der anderen. Workflows können auch bei partiellen Systemausfällen weiterlaufen, sofern die kritischen Pfade verfügbar bleiben.

Evolutionsfähigkeit Neue Services können einfach in bestehende Workflows integriert werden, indem sie auf relevante Events reagieren. Bestehende Services müssen nicht modifiziert werden.

48.5.2 Herausforderungen und Trade-offs

Challenge Beschreibung Mitigation Strategy
Workflow Visibility Schwierige Nachverfolgung des Gesamtzustands Correlation IDs, Event Tracking, Workflow Dashboards
Error Handling Komplexe Fehlerbehandlung ohne zentralen Koordinator Saga Pattern, Compensation Events, Circuit Breaker
Testing Complexity Schwierige End-to-End Tests Contract Testing, Event Sourcing für Replay
Eventual Consistency Workflows sind nicht sofort konsistent Kompensation, Monitoring, User Communication

48.5.3 Wann Choreographie vermeiden

Choreographie eignet sich nicht für alle Szenarien:

// Anti-Pattern: Zu komplexe Choreographie
@EventListener
public void handleComplexBusinessRule(OrderPlacedEvent event) {
    // Vermeiden: Komplexe Bedingungslogik in Events
    if (event.getCustomer().isPremium() && 
        event.getTotalAmount() > 1000 && 
        isWeekend() && 
        hasSpecialPromotion() &&
        inventoryService.hasStock(event.getItems()) &&
        paymentService.hasValidPaymentMethod(event.getCustomerId())) {
        
        // Diese Logik gehört in einen Orchestrator
        applySpecialProcessing(event);
    }
}

Die Choreographie glänzt bei klar abgegrenzten Geschäftsprozessen mit autonomen Services, stößt aber bei komplexen Abhängigkeiten und strikten Konsistenzanforderungen an ihre Grenzen. Das Verständnis dieser Grenzen ist entscheidend für die richtige Architekturentscheidung zwischen Choreographie und Orchestrierung, die wir im nächsten Abschnitt behandeln werden.