Bounded Contexts vor dem Serviceschnitt
Bevor ich Services schneide, kläre ich fachliche Grenzen. Ein Bounded Context ist eine Sprachgrenze und ein möglicher, aber nicht zwingender Serviceschnitt.
Wer die Zahl der Services festlegt, bevor die fachlichen Grenzen stehen, löst keine Kopplung auf – er verteilt sie übers Netzwerk. Der Schnitt trennt dann Code, aber keine Zuständigkeiten: Eine Änderung an einem einzigen Begriff läuft weiterhin durch drei Systeme, jetzt nur mit HTTP dazwischen und drei Teams, die sich abstimmen müssen.
Die Grenze, die vorher fehlt, ist keine technische. Sie verläuft dort, wo ein Wort aufhört, dasselbe zu bedeuten.
Die Architektur entsteht an den sprachlichen Grenzen und an den Übersetzungen zwischen ihnen.
- Subdomain · beschreibt einen fachlichen Problemraum
- Bounded Context · gibt Begriffen und Regeln einen eindeutigen Geltungsbereich
- Modell · implementiert die Sprache innerhalb dieser Grenze
- Context Map · benennt Abhängigkeiten und Einfluss zwischen Kontexten
- Anti-Corruption Layer · übersetzt ein fremdes Modell in die eigene Sprache
- Service · kann eine Grenze ausführen, definiert sie aber nicht automatisch
Damit werden Kästen nicht zum Selbstzweck. Entscheidend bleibt, wo Bedeutung wechselt und wer diese Übersetzung verantwortet.
Warum die Reihenfolge über das Ergebnis entscheidet
Die Frage, mit der Monolithen-Migrationen üblicherweise beginnen, lautet: "In wie viele Microservices zerlegen wir den Monolithen?" Sie ist schon falsch gestellt. Die Zahl ist ein Ergebnis und keine Vorgabe. Zu klären ist, wo die fachlichen Grenzen liegen – und ob sich für jede Grenze eine eigene, in sich konsistente Sprache benennen lässt. Wie viele Services daraus werden, zeigt sich danach.
Der Grund dafür ist ökonomisch. 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 keine Infrastruktur. Deshalb investiere ich die erste Woche eines solchen Projekts in Sprache und noch nicht in Infrastruktur- und Deployment-Diagramme. 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 – darin liegt der eigentliche Gewinn.
Ein Beispiel aus dem Handel: 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. Hier greift Conway's Law: 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 ein Unternehmen besser kann als der Wettbewerb – dorthin gehören 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 Punkt: Der spätere Serviceschnitt legt sich auf diese Context Map. Erst existiert sie, 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. Sobald ein sauber modellierter Kontext an ein Legacy- oder Fremdsystem grenzt, braucht er 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 Kostenabwägung, und die gehört ausgesprochen. 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 zum eigenen passt – etwa bei einer Generic Subdomain, die ohnehin nur eingebunden wird. Sobald aber der eigene Kern betroffen ist oder der Upstream ein schwaches Modell hat, wird die eingesparte Übersetzungsschicht zur gestundeten 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 formuliere ich die Beziehungen als Text, der im Repository neben dem Code liegt und bei jeder Änderung mitversioniert wird:
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 wert, 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, kann Services an fachlichen Grenzen ausrichten, statt sie nach technischen Schichten zu schneiden. Wer sie umdreht und Services zählt, bevor er die Sprache versteht, riskiert einen verteilten Big Ball of Mud mit mehr Netzwerk dazwischen. Erst kommt die Modellgrenze, dann die Entscheidung über den Betrieb. Nicht jeder Bounded Context muss ein eigener Service werden.
Weiterführende Quellen
- Martin Fowler – BoundedContext
- Martin Fowler – UbiquitousLanguage
- Martin Fowler – Conway's Law
- Vladik Khononov – Bounded Contexts are NOT Microservices
- Context Mapper – Context Map
Wie fandest du diesen Beitrag?
Kommentare