Legacy-Integration
API-Wrapper für Legacy-Systeme bauen: Modernisierung ohne Risiko
Das Legacy-System läuft. Seit 15 Jahren. Keiner traut sich ran. Aber plötzlich soll es mit der neuen App reden, Daten an den externen Partner liefern, oder ins moderne Ökosystem integriert werden. Die Lösung: Ein API-Wrapper, der das alte System kapselt, ohne es anzufassen. Klingt einfach – hat aber Tücken.
Wann ist ein API-Wrapper die richtige Wahl?
Nicht jedes Legacy-System braucht einen Wrapper. Manchmal ist ein Rewrite sinnvoller, manchmal reicht eine direkte Datenbankanbindung. Ein Wrapper macht Sinn, wenn:
- Das System stabil läuft – und niemand das Risiko eingehen will, es anzufassen
- Mehrere Clients zugreifen sollen – Mobile App, Partner-API, internes Dashboard
- Die Geschäftslogik komplex ist – und im Legacy-Code steckt, den niemand neu schreiben will
- Schrittweise Migration geplant ist – der Wrapper wird zum Anti-Corruption Layer
- Dokumentation fehlt – der Wrapper erzwingt eine definierte Schnittstelle
Architektur-Patterns
1. Direkter Datenbank-Wrapper
Der einfachste Ansatz: Der Wrapper greift direkt auf die Legacy-Datenbank zu und stellt die Daten über eine API bereit.
// PHP-Beispiel: Direkter DB-Wrapper
class LegacyCustomerApi {
private PDO $legacyDb;
public function __construct(PDO $legacyDb) {
$this->legacyDb = $legacyDb;
}
public function getCustomer(int $id): array {
// Legacy-Tabelle hat kryptische Spaltennamen
$stmt = $this->legacyDb->prepare("
SELECT KDNR as id,
KDNAME as name,
KDORT as city,
KDPLZ as zip,
KDSTR as street,
KDUMSATZ as revenue
FROM STAMM_KD
WHERE KDNR = ?
");
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
throw new NotFoundException("Customer not found");
}
// Transformation in modernes Format
return [
'id' => (int) $row['id'],
'name' => trim($row['name']),
'address' => [
'street' => trim($row['street']),
'zip' => trim($row['zip']),
'city' => trim($row['city'])
],
'revenue' => (float) $row['revenue']
];
}
}
Klare Linie: Direkter DB-Zugriff ist nur vertretbar für read-only Szenarien oder Reporting – niemals für schreibende Geschäftsprozesse.
2. Service-Wrapper (über bestehende Schnittstellen)
Besser, wenn das Legacy-System bereits Schnittstellen hat – auch wenn sie alt sind (SOAP, XML-RPC, proprietäre Protokolle):
// Wrapper um einen SOAP-Service
class LegacyOrderWrapper {
private SoapClient $soapClient;
public function __construct(string $wsdlUrl) {
$this->soapClient = new SoapClient($wsdlUrl, [
'trace' => true,
'exceptions' => true,
'connection_timeout' => 30
]);
}
public function createOrder(array $orderData): array {
// Transformation: modernes JSON -> Legacy SOAP
$legacyRequest = $this->transformToLegacyFormat($orderData);
try {
$response = $this->soapClient->BestellungAnlegen($legacyRequest);
// Transformation: Legacy Response -> modernes JSON
return $this->transformToModernFormat($response);
} catch (SoapFault $e) {
// Legacy-Fehlercodes in HTTP-Status übersetzen
throw $this->mapLegacyError($e);
}
}
private function transformToLegacyFormat(array $data): object {
return (object) [
'BESTELLKOPF' => (object) [
'KDNR' => $data['customerId'],
'BESTDAT' => date('Ymd'),
'LIEFART' => $this->mapDeliveryType($data['delivery'])
],
'BESTELLPOS' => array_map(fn($item) => (object) [
'ARTNR' => $item['sku'],
'MENGE' => $item['quantity'],
'EPREIS' => $item['price']
], $data['items'])
];
}
}
Dieser Ansatz respektiert die Geschäftslogik des Legacy-Systems – Sie rufen dieselben Operationen auf, die auch die Legacy-UI nutzt.
3. Screen Scraping (wenn nichts anderes geht)
Manchmal gibt es keine API, keinen Datenbankzugang, nur eine alte Web-Oberfläche. Dann bleibt Screen Scraping:
// Screen Scraping als letzte Option
class LegacyScreenWrapper {
private HttpClient $client;
private string $sessionCookie;
public function login(string $user, string $pass): void {
$response = $this->client->post('/login.asp', [
'form_params' => [
'USER' => $user,
'PASS' => $pass
]
]);
// Session-Cookie extrahieren
$this->sessionCookie = $this->extractSessionCookie($response);
}
public function getInventory(string $productId): array {
$response = $this->client->get('/inventory.asp', [
'query' => ['ARTNR' => $productId],
'headers' => ['Cookie' => $this->sessionCookie]
]);
$html = $response->getBody()->getContents();
// HTML parsen - fragil, aber manchmal alternativlos
preg_match('/Bestand:\s*(\d+)/', $html, $matches);
$stock = (int) ($matches[1] ?? 0);
preg_match('/Lagerort:\s*([A-Z0-9-]+)/', $html, $matches);
$location = $matches[1] ?? 'UNKNOWN';
return [
'productId' => $productId,
'stock' => $stock,
'location' => $location
];
}
}
Der Anti-Corruption Layer
Ein API-Wrapper ist mehr als nur Protokoll-Übersetzung. Er sollte als Anti-Corruption Layer (ACL) fungieren – eine Schutzschicht, die verhindert, dass Legacy-Konzepte in Ihre moderne Architektur durchsickern.
Domain-Übersetzung
Das Legacy-System hat andere Begriffe, andere Strukturen, andere Annahmen:
class CustomerMapper {
// Legacy kennt nur "KDTYP" mit Werten 1, 2, 3
// Modern kennt "customerType" mit business, private, partner
private const TYPE_MAP = [
1 => 'private',
2 => 'business',
3 => 'partner'
];
public function toModern(array $legacyCustomer): Customer {
return new Customer(
id: new CustomerId($legacyCustomer['KDNR']),
name: $this->parseName($legacyCustomer['KDNAME']),
type: CustomerType::from(self::TYPE_MAP[$legacyCustomer['KDTYP']] ?? 'private'),
address: $this->parseAddress($legacyCustomer),
// Legacy speichert Umsatz in Pfennig (ja, wirklich)
revenue: Money::fromCents($legacyCustomer['KDUMSATZ'])
);
}
private function parseName(string $legacyName): CustomerName {
// Legacy: "Müller, Hans" oder "Firma XYZ GmbH"
if (str_contains($legacyName, ',')) {
[$lastName, $firstName] = explode(',', $legacyName, 2);
return CustomerName::person(trim($firstName), trim($lastName));
}
return CustomerName::company($legacyName);
}
}
Fehler-Übersetzung
Legacy-Systeme haben oft kryptische Fehlercodes. Der Wrapper übersetzt sie in verständliche HTTP-Responses:
class LegacyErrorMapper {
private const ERROR_MAP = [
'E001' => ['status' => 404, 'message' => 'Customer not found'],
'E002' => ['status' => 400, 'message' => 'Invalid customer number format'],
'E017' => ['status' => 409, 'message' => 'Customer has open orders, cannot delete'],
'E099' => ['status' => 503, 'message' => 'Legacy system temporarily unavailable'],
// Die Klassiker:
'FEHLER IM SYSTEM' => ['status' => 500, 'message' => 'Internal server error'],
'ZUGRIFF VERWEIGERT' => ['status' => 403, 'message' => 'Access denied'],
];
public function map(string $legacyError): ApiException {
$mapped = self::ERROR_MAP[$legacyError] ?? [
'status' => 500,
'message' => 'Unknown legacy error: ' . $legacyError
];
return new ApiException(
$mapped['message'],
$mapped['status'],
['legacyCode' => $legacyError]
);
}
}
Performance-Optimierung
Legacy-Systeme sind selten performant. Der Wrapper kann das teilweise kompensieren:
Caching
class CachedLegacyWrapper {
private LegacyApi $legacy;
private CacheInterface $cache;
public function getProduct(string $sku): array {
$cacheKey = "product:{$sku}";
return $this->cache->get($cacheKey, function() use ($sku) {
// Legacy-Call nur wenn nicht im Cache
$product = $this->legacy->getProduct($sku);
// TTL abhängig von Datentyp
// Produktstammdaten: länger cachen
// Bestandsdaten: kürzer oder gar nicht
return $product;
}, ttl: 3600);
}
public function getStock(string $sku): array {
// Bestand nicht cachen - muss live sein
return $this->legacy->getStock($sku);
}
}
Request Batching
class BatchingWrapper {
private array $pendingRequests = [];
private LegacyApi $legacy;
public function getCustomer(int $id): Promise {
// Request sammeln statt sofort ausführen
$deferred = new Deferred();
$this->pendingRequests[] = [
'type' => 'customer',
'id' => $id,
'deferred' => $deferred
];
return $deferred->promise();
}
public function flush(): void {
if (empty($this->pendingRequests)) {
return;
}
// Alle Customer-IDs sammeln
$customerIds = array_column(
array_filter($this->pendingRequests, fn($r) => $r['type'] === 'customer'),
'id'
);
// Ein Batch-Call statt vieler Einzelcalls
$customers = $this->legacy->getCustomersBatch($customerIds);
// Ergebnisse verteilen
foreach ($this->pendingRequests as $request) {
if ($request['type'] === 'customer') {
$request['deferred']->resolve($customers[$request['id']] ?? null);
}
}
$this->pendingRequests = [];
}
}
Sicherheits-Aspekte
Der Wrapper ist eine Angriffsfläche. Er muss sicherer sein als das Legacy-System dahinter:
Input-Validierung
class SecureWrapper {
public function getCustomer(int $id): array {
// Validierung VOR dem Legacy-Call
if ($id <= 0 || $id > 999999999) {
throw new ValidationException('Invalid customer ID');
}
return $this->legacy->getCustomer($id);
}
public function searchCustomers(string $query): array {
// SQL Injection verhindern, auch wenn Legacy anfällig wäre
$sanitized = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\s-]/', '', $query);
if (strlen($sanitized) < 2) {
throw new ValidationException('Search query too short');
}
return $this->legacy->searchCustomers($sanitized);
}
}
Rate Limiting
class RateLimitedWrapper {
private RateLimiter $limiter;
public function createOrder(array $data): array {
$clientId = $this->getClientId();
// Legacy-System schützen vor Überlastung
if (!$this->limiter->allow($clientId, 'orders', limit: 100, window: 3600)) {
throw new TooManyRequestsException(
'Order limit exceeded. Max 100 orders per hour.'
);
}
return $this->legacy->createOrder($data);
}
}
Audit Logging
class AuditWrapper {
public function updateCustomer(int $id, array $data): array {
$before = $this->legacy->getCustomer($id);
$result = $this->legacy->updateCustomer($id, $data);
$this->logger->info('Customer updated via API', [
'customerId' => $id,
'clientId' => $this->getClientId(),
'changes' => $this->diff($before, $result),
'timestamp' => time(),
'ip' => $this->getClientIp()
]);
return $result;
}
}
Testing-Strategien
Contract Tests
Der Wrapper muss stabil bleiben, auch wenn sich das Legacy-System ändert:
class LegacyContractTest extends TestCase {
/**
* Dieser Test schlägt fehl, wenn das Legacy-System
* sein Antwortformat ändert
*/
public function testCustomerResponseFormat(): void {
$response = $this->legacyApi->getCustomer(12345);
// Pflichtfelder müssen vorhanden sein
$this->assertArrayHasKey('KDNR', $response);
$this->assertArrayHasKey('KDNAME', $response);
$this->assertArrayHasKey('KDTYP', $response);
// Datentypen müssen stimmen
$this->assertIsNumeric($response['KDNR']);
$this->assertIsString($response['KDNAME']);
$this->assertContains($response['KDTYP'], [1, 2, 3]);
}
}
Integration Tests gegen Testdaten
class WrapperIntegrationTest extends TestCase {
/**
* Test gegen bekannte Testdaten im Legacy-System
*/
public function testGetKnownCustomer(): void {
// Testkunde existiert im Legacy-System
$result = $this->wrapper->getCustomer(99999);
$this->assertEquals('Test GmbH', $result['name']);
$this->assertEquals('business', $result['type']);
$this->assertEquals('12345', $result['address']['zip']);
}
}
Monitoring & Alerting
Ein Wrapper ohne Monitoring ist eine Zeitbombe:
class MonitoredWrapper {
private MetricsCollector $metrics;
public function getCustomer(int $id): array {
$start = microtime(true);
try {
$result = $this->legacy->getCustomer($id);
$this->metrics->timing('legacy.customer.get', microtime(true) - $start);
$this->metrics->increment('legacy.customer.get.success');
return $result;
} catch (LegacyException $e) {
$this->metrics->increment('legacy.customer.get.error', [
'error_code' => $e->getCode()
]);
// Alert bei kritischen Fehlern
if ($e->getCode() === 'E099') {
$this->alerting->critical('Legacy system unavailable');
}
throw $e;
}
}
}
Wichtige Metriken:
- Response Time: Wird das Legacy-System langsamer?
- Error Rate: Steigen die Fehler?
- Request Volume: Überlastet der Wrapper das Legacy-System?
- Cache Hit Rate: Funktioniert das Caching?
Typische Fallstricke
1. Leaky Abstractions
Der häufigste Fehler: Legacy-Konzepte sickern durch den Wrapper durch.
Schlecht:
// Legacy-Feldnamen in der API
{
"KDNR": 12345,
"KDNAME": "Müller, Hans",
"KDTYP": 1
}
Besser:
// Moderne, selbsterklärende Struktur
{
"id": 12345,
"name": "Hans Müller",
"type": "private"
}
2. Wrapper-Bypass
Entwickler umgehen den Wrapper und greifen direkt auf die Legacy-Datenbank zu – „nur für diesen einen Report". Sechs Monate später gibt es zehn solcher Direktzugriffe, und der Wrapper ist wertlos.
Lösung: Netzwerk-Segmentierung. Die Legacy-Datenbank ist nur vom Wrapper-Service erreichbar.
3. Scope Creep
Der Wrapper wächst und wächst. Plötzlich enthält er Geschäftslogik, die eigentlich ins Legacy-System oder einen neuen Service gehört.
Lösung: Klare Regel: Der Wrapper übersetzt nur. Keine fachliche Geschäftslogik – nur technische Schutz- und Übersetzungslogik (Error Mapping, Rate Limiting, Input Validation).
4. Wartungsaufwand unterschätzen
Der Wrapper muss gepflegt werden. Wenn das Legacy-System Updates bekommt, muss der Wrapper angepasst werden. Wenn neue Felder hinzukommen, muss der Wrapper erweitert werden.
Lösung: Wrapper-Maintenance als festen Posten einplanen. Nicht als „macht man nebenbei".
Fazit
Ein API-Wrapper ist kein Allheilmittel, aber oft der pragmatischste Weg, Legacy-Systeme zu integrieren. Der Schlüssel liegt in:
- Klare Trennung: Der Wrapper übersetzt, nicht mehr
- Saubere Abstraktion: Keine Legacy-Konzepte nach außen durchlassen
- Robustheit: Caching, Rate Limiting, Error Handling
- Monitoring: Probleme erkennen, bevor User sie melden
- Dokumentation: Die API ist die neue Wahrheit
Zeitliche Perspektive: Ein Wrapper ist fast immer ein temporärer Baustein – mit einer Lebensdauer von Jahren, nicht Wochen. Planen Sie entsprechend.
Legacy-System integrieren?
Ich analysiere Ihre bestehende Systemlandschaft und entwickle eine Integrationsstrategie – pragmatisch, sicher, wartbar.
Kostenlose Erstanalyse anfragen