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.
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.
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.
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)Die Umstellung erfolgt in kontrollierten Phasen:
@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;
}
}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.
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]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());
}
}
}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.
@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'])
)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);
}
}| 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.
Ein typisches Migrationsszenario kombiniert alle drei Muster:
// 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.