Alte und fremde Systeme anbinden
Kurz gesagt: Ein Anti-Corruption Layer übersetzt das Modell eines alten oder fremden Systems in deine Domäne, und ein Hub mit kanonischem Format bringt die Verbindungen zwischen n Systemen auf einen Adapter pro System.
Problem
Ein Lager von 2009 und die Buchhaltung in XML
Der Shop hat ein altes Lager: ein Programm von 2009, das sich niemand anzufassen traut. Es spricht CSV, mit ; und Codes: A heißt verfügbar, B gesperrt, und die Menge -1 heißt „wird gerade gezählt“. Die Buchhaltung will XML, mit der Summe samt Punkt geschrieben. Die Lieferung will JSON.
So sagen die Bestellungen allen dreien Bescheid:
class Orders(Endpoint warehouse, Endpoint accounting, Endpoint delivery){ public void Place(int id, string sku, int qty, long cents) { // das Lager von 2009: CSV und der Code OUT warehouse.Send($"{id};{sku};{qty};OUT"); // Buchhaltung: XML, die Summe mit Punkt var total = Euros(cents); accounting.Send( $"""<invoice no="{id}" total="{total}"/>"""); // Lieferung: JSON var json = $$"""{"order":{{id}},"sku":"{{sku}}"}"""; delivery.Send(json); }}var orders = new Orders(new Endpoint("Lager"), new Endpoint("Buchhaltung"), new Endpoint("Lieferung"));orders.Place(1042, "HP-01", 1, 5900);Lager ← 1042;HP-01;1;OUTBuchhaltung ← <invoice no="1042" total="59.00"/>Lieferung ← {"order":1042,"sku":"HP-01"}Es funktioniert. Aber die Bestellungen kennen jetzt drei fremde Formate, und die Codes des Lagers haben sich bis in den Checkout ausgebreitet: Irgendwo steht if (status == "B"). Auch der Katalog spricht mit dem Lager, die Verkäufer mit der Buchhaltung. Wenn das Lager einen neuen Status bekommt, weiß niemand, an wie vielen Stellen er suchen muss.
Die Idee
Ein Übersetzer an der Grenze
Ein fremdes System hat sein eigenes Modell: eigene Codes, eigene Einheiten, eigene seltsame Werte. Gelangt dieses Modell in die Domäne, fängt die Domäne an, in der Sprache des Lagers von 2009 zu denken, und ändert sich jedes Mal, wenn es das Lager tut.
Darum steht an der Grenze ein Übersetzer. Er liest das fremde Format und lässt nur die Begriffe der Domäne durch: Stock(sku, available). Nur der Übersetzer weiß, was A, B und -1 bedeuten. Das nennt man einen Anti-Corruption Layer.
Innen ist das ein Adapter, aber mit einer größeren Aufgabe: Ein Adapter ändert die Form einer Schnittstelle, ein Anti-Corruption Layer verhindert, dass ein ganzes Modell durchsickert. Er entscheidet, was jeder fremde Wert bedeutet und was mit dem Unbekannten geschieht. Die Regel: Das Unbekannte wird laut abgelehnt, nie geraten.
Sieh es dir an
Der Übersetzer im Code
record Stock(string Sku, int Available);
// die Grenze: nur hier weiß man, was A, B und -1 bedeutenclass WarehouseAcl{ public List<string> Rejected { get; } = [];
public List<Stock> Read(IEnumerable<string> lines) { var stock = new List<Stock>(); foreach (var line in lines) { var f = line.Split(';'); var sku = f[0]; var qty = int.Parse(f[1]); switch (f[2]) { case "A" when qty >= 0: stock.Add(new Stock(sku, qty)); break; case "A": Rejected.Add($"{sku}: unbekannte Menge"); break; case "B": stock.Add(new Stock(sku, 0)); break; default: Rejected.Add($"{sku}: unbekannter Status {f[2]}"); break; } } return stock; }}case "A" when qty >= 0 ist ein Fall mit einer Bedingung: "A" mit negativer Menge geht in den nächsten Fall weiter.Die Datei von heute Morgen:
// die Datei, die das alte Lager jede Stunde per FTP hochlädtstring[] file = ["HP-01;3;A", "CH-07;0;A", "KB-12;5;B", "MS-03;-1;A", "SP-02;2;R"];var acl = new WarehouseAcl();foreach (var s in acl.Read(file)) Console.WriteLine($"{s.Sku}: {s.Available}");foreach (var r in acl.Rejected) Console.WriteLine($"✗ {r}");HP-01: 3CH-07: 0KB-12: 0✗ MS-03: unbekannte Menge✗ SP-02: unbekannter Status RKB-12 ist gesperrt, also sind für die Domäne 0 verfügbar. MS-03 wird gezählt: Der Übersetzer weiß nicht, wie viele es sind, und erfindet keine Zahl. SP-02 hat den Status R, den niemand beschrieben hat. Die Zeile wird abgelehnt, und jemand fragt den Besitzer des Lagers, was er bedeutet.
Probier es aus
Finde den Fehler
Eine Kollegin schreibt einen kürzeren Übersetzer: „Alles, was nicht gesperrt ist, ist verkäuflich.“ Einen Monat später fängt das Lager an, den Status R zu schicken (reserviert für einen externen Verkäufer), und der Shop verkauft denselben Lautsprecher an zwei Kunden. Außerdem steht MS-03 mit -1 auf Lager auf der Seite. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Der Übersetzer rät, statt zu übersetzen: Jeder Status, der nicht B ist, wird zu „verfügbar“, und jede Menge, sogar -1, geht in die Domäne. Das Unbekannte muss laut abgelehnt werden, wie es WarehouseAcl oben tut: die bekannten Fälle aufgezählt und für alles andere eine Ablehnung.
Die Idee
Das Geflecht aus Verbindungen
Sechs Systeme: Bestellungen, Katalog, Lager, Buchhaltung, Lieferung und externe Verkäufer. Wenn jedes mit jedem spricht, sind es n(n-1)/2 = 15 Verbindungen, jede mit ihrer eigenen Übersetzung. Eine Änderung des Lagerformats betrifft 5 davon, in 5 Systemen, die zusammen ausgeliefert werden müssen.
Der andere Weg ist ein Hub mit einer kanonischen Form: eine gemeinsame Form der Nachricht (OrderPlaced), die alle verstehen. Jedes System verbindet sich einmal, mit einem Adapter zwischen seinem eigenen Format und dem kanonischen. Sechs Systeme, sechs Adapter. Eine Änderung des Lagerformats betrifft nur seinen Adapter: Dieser Adapter ist der Anti-Corruption Layer des Lagers.
record OrderPlaced(int Id, string Sku, int Qty, long Cents);
delegate string Adapter(OrderPlaced e);
class Hub{ readonly Dictionary<Endpoint, Adapter> adapters = [];
public void Connect(Endpoint system, Adapter adapter) => adapters[system] = adapter;
public void Publish(OrderPlaced e) { foreach (var (system, adapter) in adapters) system.Send(adapter(e)); }
public int Size => adapters.Count;}class Orders(Endpoint warehouse, Endpoint accounting, Endpoint delivery){ public void Place(int id, string sku, int qty, long cents) { // das Lager von 2009: CSV und der Code OUT warehouse.Send($"{id};{sku};{qty};OUT"); // Buchhaltung: XML, die Summe mit Punkt var total = Euros(cents); accounting.Send( $"""<invoice no="{id}" total="{total}"/>"""); // Lieferung: JSON var json = $$"""{"order":{{id}},"sku":"{{sku}}"}"""; delivery.Send(json); }}Schritt für Schritt, in Worten
- Die Bestellungen senden an drei Systeme, jedes in seiner eigenen Sprache: CSV ans Lager von 2009, XML an die Buchhaltung, JSON an die Lieferung. Die Bestellungen kennen alle drei Formate.
- So sprechen auch die anderen: der Katalog mit dem Lager, die Verkäufer mit der Buchhaltung. 6 Systeme, die alle miteinander sprechen, sind 15 Verbindungen, jede mit ihrer eigenen Übersetzung.
- Das Lager ändert sein Format. Jede Verbindung dorthin ändert sich: 5 Stellen in 5 Systemen, zusammen ausgeliefert.
- Ein Hub: Jedes System verbindet sich einmal, mit einem Adapter zwischen seinem Format und einer kanonischen Form. 6 Systeme, 6 Adapter.
- Die Bestellungen veröffentlichen OrderPlaced einmal und kennen weder CSV noch XML. Der Hub gibt jedem System seine eigene Übersetzung.
- Das Lager ändert sein Format erneut: Nur sein Adapter ändert sich. Er hält das alte Modell aus der Domäne heraus: ein Anti-Corruption Layer.
- Punkt-zu-Punkt: 15 Verbindungen, 5 Änderungen bei einem neuen Format. Hub: 6 Adapter, 1 Änderung. Der Preis: Der Hub ist ein weiteres System, das nicht ausfallen darf.
Sieh es dir an
Der Hub im Code
Der Adapter ist ein Delegate: Jedes Lambda, das ein OrderPlaced nimmt und Text zurückgibt, passt. Die Bestellungen veröffentlichen einmal, und die Adapter sitzen neben jedem System:
var hub = new Hub();hub.Connect(new Endpoint("Lager"), e => $"{e.Id};{e.Sku};{e.Qty};OUT");hub.Connect(new Endpoint("Buchhaltung"), e => $"""<invoice no="{e.Id}" total="{Euros(e.Cents)}"/>""");hub.Connect(new Endpoint("Lieferung"), e => $$"""{"order":{{e.Id}},"sku":"{{e.Sku}}"}""");// die Bestellungen veröffentlichen einmal, in der kanonischen Formhub.Publish(new OrderPlaced(1042, "HP-01", 1, 5900));Console.WriteLine($"Adapter: {hub.Size}");Lager ← 1042;HP-01;1;OUTBuchhaltung ← <invoice no="1042" total="59.00"/>Lieferung ← {"order":1042,"sku":"HP-01"}Adapter: 3Die Nachrichten sind dieselben wie am Anfang. Der Unterschied ist, wer das Format kennt: Die Bestellungen kennen CSV und XML nicht mehr, und für ein neues System kommt ein weiteres connect dazu, ohne dass die Bestellungen angefasst werden. Euros macht aus Cent einen Text mit Punkt: 5900 wird zu 59.00.
Probier es aus
Sag die Ausgabe voraus
foreach (var n in new[] { 3, 6, 10 }){ var links = n * (n - 1) / 2; Console.WriteLine($"{n} Systeme: {links} Verbindungen oder {n} Adapter");}Die Verbindungen sind die Paare von Systemen: jedes von n mit jedem der anderen n - 1, geteilt durch 2, weil die Verbindung A–B dasselbe ist wie B–A. Sie wachsen fast wie n², die Adapter wachsen wie n. Das ist der schlimmste Fall: In einem echten Shop spricht nicht jedes System mit jedem anderen. Aber schon 8 Verbindungen mit 8 verschiedenen Übersetzungen sind viel.
Die Idee
Synchron oder asynchron, und der Vertrag
Nicht jede Verbindung ist gleich:
| Synchron (Anfrage und Antwort) | Asynchron (Warteschlange, Datei) | |
|---|---|---|
| Wann | die Antwort wird jetzt gebraucht: „Ist es auf Lager?“ vor dem Verkauf | es kann warten: die Rechnungen bis zum Morgen, die Lagerdatei jede Stunde |
| Wenn die andere Seite langsam ist | du wartest auch, daher Timeout und Retry | die Nachricht wartet in der Warteschlange |
| Wenn die andere Seite ausfällt | du lehnst ab oder hast eine Ersatzantwort | sie holt auf, sobald sie wieder da ist |
Die kanonische Form ist ein Vertrag: Alle hängen von ihr ab. Sie ändert sich wie ein API-Vertrag: Ein neues Feld wird hinzugefügt, ohne die alten Adapter zu brechen, und ein Feld zu entfernen ist eine neue Version. Wenn der Vertrag Felder für das Lager und Felder nur für die Buchhaltung bekommt, gehört er allen und niemandem.
Die Idee
Wann der Hub schadet
- Bei zwei oder drei Systemen. Ein Hub ist ein weiteres System: Er wird ausgeliefert, überwacht, und er fällt aus. Bei wenigen Verbindungen ist eine direkte Verbindung mit einem Übersetzer billiger.
- Wenn Logik in ihn hineinkommt. Ein Hub, der Rabatte entscheidet oder nach Geschäftsregeln leitet, wird zu einem Ort, den kein Team besitzt und auf den jedes Team wartet. Der Hub transportiert und übersetzt; die Regeln bleiben in den Systemen.
- Wenn die kanonische Form alles will. Ein Modell, das die Bestellung für alle Systeme auf einmal beschreibt, braucht Monate bis zur Einigung, und dann traut sich niemand mehr, es anzufassen. Mehrere kleine Nachrichten (
OrderPlaced,StockChanged), jede mit einem klaren Besitzer, funktionieren besser. - Ein Anti-Corruption Layer ist nicht immer nötig. Ist das fremde Modell gut und stabil (zum Beispiel der Vertrag des Zahlungsanbieters) oder ein Modul deines eigenen Teams, ist der Übersetzer nur eine weitere Schicht, die du pflegen musst.
Genauer, für Neugierige
Echte Hubs und Broker: Apache Kafka, RabbitMQ, die Cloud-Warteschlangen (Amazon SQS, Azure Service Bus); für die Integration: Apache Camel, Spring Integration, MuleSoft, Kafka Connect. Der ältere Name ist ESB (Enterprise Service Bus). Sein schlechter Ruf kommt von den Bussen, in denen am Ende die gesamte Geschäftslogik lag.
Die Muster haben Namen. Das Buch „Enterprise Integration Patterns“ (Gregor Hohpe und Bobby Woolf, 2003) beschreibt den Message Translator, das Canonical Data Model, den Router und Dutzende weitere. Der Anti-Corruption Layer stammt aus „Domain-Driven Design“ von Eric Evans (2003).
Alte Systeme in der Praxis: Der Übersetzer bewahrt auch die Originaldatei auf, damit eine Zeile erneut abgespielt werden kann, sobald jemand herausfindet, was R bedeutet. Eine Datei über FTP wird erst gelesen, wenn sie vollständig hochgeladen ist (zum Beispiel signalisiert durch eine leere .done-Datei daneben). Die FTP-Server selbst, die Broker und das Netz dazwischen sind Infrastruktur und werden im DevOps-Kurs ZeroToOps gelehrt.
Synchron über einen Hub gibt es auch (ein API-Gateway vor mehreren Systemen), aber dann ist der Hub Teil jeder Anfrage: Ist er langsam, ist alles langsam.
Übung
Zurückgegebene Ware
Löse die Aufgabe in deinem Editor.
Das alte Lager lädt eine zweite Datei hoch: die zurückgegebene Ware. Eine Zeile lautet RET;Bestellung;Artikel;Anzahl;Datum, und das Datum hat die Form dd.MM.yyyy. Manchmal stehen in der Datei auch Zeilen aus der Inventur (INV;…), die keine Rückgaben sind. Schreibe einen Übersetzer, der der Domäne Rückgaben mit dem Datum im ISO-Format (2026-10-15) liefert und jede Zeile, die keine Rückgabe ist, laut ablehnt.
Die Eingabe ist RET;1042;HP-01;1;15.10.2026, RET;1043;CH-07;2;16.10.2026 und INV;HP-01;3.
Rückgabe 1042: HP-01 × 1 am 2026-10-15Rückgabe 1043: CH-07 × 2 am 2026-10-16✗ keine Rückgabe: INV;HP-01;3Weitere Aufgaben
- Externe Verkäufer. Verbinde auch die Verkäufer mit dem Hub: Sie wollen die Zeile
ORDER 1042 HP-01. Wie viele Zeilen ändern sich in den Bestellungen? - Die Buchhaltung antwortet. Die Buchhaltung schickt XML
<paid no="1042" cents="5900"/>zurück. Schreibe den Übersetzer nach innen: Die Domäne will eine Zahlung in Euro.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Ereignisse und Warteschlangen · API-Design: Vertrag, Versionen, wiederholte Anfragen · Wenn der Nachbar langsam ist: Timeout, Retry, Circuit Breaker · Eine Transaktion über Services hinweg (Saga)
- Geht weiter in: Ein laufendes System verändern (Strangler Fig, Expand–Contract) · Die Unternehmenslandschaft und ihre Capabilities · Deine erste Architekturaufgabe
- Aufgaben zum Thema: Der Architekt bei der Arbeit