post

Bounded Contexts: die Grenze vor dem Microservice

Bevor ich Services schneide, ziehe ich fachliche Grenzen: Der Bounded Context ist eine Sprachgrenze, und der spätere Serviceschnitt folgt ihr statt umgekehrt.

≈ 8 Min. Lesezeit

Bevor ich Services schneide, ziehe ich fachliche Grenzen. Der Bounded Context ist zuerst eine Sprachgrenze, und der spätere Serviceschnitt folgt ihr, nicht umgekehrt.

Warum die Reihenfolge über das Ergebnis entscheidet

In den meisten Migrationsprojekten, in die ich als Berater komme, ist die Frage schon falsch gestellt. Sie lautet: "In wie viele Microservices zerlegen wir den Monolithen?" Ich drehe sie um. Die Frage ist nicht, wie viele Services es werden, sondern wo die fachlichen Grenzen liegen – und ob wir für jede Grenze eine eigene, in sich konsistente Sprache benennen können. Wer zuerst die Anzahl der Services festlegt und danach die Fachlichkeit hineinpresst, bekommt verteilte Kopplung statt Entkopplung.

Der Grund ist unbequem, aber er spart später Monate. Ein Serviceschnitt ist billig zu zeichnen und teuer zu korrigieren, sobald Teams, Deployments und Datenbanken daran hängen. Eine fachliche Grenze dagegen lässt sich am Whiteboard verschieben, solange sie noch Modell ist und nicht Infrastruktur. Deshalb investiere ich die erste Woche eines solchen Projekts nicht in Infrastruktur- und Deployment-Diagramme, sondern in Sprache. Das Business-Ergebnis ist messbar: kürzere Änderungszyklen, weil eine Änderung in einem Kontext bleibt; weniger Reibung zwischen Teams, weil niemand über geteilte Tabellen stolpert; höhere Qualität, weil das Modell die Fachlichkeit abbildet statt einer technischen Zufallsstruktur.

Der Bounded Context ist eine Sprachgrenze

Eric Evans hat den Begriff 2003 in Domain-Driven Design geprägt, Martin Fowler hat ihn in seinem Bliki auf den Punkt gebracht: Ein Bounded Context ist der Bereich, in dem genau ein Modell und genau eine Ubiquitous Language gelten. Innerhalb dieser Grenze bedeutet jedes Wort exakt eine Sache. Über die Grenze hinweg darf dasselbe Wort etwas anderes heißen – und das ist kein Fehler, sondern der eigentliche Gewinn.

Ein Beispiel, das ich in fast jedem Handelsprojekt sehe: das Wort "Product". Im Verkauf ist ein Product ein Ding mit Preis, Beschreibung und Verfügbarkeit. Im Lager ist dasselbe Product eine SKU mit Bestand, Lagerort und Nachbestellpunkt. In der Buchhaltung ist es eine Position mit Steuersatz und Erlöskonto. Drei Kontexte, drei legitime Bedeutungen. Der klassische Fehler ist, daraus ein einziges kanonisches Product-Objekt bauen zu wollen, das allen dreien gerecht wird. Das Resultat ist eine Klasse mit vierzig Feldern, von denen jeder Kontext zehn ignoriert – und jede Änderung betrifft alle drei Teams.

Fowler nennt als dominanten Faktor beim Ziehen dieser Grenzen nicht die Technik, sondern "human culture" – Sprache und Organisation. Das ist Conway's Law an der Arbeit: Kontextgrenzen und Teamgrenzen beeinflussen sich gegenseitig. Wenn ich eine saubere Grenze ziehe, an der aber zwei Teams gemeinsam arbeiten müssen, ziehe ich sie am falschen Ort.

Praktisch teste ich eine vermutete Grenze mit zwei Fragen. Erstens: Kann ich in diesem Kontext einen Satz sagen, den ein Domänenexperte des Nachbarkontexts ohne Übersetzung nicht mehr eindeutig versteht? Wenn ja, sind es zwei Sprachen und damit zwei Kontexte. Zweitens: Gibt es eine Regel, die genau hier gilt und nebenan nicht – etwa "ein Product ohne Bestand ist im Verkauf trotzdem sichtbar, im Lager aber inexistent"? Solche Regelbrüche sind die verlässlichsten Grenzmarken, die ich kenne. Sie stehen im Fachgespräch, nicht im Klassendiagramm, und genau deshalb finde ich sie nur, wenn ich zuerst zuhöre statt zu modellieren.

Subdomains ordnen, Bounded Contexts implementieren

Bevor ich Grenzen ziehe, sortiere ich den Problemraum. Strategisches DDD trennt Problem Space und Solution Space. Im Problemraum liegen die Subdomains, und die kategorisiere ich in drei Klassen:

Die Core Domain ist das, was mein Kunde besser kann als der Wettbewerb – hier investiere ich das beste Team und die meiste Modellierungsarbeit. Supporting Subdomains sind notwendig, aber kein Differenzierungsmerkmal; hier reicht solide Standardarbeit. Generic Subdomains sind gelöste Probleme wie Authentifizierung, Buchhaltung oder Benachrichtigung; die kaufe oder standardisiere ich, statt sie selbst zu bauen.

Diese Einordnung ist eine Geschäftsentscheidung, keine technische. Sie beantwortet die Frage, wo Aufwand einen Return bringt. Bounded Contexts sind dann die Antwort im Lösungsraum: Idealerweise entspricht eine Subdomain genau einem Bounded Context. In der Realität ist das selten deckungsgleich – ein historisch gewachsenes System schneidet oft quer. Aber die Zuordnung Core/Supporting/Generic sagt mir, an welcher Grenze ich Präzision brauche und wo Pragmatismus genügt.

Die Kanten sind die Architektur, nicht die Kästen

Isolierte Modelle sind noch kein System. Das System entsteht an den Beziehungen zwischen den Kontexten, und diese Beziehungen zu benennen ist der Kern der Arbeit – das Context Mapping. Ohne Context Map entsteht genau das, was Evans einen "distributed Big Ball of Mud" nennt: viele Services, die alle voneinander abhängen, ohne dass jemand die Übersetzung kontrolliert.

Eine Context Map zeichne ich als Kästen mit Kanten. Die Kästen sind die Kontexte, jeder mit seiner eigenen Sprache. Interessant sind die Kanten: Wer ist upstream (bestimmt das Modell), wer downstream (muss sich anpassen)? Wer übersetzt, wer publiziert?

graph LR
  subgraph U["Upstream"]
    INV["Inventory<br/>Product = SKU + stock"]
  end
  subgraph D["Downstream"]
    SAL["Sales<br/>Product = price + description"]
    BIL["Billing<br/>Product = tax + revenue account"]
  end
  INV -->|"OHS / PL"| SAL
  SAL -->|"ACL"| BIL
  INV -.->|"SK shared kernel"| SAL

Die Beschriftungen tragen die Aussage. Ein Open Host Service mit Published Language (OHS/PL) auf der Upstream-Seite heißt: Inventory bietet eine stabile, dokumentierte Schnittstelle an, an die sich viele andocken können. Ein Anti-Corruption Layer (ACL) auf der Downstream-Seite heißt: Billing schützt sein Modell aktiv vor fremden Begriffen. Ein Shared Kernel (SK) heißt: Zwei Kontexte teilen sich bewusst ein kleines gemeinsames Modell – und damit Kopplung.

Der entscheidende Satz für meine Kunden lautet: Der spätere Serviceschnitt legt sich auf diese Karte. Erst existiert die Context Map, dann entscheide ich, welcher Kontext ein Service wird, welcher zwei, welche zusammen deployt bleiben. Der Bounded Context ist die Modellgrenze, der Service die Deployment-Einheit. Vladik Khononov hat 2018 treffend formuliert: Bounded Contexts sind nicht Microservices. Ein Service kann Teil eines Kontexts sein, ein Kontext kann mehrere Services umfassen.

Das Muster, das die meiste Reibung spart: der Anti-Corruption Layer

Von allen Context-Mapping-Mustern ist der Anti-Corruption Layer das, dessen Fehlen ich am häufigsten teuer bezahlt sehe. Immer wenn mein sauberer Kontext an ein Legacy- oder Fremdsystem grenzt, brauche ich eine Übersetzungsschicht. Ohne sie sickert das fremde Modell – oft ein schlechtes – in meinen Kern. Was als "wir passen uns kurz an" (Conformist) beginnt, wird stillschweigend zur Dauerlösung.

Konkret: Mein Sales-Kontext kennt ein sauberes Product. Das Inventory-System spricht in LegacyInventoryResponse mit sku und price_cents. Ich lasse diese Fremd-DTOs nie in mein Domänenmodell. Ein Adapter übersetzt an genau einer Stelle:

// Downstream context: Sales
interface CatalogPort {
    Product findById(ProductId id);
}

// ACL: isolates the upstream "Inventory" model.
// Only this class knows the foreign DTOs; the domain never does.
class InventoryAntiCorruptionLayer implements CatalogPort {

    private final LegacyInventoryClient legacyClient;

    @Override
    public Product findById(ProductId id) {
        LegacyInventoryResponse raw = legacyClient.get(id.value());
        return new Product(
            new ProductId(raw.sku),
            Money.of(raw.price_cents) // translate cents into the domain type
        );
    }
}

Der Gewinn ist nicht kosmetisch. Wenn das Inventory-Team sein Format ändert, ändert sich genau eine Klasse. Mein Kern bleibt stabil, meine Tests bleiben grün, und die Übersetzung ist dokumentiert statt über den Code verstreut. Das ist der Unterschied zwischen einer Grenze, die hält, und einer, die leckt.

Die Kehrseite gilt für den Shared Kernel: Ich setze ihn nur ein, wenn zwei Teams in echter Partnership arbeiten und die Kopplung bewusst wollen. Geteiltes Modell heißt geteilte Änderung – jede Anpassung zwingt beide Teams zur Koordination. Als Default missbraucht wird der Shared Kernel zur Bremse. Im Zweifel gebe ich jedem Kontext sein eigenes Modell und verbinde sie über eine Published Language.

Zwischen Conformist und ACL liegt eine bewusste Kostenabwägung, die ich offen mit dem Kunden führe. Conformist heißt: Wir übernehmen das fremde Modell eins zu eins und sparen uns die Übersetzung. Das ist legitim, wenn der Upstream stabil ist und sein Modell zu meinem passt – etwa bei einer Generic Subdomain, die ich ohnehin nur einbinde. Sobald aber der eigene Kern betroffen ist oder der Upstream ein schwaches Modell hat, ist die eingesparte Übersetzungsschicht keine Ersparnis, sondern eine gestundete Schuld. Der ACL kostet einmal Aufbau und zahlt bei jeder Fremdänderung zurück. Diese Rechnung mache ich explizit, statt die Entscheidung dem Zufall des ersten Integrationssprints zu überlassen. In der Praxis notiere ich sie als kurze Entscheidung: Welches Fremdmodell koppeln wir, wie stabil ist es, was kostet uns ein Wechsel? Wer diese Frage einmal bewusst beantwortet, trifft sie nicht mehr versehentlich unter Zeitdruck.

Grenzen deklarativ festhalten

Damit die Context Map nicht als Foto vom Whiteboard verrottet, halte ich sie versionierbar fest. Mit dem Context Mapper und seiner DSL lässt sich eine Karte als Text ausdrücken, der neben dem Code lebt und mitwandert:

ContextMap ShopMap {
    contains Sales, Inventory, Billing

    Inventory -> Sales : Customer-Supplier {
        implementationTechnology = "REST"
        downstreamRights = INFLUENCER
        exposedAggregates = Product
    }

    Sales -> Billing : Conformist {
        implementationTechnology = "Messaging"
    }
}

Das ist kein Selbstzweck. Der Wert liegt darin, dass die Beziehung – Customer-Supplier, wer upstream ist, welches Aggregat exponiert wird – überprüfbar dokumentiert ist. Ein neuer Entwickler liest an einer Stelle ab, wie die Kontexte zusammenhängen, statt es aus Aufrufketten zu rekonstruieren. Diskussionen über Grenzen führe ich am Diff dieser Datei, nicht am nächsten abgewischten Whiteboard.

Wie ich in der Praxis vorgehe

Mein Ablauf ist bewusst unspektakulär und braucht keine Werkzeugkette. Ich hole die Domänenexperten und ein bis zwei Entwickler in einen Raum und lasse sie den fachlichen Ablauf erzählen – bei größeren Domänen nutze ich EventStorming als Discovery-Technik, um die Ereignisse an die Wand zu bringen. Dabei höre ich auf Sprachbrüche: Sobald dasselbe Wort zweimal Verschiedenes meint, habe ich vermutlich eine Kontextgrenze gefunden. Dann benenne ich die Kontexte, ordne sie in Core, Supporting und Generic ein und zeichne die Kanten mit ihren Mustern. Jede vermutete Grenze spiegele ich sofort an den Domänenexperten zurück: Ich erzähle den Ablauf in ihrer Sprache nach und beobachte, wo sie mich korrigieren. Diese Korrekturen sind Gold, denn sie zeigen mir die Stellen, an denen mein Modell und die gelebte Fachlichkeit noch auseinanderlaufen. Erst danach – und oft erst Wochen später – reden wir über Services, Repositories und Deployment.

Dieses Vorgehen fühlt sich für technikgetriebene Teams zunächst langsam an. Es ist das Gegenteil. Die Wochen, die ich vorne in Sprache investiere, spare ich hinten dreifach, weil ich Grenzen nicht mehr durch produktives System hindurch korrigieren muss. Einfach und elegant heißt hier: wenige, gut begründete Grenzen statt vieler zufälliger.

Fazit

Der rote Faden ist eine Reihenfolge. Zuerst die Sprache: Wo gilt welches Modell, wo bricht ein Begriff? Das ist der Bounded Context. Dann die Beziehungen: Wer ist upstream, wer übersetzt, wo schütze ich meinen Kern mit einem Anti-Corruption Layer, wo koppele ich bewusst über einen Shared Kernel? Das ist die Context Map. Und erst ganz zuletzt die Deployment-Einheit: Welcher Kontext wird ein Microservice?

Wer diese Reihenfolge einhält, bekommt Services, die einer fachlichen Grenze folgen – mit kürzeren Änderungszyklen, weniger Team-Reibung und einem Modell, das die Domäne trägt. Wer sie umdreht und Services zählt, bevor er die Sprache versteht, baut einen verteilten Big Ball of Mud mit mehr Netzwerk dazwischen. Die Grenze kommt vor dem Microservice. Immer.

Weiterführende Quellen

Kommentare