38 Integration und Systemkopplung

Die Einführung von Event-Driven Architecture erfolgt selten auf der grünen Wiese. Meist müssen bestehende Systeme schrittweise integriert oder abgelöst werden, ohne den laufenden Betrieb zu gefährden. Dabei entstehen temporäre Hybridarchitekturen, die sowohl synchrone Legacy-Komponenten als auch asynchrone Event-Streams enthalten.

38.1 Patterns zur Integration bestehender Systeme

38.1.1 Das Integrationsproblem

Ein typisches E-Commerce-System läuft oft jahrelang erfolgreich mit einer monolithischen Architektur. Das Bestellsystem, die Lagerverwaltung und die Zahlung sind direkt gekoppelt und kommunizieren über Datenbankzugriffe und Methodenaufrufe. Die schrittweise Einführung einer Event-getriebenen Architektur erfordert eine durchdachte Migrationsstrategie, um flexibler auf Marktveränderungen reagieren zu können.

Herausforderung Legacy-System Ziel-Architektur
Kopplung Tight gekoppelt über DB Loose gekoppelt über Events
Kommunikation Synchron Asynchron
Datenmodell Gemeinsame Datenbank Service-eigene Daten
Fehlerbehandlung Transaktionen Eventual Consistency

Die Transformation kann nicht über Nacht erfolgen. Bewährte Integrationsmuster helfen dabei, den Übergang zu gestalten und Risiken zu minimieren.

38.1.2 Strangler Fig Pattern

Das Strangler Fig Pattern, benannt nach einer Kletterpflanze, die Bäume allmählich umschließt, beschreibt den schrittweisen Ersatz von Legacy-Funktionalität durch neue Services.

Grundprinzip: Neue Funktionalität wird parallel zur alten implementiert und schrittweise mehr Traffic übernimmt, bis das alte System komplett ersetzt ist.

38.1.2.1 Praktisches Vorgehen

Angenommen, Ihr monolithisches Bestellsystem soll durch einen Event-getriebenen OrderService ersetzt werden:

// Legacy OrderController (wird schrittweise ersetzt)
@RestController
@RequestMapping("/api/v1/orders")
public class LegacyOrderController {
    
    @Autowired
    private OrderMigrationService migrationService;
    
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        // Entscheidung: Legacy oder neuer Service?
        if (migrationService.shouldUseNewService(request)) {
            return delegateToNewService(request);
        } else {
            return handleWithLegacyCode(request);
        }
    }
    
    private ResponseEntity<OrderResponse> delegateToNewService(OrderRequest request) {
        // Transformation zu Event-getriebenem Format
        OrderCommand command = transformToCommand(request);
        OrderEvent event = newOrderService.processOrder(command);
        return transformToResponse(event);
    }
}
# Python Implementation mit Feature Flags
class OrderController:
    def __init__(self, migration_service, legacy_service, new_service):
        self.migration_service = migration_service
        self.legacy_service = legacy_service
        self.new_service = new_service
    
    async def create_order(self, order_request):
        if await self.migration_service.should_use_new_service(order_request):
            return await self._delegate_to_new_service(order_request)
        else:
            return await self._handle_with_legacy(order_request)
    
    async def _delegate_to_new_service(self, request):
        command = self._transform_to_command(request)
        event = await self.new_service.process_order(command)
        return self._transform_to_response(event)

38.1.2.2 Migrationsstrategie

Die Umstellung erfolgt in kontrollierten Phasen:

  1. Parallelaufbau: Neuer Service wird parallel entwickelt und getestet
  2. Selective Routing: Feature Flags steuern, welche Requests zum neuen Service gehen
  3. Graduelle Migration: Schrittweise Erhöhung des Traffic-Anteils
  4. Legacy-Stilllegung: Vollständige Umstellung und Abschaltung des alten Systems
@Component
public class OrderMigrationService {
    
    @Value("${order.migration.percentage:0}")
    private int migrationPercentage;
    
    @Value("${order.migration.customer-types}")
    private Set<String> enabledCustomerTypes;
    
    public boolean shouldUseNewService(OrderRequest request) {
        // Graduelle Rollout-Logik
        if (enabledCustomerTypes.contains(request.getCustomerType())) {
            return true;
        }
        
        // Prozentbasierter Rollout
        return ThreadLocalRandom.current().nextInt(100) < migrationPercentage;
    }
}

38.1.3 Anti-Corruption Layer

Der Anti-Corruption Layer (ACL) schützt das neue Domänenmodell vor den Eigenarten und Inkonsistenzen des Legacy-Systems. Er fungiert als Übersetzungsschicht, die verhindert, dass Legacy-Konzepte in die neue Architektur “eindringen”.

Kernidee: Eine dedizierte Schicht übersetzt zwischen Legacy-Datenstrukturen und dem sauberen Domänenmodell der neuen Event-getriebenen Services.

38.1.3.1 Datenmodell-Transformation

Legacy-Systeme haben oft gewachsene Datenstrukturen, die nicht zu modernen Domänenmodellen passen:

// Legacy Order Struktur (historisch gewachsen)
public class LegacyOrder {
    public String orderNum;         // Format: "ORD-2024-001234"
    public String custInfo;         // Concatenated: "John,Doe,john@mail.com"
    public String itemList;         // CSV: "item1:2:9.99,item2:1:19.99"
    public int statusCode;          // Magic numbers: 1=new, 2=paid, 3=shipped
    public Date createTS;           // Verschiedene Zeitzonen möglich
}

// Anti-Corruption Layer
@Component
public class OrderTranslationService {
    
    public OrderPlacedEvent translateFromLegacy(LegacyOrder legacyOrder) {
        return OrderPlacedEvent.builder()
            .orderId(extractOrderId(legacyOrder.orderNum))
            .customer(parseCustomerInfo(legacyOrder.custInfo))
            .items(parseItemList(legacyOrder.itemList))
            .status(translateStatus(legacyOrder.statusCode))
            .timestamp(normalizeTimestamp(legacyOrder.createTS))
            .build();
    }
    
    private OrderStatus translateStatus(int statusCode) {
        return switch (statusCode) {
            case 1 -> OrderStatus.PLACED;
            case 2 -> OrderStatus.PAYMENT_CONFIRMED;
            case 3 -> OrderStatus.SHIPPED;
            default -> throw new IllegalArgumentException("Unknown status: " + statusCode);
        };
    }
    
    private List<OrderItem> parseItemList(String itemList) {
        return Arrays.stream(itemList.split(","))
            .map(this::parseItemEntry)
            .collect(Collectors.toList());
    }
}
# Python Anti-Corruption Layer
from dataclasses import dataclass
from typing import List
import json

@dataclass
class LegacyOrder:
    order_num: str
    cust_info: str
    item_list: str
    status_code: int
    create_ts: str

class OrderTranslationService:
    
    def translate_from_legacy(self, legacy_order: LegacyOrder) -> dict:
        return {
            'eventId': self._generate_event_id(),
            'eventType': 'OrderPlaced',
            'timestamp': self._normalize_timestamp(legacy_order.create_ts),
            'data': {
                'orderId': self._extract_order_id(legacy_order.order_num),
                'customer': self._parse_customer_info(legacy_order.cust_info),
                'items': self._parse_item_list(legacy_order.item_list),
                'status': self._translate_status(legacy_order.status_code)
            }
        }
    
    def _translate_status(self, status_code: int) -> str:
        status_mapping = {
            1: 'PLACED',
            2: 'PAYMENT_CONFIRMED', 
            3: 'SHIPPED'
        }
        
        if status_code not in status_mapping:
            raise ValueError(f"Unknown status code: {status_code}")
        
        return status_mapping[status_code]

38.1.3.2 Event-Generierung aus Legacy-Änderungen

Der ACL kann auch Legacy-Systemänderungen in Events übersetzen:

@Component
public class LegacyOrderEventAdapter {
    
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;
    
    @Autowired
    private OrderTranslationService translationService;
    
    // Wird von Legacy-System bei Statusänderungen aufgerufen
    public void onLegacyOrderStatusChange(LegacyOrder updatedOrder) {
        try {
            OrderEvent event = translationService.translateFromLegacy(updatedOrder);
            
            kafkaTemplate.send("order.lifecycle.v1", 
                event.getOrderId(), 
                event);
            
            log.info("Legacy order {} translated to event {}", 
                updatedOrder.orderNum, 
                event.getEventId());
                
        } catch (Exception e) {
            log.error("Failed to translate legacy order {}: {}", 
                updatedOrder.orderNum, 
                e.getMessage());
        }
    }
}

38.1.4 Legacy System Adapter

Der Legacy System Adapter kapselt die spezifischen Zugriffsmuster und APIs alter Systeme und stellt eine einheitliche, Event-orientierte Schnittstelle bereit.

Ziel: Legacy-Systeme werden zu gleichberechtigten Teilnehmern der Event-Architektur, ohne ihre interne Struktur ändern zu müssen.

38.1.4.1 Adapter-Implementierung

@Service
public class LegacyInventoryAdapter {
    
    private final LegacyInventoryService legacyService;
    private final KafkaTemplate<String, Object> eventPublisher;
    
    public LegacyInventoryAdapter(LegacyInventoryService legacyService,
                                 KafkaTemplate<String, Object> eventPublisher) {
        this.legacyService = legacyService;
        this.eventPublisher = eventPublisher;
    }
    
    // Event-Handler für neue Bestellungen
    @KafkaListener(topics = "order.placed.v1")
    public void handleOrderPlaced(OrderPlacedEvent event) {
        try {
            // Legacy-System-spezifische Reservierung
            LegacyReservationResult result = legacyService.reserveInventory(
                event.getOrderId(),
                transformToLegacyItems(event.getItems())
            );
            
            // Ergebnis als Event publizieren
            if (result.isSuccess()) {
                publishInventoryReservedEvent(event.getOrderId(), result);
            } else {
                publishInventoryReservationFailedEvent(event.getOrderId(), result);
            }
            
        } catch (LegacySystemException e) {
            publishInventoryReservationFailedEvent(event.getOrderId(), e);
        }
    }
    
    private void publishInventoryReservedEvent(String orderId, 
                                             LegacyReservationResult result) {
        InventoryReservedEvent event = InventoryReservedEvent.builder()
            .orderId(orderId)
            .reservationId(result.getReservationId())
            .reservedItems(transformFromLegacyItems(result.getReservedItems()))
            .timestamp(Instant.now())
            .build();
            
        eventPublisher.send("inventory.reserved.v1", orderId, event);
    }
}
# Python Adapter mit async Processing
import asyncio
from typing import Dict, Any

class LegacyInventoryAdapter:
    
    def __init__(self, legacy_service, event_publisher):
        self.legacy_service = legacy_service
        self.event_publisher = event_publisher
    
    async def handle_order_placed(self, event: Dict[str, Any]):
        try:
            # Legacy-Call in separatem Thread ausführen
            loop = asyncio.get_event_loop()
            result = await loop.run_in_executor(
                None, 
                self._reserve_inventory_sync, 
                event
            )
            
            if result['success']:
                await self._publish_inventory_reserved(event['data']['orderId'], result)
            else:
                await self._publish_reservation_failed(event['data']['orderId'], result)
                
        except Exception as e:
            await self._publish_reservation_failed(event['data']['orderId'], str(e))
    
    def _reserve_inventory_sync(self, event):
        # Synchroner Legacy-Call
        return self.legacy_service.reserve_inventory(
            event['data']['orderId'],
            self._transform_to_legacy_items(event['data']['items'])
        )

38.1.4.2 Polling-basierte Integration

Wenn das Legacy-System keine Event-Callbacks unterstützt, kann der Adapter Änderungen durch Polling erkennen:

@Component
public class LegacyOrderStatusPoller {
    
    @Scheduled(fixedDelay = 30000) // Alle 30 Sekunden
    public void pollForOrderStatusChanges() {
        try {
            List<LegacyOrder> updatedOrders = legacyOrderService
                .getOrdersUpdatedSince(getLastPollTime());
            
            for (LegacyOrder order : updatedOrders) {
                processOrderUpdate(order);
            }
            
            updateLastPollTime();
            
        } catch (Exception e) {
            log.error("Error during legacy order polling", e);
        }
    }
    
    private void processOrderUpdate(LegacyOrder order) {
        OrderStatusChangedEvent event = translationService
            .translateStatusChange(order);
            
        eventPublisher.send("order.status.changed.v1", 
            order.getOrderId(), 
            event);
    }
}

38.1.5 Vergleich der Integrationsmuster

Muster Einsatzbereich Vorteil Nachteil
Strangler Fig Vollständiger Systemersatz Risikoarme Migration Längere Parallelphase
Anti-Corruption Layer Datenmodell-Inkompatibilität Saubere Domänentrennung Zusätzliche Transformationsschicht
Legacy Adapter System-Integration Legacy bleibt unverändert Polling oder Callback-Abhängigkeit

Die Muster können kombiniert werden: Ein Strangler Fig Ansatz nutzt oft Anti-Corruption Layer für saubere Datenmodellierung und Legacy Adapter für die Integration noch nicht ersetzte Systeme.

38.1.6 Praktische Umsetzung in der E-Commerce-Migration

Ein typisches Migrationsszenario kombiniert alle drei Muster:

  1. Phase 1: Legacy Adapter integriert das bestehende System in die Event-Architektur
  2. Phase 2: Anti-Corruption Layer bereinigt schrittweise die Datenmodelle
  3. Phase 3: Strangler Fig Pattern ersetzt Komponente für Komponente
  4. Phase 4: Legacy-System wird stillgelegt
// Koordination der Migrationsstrategie
@Configuration
public class MigrationCoordinator {
    
    @Bean
    @ConditionalOnProperty(name = "migration.phase", havingValue = "integration")
    public LegacyOrderAdapter legacyAdapter() {
        return new LegacyOrderAdapter();
    }
    
    @Bean
    @ConditionalOnProperty(name = "migration.phase", havingValue = "transformation")
    public OrderTranslationService translationService() {
        return new OrderTranslationService();
    }
    
    @Bean
    @ConditionalOnProperty(name = "migration.phase", havingValue = "replacement")
    public StranglerFigOrderController stranglerController() {
        return new StranglerFigOrderController();
    }
}

Diese Patterns bilden das Fundament für eine kontrollierte Migration zu Event-Driven Architecture, ohne dabei den laufenden Betrieb zu gefährden oder bestehende Systeme abrupt zu ersetzen.