Zum Inhalt springen

Anti-Corruption Layer

Anti-Corruption Layer schützt nicht vor APIs. Er schützt vor fremden Weltbildern.

Ein Anti-Corruption Layer ist keine akademische DDD-Dekoration.

Er ist die Stelle im System, an der man sagt:

Schön, dass ihr ein Modell habt.
Aber es ist nicht unseres.

Und genau das ist der Punkt.

Viele Systeme gehen nicht kaputt, weil eine externe API schlecht ist. Sie gehen kaputt, weil deren Begriffe, Zustände, IDs, Fehlercodes, Workflows und Kompromisse ungebremst in die eigene Domäne einwandern.

Erst ist es nur ein DTO.

Dann ist es ein Interface.

Dann ist es ein Use Case.

Dann heißt plötzlich die eigene Fachlogik externalCustomerStatusCode.

Und irgendwo sitzt ein Entwickler, starrt auf status = 7 und sagt den Satz, mit dem jede Architektur langsam stirbt:

Das kommt halt so vom Backend.

Nicht jede Schnittstelle verdient Vertrauen. Manche verdienen einen Übersetzer.

Der häufigste Einwand gegen Anti-Corruption Layer klingt harmlos:

Wozu der Aufwand?
Wir können das DTO doch direkt verwenden.

Natürlich kann man das.

Man kann auch Abwasserrohre direkt durch die Küche legen. Technisch fließt es.

Die Frage ist nicht, ob es funktioniert. Die Frage ist, was dadurch verunreinigt wird.

Ein externes Modell ist selten neutral. Es trägt Annahmen mit sich:

  • wie ein anderer Bereich über die Welt denkt
  • welche Altlasten dort existieren
  • welche Felder historisch gewachsen sind
  • welche Zustände nie sauber modelliert wurden
  • welche Workarounds in der API konserviert wurden
  • welche technischen Details versehentlich fachlich aussehen

Wenn dieses Modell direkt in die eigene Anwendung übernommen wird, übernimmt man nicht nur Daten.

Man übernimmt ein fremdes Weltbild.

Externe Modelle sind nicht falsch. Sie gehören nur woanders hin.

Abschnitt betitelt „Externe Modelle sind nicht falsch. Sie gehören nur woanders hin.“

Ein externer Service kann völlig sinnvoll gebaut sein.

Für seinen Kontext.

Das ist der Teil, der oft vergessen wird.

Ein Abrechnungssystem denkt in Rechnungen, Buchungsperioden, Mahnstufen und Zahlungsstatus. Ein Vertriebssystem denkt in Leads, Opportunities, Kontakten und Abschlusswahrscheinlichkeiten. Ein Support-System denkt in Tickets, Prioritäten, Eskalationen und SLA-Zeitfenstern.

Alle können über denselben Kunden sprechen.

Aber sie meinen nicht dasselbe Modell.

Der Kunde im CRM ist nicht automatisch der Kunde in der Abrechnung. Der Kunde in der Abrechnung ist nicht automatisch der Nutzer im Frontend. Und der Nutzer im Frontend ist nicht automatisch das Aggregate in der eigenen Domäne.

Wenn man diese Unterschiede ignoriert, spart man kurzfristig ein paar Mapper.

Langfristig bezahlt man mit einem System, das nicht mehr in den eigenen Begriffen sprechen kann.

Gleicher Name bedeutet nicht gleiches Modell.

Architekturkorruption klingt dramatisch.

In der Praxis sieht sie meistens langweilig aus.

Zum Beispiel so:

if (customer.statusCode === 3 || customer.statusCode === 7) {
showUpgradeBanner();
}

Oder so:

const isActive = user.externalState !== 'D' && user.billingFlag !== 'BLOCKED' && user.legacyValidUntil > today;

Oder so:

interface Project {
id: string;
name: string;
projectTypeId: number;
crmAccountId: string;
sapCustomerNumber: string;
isArchivedFlag: 'Y' | 'N';
}

Das Problem ist nicht, dass dieser Code technisch nicht läuft.

Das Problem ist, dass die Anwendung irgendwann nicht mehr beschreibt, was fachlich passiert. Sie beschreibt, wie irgendein anderes System seine Daten liefert.

Dann werden Fachfragen zu Ratespielen:

Wann ist ein Kunde aktiv?

Antwort:

Wenn statusCode 3 ist, außer bei migrierten Kunden aus System B, dann 7, aber nicht wenn billingFlag gesetzt ist, außer das Feature kommt aus dem Partnerportal.

Herzlichen Glückwunsch.

Die Domäne wurde erfolgreich durch einen Exportvertrag ersetzt.

Direkte DTO-Nutzung fühlt sich am Anfang schnell an.

Kein Mapping. Keine extra Typen. Kein scheinbarer Overhead.

Der Code sieht schlank aus.

Bis die externe Schnittstelle sich ändert.

Bis ein zweiter externer Provider dazukommt.

Bis das Frontend Sonderlogik braucht.

Bis Tests anfangen, riesige externe Payloads nachzubauen.

Bis niemand mehr weiß, ob ein Feld fachlich relevant oder nur technisch durchgereicht ist.

Dann wird aus „nur Mapping“ plötzlich ein flächendeckender Umbau.

Nicht an einer Stelle.

Überall.

In Komponenten. Stores. Services. Tests. Formularen. Guards. Resolvern. Tabellen. Filtern. Berechtigungslogik. Validierung. Fehlermeldungen.

Das ist der Moment, in dem man erkennt:

Man hat sich Arbeit nicht gespart.

Man hat sie verteilt.

Was nicht an der Grenze übersetzt wird, wird überall sonst interpretiert.

Ein Anti-Corruption Layer ist keine magische Architekturwand.

Er ist eine bewusste Übersetzungsschicht zwischen zwei Modellen.

Er nimmt fremde Strukturen entgegen und erzeugt eigene Begriffe.

Nicht:

ExternalCustomerDto;

überall durchreichen.

Sondern:

CustomerProfile;

für die eigene Anwendung formen.

Nicht:

statusCode: 7;

in der UI prüfen.

Sondern:

accountState: 'active' | 'restricted' | 'closed';

in der eigenen Sprache modellieren.

Nicht:

errorCode: 'X-991-A';

bis zur Komponente schleifen.

Sondern:

CustomerProfileUnavailable;

oder:

CustomerRequiresManualReview;

als fachlich verständlichen Fall behandeln.

Das klingt banal.

Ist es aber nicht.

Denn genau an dieser Stelle entscheidet das Team, welche fremden Details relevant sind, welche ignoriert werden und welche in eigene Konzepte übersetzt werden müssen.

Das ist Architekturarbeit.

Nicht Tipparbeit.

Ein häufiger Selbstbetrug sieht so aus:

Wir haben doch Mapper.

Ja.

Aber ein Mapper, der nur Feldnamen umbenennt, schützt wenig.

function mapCustomer(dto: CustomerDto): Customer {
return {
id: dto.id,
name: dto.name,
status: dto.status,
type: dto.type,
};
}

Das ist syntaktische Kosmetik.

Ein Anti-Corruption Layer macht mehr:

  • Er entscheidet, welche externen Felder überhaupt in das eigene Modell dürfen.
  • Er übersetzt technische Codes in fachliche Zustände.
  • Er kapselt externe Fehlerbilder.
  • Er schützt die eigene Domäne vor optionalem Payload-Chaos.
  • Er verhindert, dass fremde IDs und Workflows zur inneren Architektur werden.
  • Er macht externe Besonderheiten explizit statt diffus.

Ein guter ACL ist deshalb nicht einfach eine Datei namens mapper.ts.

Er ist eine Grenze.

Und Grenzen sind nur dann hilfreich, wenn sie kontrollieren, was hindurch darf.

Umbenennen ist kein Schutz. Übersetzen schon.

Frontend: Der Ort, an dem Korruption besonders schnell sichtbar wird

Abschnitt betitelt „Frontend: Der Ort, an dem Korruption besonders schnell sichtbar wird“

Im Frontend sieht man Architekturkorruption oft früher als im Backend.

Nicht, weil Frontends grundsätzlich chaotischer sind.

Sondern weil Frontends viele Welten gleichzeitig berühren:

  • API-Verträge
  • UI-Zustände
  • Formularlogik
  • Validierung
  • Berechtigungen
  • Routing
  • Fehlermeldungen
  • i18n
  • Nutzererwartungen
  • Feature-Flags
  • Ladezustände

Wenn externe Modelle hier direkt landen, werden Komponenten schnell zu kleinen Dolmetschern mit Burnout.

Dann steht in einer Komponente nicht mehr:

canEditProject(project);

sondern:

project.typeId !== 4 && project.externalState !== 'LOCKED' && user.roles.includes('PROJECT_ADMIN') && project.sourceSystem !== 'LEGACY_X';

Das ist nicht „nah an der UI“.

Das ist fachliche Logik im Tarnanzug.

Und natürlich wird sie kopiert. Einmal in die Liste. Einmal ins Detail. Einmal ins Kontextmenü. Einmal in den Button. Einmal in den Test.

Danach fragt jemand, warum das Refactoring so lange dauert.

Gerade in Angular-, React- oder Vue-Anwendungen lohnt sich ein eigenes Modell.

Nicht als Dogma.

Sondern weil UI nicht automatisch API ist.

Eine API liefert vielleicht:

{
"id": "123",
"state": "A",
"validUntil": "2026-12-31",
"permissions": ["P_EDIT", "P_ARCHIVE"],
"flags": {
"migrated": true,
"billingBlocked": false
}
}

Die Oberfläche braucht aber vielleicht:

{
id: '123',
availability: 'usable',
actions: {
canEdit: true,
canArchive: false
},
warnings: ['migratedAccount']
}

Das ist nicht redundant.

Das ist eine andere Sicht auf dieselbe Realität.

Die API beschreibt Lieferung.

Das Frontend-Modell beschreibt Verwendung.

Wer diese beiden Dinge verwechselt, baut Oberflächen, die API-Verträge visualisieren, statt Nutzerprobleme zu lösen.

Die API liefert Daten. Die Anwendung braucht Bedeutung.

Es gibt nicht die eine perfekte Stelle.

Ein Anti-Corruption Layer kann je nach Architektur unterschiedlich aussehen:

  • im Backend zwischen fremdem Service und eigener Domäne
  • im API-Gateway zwischen Legacy-System und Frontend
  • im Frontend zwischen HTTP-Client und Store
  • in einer BFF-Schicht
  • in einem Adapter um SDKs oder externe Clients
  • in einem Importprozess
  • in einem Consumer für Events

Wichtig ist nicht der Name des Ordners.

Wichtig ist die Richtung der Abhängigkeit.

Die eigene Domäne sollte nicht gezwungen sein, in Begriffen des fremden Systems zu denken.

Ein brauchbarer Test ist simpel:

Wenn der externe Provider morgen ersetzt wird, welche Teile unseres Systems müssen fachlich umdenken?

Wenn die Antwort lautet „eigentlich alles“, dann war die Grenze keine Grenze.

Dann war sie nur ein dekorativer Zaun mit offenen Toren.

Anti-Corruption Layer heißt nicht: alles abstrahieren

Abschnitt betitelt „Anti-Corruption Layer heißt nicht: alles abstrahieren“

Natürlich kann man es übertreiben.

Nicht jede API braucht einen ausgewachsenen Schutzwall.

Nicht jede kleine Datenstruktur muss in fünf Modelle zerlegt werden.

Nicht jeder GET /countries-Call verdient ein DDD-Seminar mit Beamer und kaltem Kaffee.

Die entscheidende Frage ist Risiko.

Ein Anti-Corruption Layer lohnt sich besonders, wenn:

  • das externe Modell fachlich anders denkt als die eigene Anwendung
  • das externe System Legacy-Altlasten enthält
  • die API instabil ist
  • mehrere Provider möglich sind
  • fachliche Entscheidungen aus den Daten abgeleitet werden
  • die Payload viele technische Codes, Flags oder Sonderfälle enthält
  • externe Begriffe sonst in UI, Tests und Use Cases landen würden
  • das Team langfristig unabhängig vom Provider bleiben muss

Bei trivialen Daten kann ein einfacher Adapter reichen.

Bei fachlich kritischen Integrationen ist direkte DTO-Nutzung dagegen ein Kredit mit schlechtem Zinssatz.

Man bekommt Geschwindigkeit sofort.

Die Rechnung kommt später.

Mit Zinsen.

Nicht jede Grenze braucht eine Mauer. Aber jede kritische Grenze braucht Kontrolle.

Der Klassiker:

Man baut erst alles direkt gegen die API.

Dann kommen Sonderfälle.

Dann kommen Mapper.

Dann kommen Helper.

Dann kommen Utils.

Dann kommen normalizeCustomer, enhanceCustomer, mapCustomerForUi, mapCustomerForDetail, mapCustomerForEdit, mapCustomerButDifferent.

Und irgendwann besteht die halbe Anwendung aus verstreuter Übersetzungslogik, aber niemand nennt es Architektur.

Das Problem ist nicht, dass Mapping existiert.

Das Problem ist, dass Mapping überall existiert.

Ein Anti-Corruption Layer macht diese Arbeit sichtbar, zentral und testbar.

Er zwingt das Team, Entscheidungen an der Grenze zu treffen:

  • Welche externen Zustände kennen wir?
  • Welche davon sind für uns relevant?
  • Was ist unser eigener Begriff dafür?
  • Was passiert bei unbekannten Codes?
  • Welche Daten garantieren wir nach innen?
  • Welche externen Fehler werden zu welchen fachlichen Fällen?
  • Welche Sonderfälle bleiben bewusst an der Grenze?

Das ist nicht schwer, weil TypeScript kompliziert wäre.

Das ist schwer, weil man denken muss.

Leider lässt sich Denken nicht mit npm install architecture nachrüsten.

Ein weiterer Einwand:

Dann müssen wir ja noch mehr testen.

Ja.

Aber an der richtigen Stelle.

Ohne Anti-Corruption Layer müssen Tests oft externe Payloads imitieren. Komponenten-Tests kennen plötzlich API-Codes. Store-Tests brauchen Legacy-Flags. Use-Case-Tests arbeiten mit Datenstrukturen, die fachlich niemand erklären kann.

Mit ACL testet man die Übersetzung einmal gezielt:

it('maps blocked billing flag to restricted account state', () => {
const result = mapCustomerDtoToProfile({
statusCode: 3,
billingFlag: 'BLOCKED',
});
expect(result.accountState).toBe('restricted');
});

Danach können innere Tests mit dem eigenen Modell arbeiten.

Das ist der Unterschied zwischen:

Wir testen überall dieselbe Fremdlogik mit.

und:

Wir testen die Grenze einmal sauber und innen unsere eigene Fachlichkeit.

Das ist nicht mehr Testaufwand.

Das ist weniger Verunreinigung.

Teste die Übersetzung an der Grenze – nicht den Fremdvertrag in jeder Komponente.

Management-Perspektive: Warum das nicht nur Entwicklerästhetik ist

Abschnitt betitelt „Management-Perspektive: Warum das nicht nur Entwicklerästhetik ist“

Anti-Corruption Layer werden gerne als Entwickler-Vorliebe abgetan.

So nach dem Motto:

Die wollen wieder schöne Architektur bauen.

Ja, schlimm.

Entwickler möchten verhindern, dass das System in sechs Monaten aussieht wie ein schlecht gelüfteter Keller voller Adapterkabel. Sehr exzentrisch.

Aus Management-Sicht geht es aber um etwas anderes:

  • Wechselbarkeit von externen Systemen
  • geringere Abhängigkeit von Lieferanten
  • bessere Planbarkeit bei API-Änderungen
  • weniger Regressionen durch externe Modelländerungen
  • klarere Verantwortlichkeiten zwischen Teams
  • schnellere Weiterentwicklung der eigenen Fachlogik
  • weniger versteckte Kopplung

Direkte Integration ist oft eine Wette:

Dieses externe Modell bleibt stabil, passend und fachlich richtig für uns.

Das kann stimmen.

Aber wenn es nicht stimmt, ist die Kopplung bereits überall.

Ein ACL ist keine Versicherung gegen Änderung.

Aber er begrenzt den Schadensbereich.

Und genau darum geht es in Architektur.

Nicht darum, jede Änderung zu verhindern.

Sondern darum, dass eine Änderung nicht das halbe Gebäude mitnimmt.

Ein einfacher Aufbau reicht oft aus:

feature/
data-access/
external-customer.dto.ts
customer.api.ts
customer-acl.mapper.ts
domain/
customer-profile.ts
customer-state.ts
application/
customer.facade.ts
customer.store.ts
ui/
customer-card.component.ts
customer-detail.component.ts

Die Regel:

Externe DTOs bleiben in data-access.

Die Anwendung spricht mit eigenen Modellen.

Die UI sieht keine API-Codes.

Die Fachlogik prüft keine Legacy-Flags.

Tests außerhalb der Grenze verwenden keine externen Payloads.

Das ist kein Hexenwerk.

Aber es ist eine bewusste Entscheidung gegen die bequeme Durchreichung.

Und genau diese Entscheidung trennt Architektur von Dateisortierung.

Externe API:

interface ExternalSubscriptionDto {
id: string;
status: 'A' | 'I' | 'S' | 'X';
paymentState: 'OK' | 'FAILED' | 'UNKNOWN';
migratedFlag?: boolean;
}

Eigene Anwendung:

type SubscriptionAvailability = 'usable' | 'temporarilyBlocked' | 'inactive' | 'requiresReview';
interface Subscription {
id: string;
availability: SubscriptionAvailability;
warnings: SubscriptionWarning[];
}

ACL:

function mapSubscription(dto: ExternalSubscriptionDto): Subscription {
return {
id: dto.id,
availability: mapAvailability(dto),
warnings: mapWarnings(dto),
};
}
function mapAvailability(dto: ExternalSubscriptionDto): SubscriptionAvailability {
if (dto.status === 'X') {
return 'requiresReview';
}
if (dto.paymentState === 'FAILED') {
return 'temporarilyBlocked';
}
if (dto.status === 'I') {
return 'inactive';
}
return 'usable';
}

Das Entscheidende ist nicht der Code.

Das Entscheidende ist, dass die innere Anwendung jetzt nicht mehr wissen muss, was X, FAILED und migratedFlag historisch bedeuten.

Sie arbeitet mit Begriffen, die für sie relevant sind.

Bei jeder externen Integration sollte ein Team sich fragen:

Wollen wir dieses Modell wirklich in unserem Inneren haben?

Nicht:

Können wir es verwenden?

Natürlich können wir.

Wir können fast alles verwenden.

Die Frage ist, ob wir es später noch loswerden.

Ein Anti-Corruption Layer ist die Architekturantwort auf genau diese Frage.

Er sagt nicht:

Wir vertrauen niemandem.

Er sagt:

Wir übernehmen Verantwortung für unsere eigenen Begriffe.

Das ist ein riesiger Unterschied.

Ein Anti-Corruption Layer fehlt oft, wenn man solche Dinge hört:

Das Feld heißt komisch, aber das kommt halt so.

Den Status muss man mit der Tabelle im Wiki interpretieren.

Die Komponente braucht leider das komplette Response-Objekt.

Für den neuen Provider müssen wir nur ein paar Stellen anfassen.

Wir haben die Logik schon in drei Komponenten, aber leicht anders.

Der Test braucht diese riesige JSON-Datei, sonst läuft er nicht.

Das Flag bedeutet meistens aktiv.

Bei „meistens aktiv“ sollte übrigens irgendwo im Gebäude ein leises Alarmsignal losgehen.

Nicht laut.

Nur so laut, dass niemand später behaupten kann, er habe es nicht gehört.

Wenn Fachlichkeit in Kommentaren erklärt werden muss, fehlt oft ein Modell.

Die Alternative ist nicht, aus jedem DTO ein religiöses Ereignis zu machen.

Die Alternative ist bewusste Modellgrenze.

Pragmatisch heißt das:

  1. Externe Verträge als externe Verträge behandeln.
  2. Eigene Modelle für eigene Use Cases definieren.
  3. Übersetzung an einer klaren Grenze bündeln.
  4. Fremde Codes und Flags in eigene Bedeutung übersetzen.
  5. Unbekannte und kaputte Fälle explizit behandeln.
  6. Tests auf die Grenze konzentrieren.
  7. Innen keine externen Details durchreichen.

Das ist keine Überarchitektur.

Das ist Hygiene.

Und wie bei Hygiene merkt man den Wert oft erst, wenn sie fehlt.

Ein Anti-Corruption Layer schützt nicht davor, Daten von außen zu bekommen.

Er schützt davor, die eigene Sprache zu verlieren.

Denn sobald fremde Modelle ungefiltert in Komponenten, Stores, Use Cases und Tests landen, wird die eigene Anwendung abhängig von Begriffen, die sie nicht kontrolliert.

Das ist am Anfang bequem.

Später ist es teuer.

Gute Architektur bedeutet nicht, jede externe Integration kompliziert zu machen.

Gute Architektur bedeutet, an kritischen Grenzen bewusst zu entscheiden:

Was übernehmen wir? Was übersetzen wir? Was bleibt draußen?

Der Anti-Corruption Layer ist genau dieser Punkt.

Nicht mehr.

Aber auch nicht weniger.

Was du an der Grenze nicht übersetzt, erklärt später dein ganzes System.

Nicht externe APIs korrumpieren dein System.

Ungeprüfte Übernahme ihrer Modelle tut es.