Betrachtet man diverse Publikationen rund um Domain Driven Design und Microservices, so stellt man fest, dass das Konzept des Bounded Contexts von zentraler Natur ist. Es gibt kaum eine Veröffentlichung zur Modellierung von Microservices, die dieses Konzept nicht erwähnt. Allerdings greift diese Konstellation zu kurz: es gibt weitaus mehr über Domain Driven Design und Microservices zu berichten als den Bounded Context. Betrachtet man auf der anderen Seite das Thema Domain Driven Design so ist festzustellen, dass das Thema weit über die hinlänglich bekannten Entitäten, Value Objects und Aggregate hinausgeht.

Im Kontext von Microservices hilft uns Domain Driven Design in vier Bereichen: Das strategische Design hilft bei der Strukturierung von Microservice-Landschaften und der Beschreibung von Liefer- und Leistungsbeziehungen zwischen einzelnen Microservices. Der Bereich der internen Bausteine hält Lösungsvorschläge für den internen Entwurf einzelner Microservices bereit. Die Ideen und Patterns des Domain Driven Design Kapitels zum Thema grosse Strukturen helfen uns bei der Evolution von schnell wachsenden Microservice Landschaften. Abschließend liefert der Bereich der Destillation Anregungen und Ideen für das Refactoring bestehender Anwendungen in Richtung von Microservices. Im Folgenden werden die eben genannten Bereiche vorgestellt.

Strategische Design (Strategic Design)

Zentrale Idee des strategischen Designs ist der Bounded Context. Dieser ist wie eingangs erwähnt eine gute Hilfestellung für die Findung einer passenden Granularität für Microservices. Jede anspruchsvollere Fachdomäne wird mit Sicherheit aus mehreren Bounded Contexts bestehen. Ein Bounded Context ist rein formell als Gültigkeitsgrenze für ein fachliches Modell zu betrachten. Diese auf den ersten Blick abstrakt wirkende Definition ist jedoch recht leicht zu verstehen, wenn man sie anhand eines konkreten Beispiels betrachtet. Angenommen wir wollen eine neue Anwendungslandschaft für die Organisation einer Konferenz wie der W-JAX entwickeln. Eine solche Fachlichkeit hat vereinfacht betrachtet beispielsweise drei Domänen:

Diese Domänen können als einzelne Bounded Contexts betrachtet werden. Das Modell des Besuchers kann innerhalb dieser Domänen jedoch unterschiedlich behandelt und dargestellt werden. So kann ein Besucher im Bounded Context der Registrierung mit den Attributen Name, Vorname, Firma, Anschrift, Zahlungsweise und angemeldete Tracks repräsentiert werden. Im Bounded Context des Konferenz-Pass Drucks kann der Besucher durch die Attribute vollständiger Name, Twitter Handle und Job-Titel dargestellt werden. Im Gegensatz dazu sind hinsichtlich des Besuchers im Kontext der Kapazitätsplanung primär die Essenspräferenzen (Vegetarier oder nicht?) und Session-Interessen von Relevanz. Dieses Modell ist auch dafür geeignet unabhängige Datendarstellungen abzubilden, wobei es besonders im Hinblick auf den Namen des Konferenzbesuchers Potential für geteilte Daten gibt. Idealerweise würden die einzelnen Bounded Contexts über ein Event System miteinander agieren.

Im Umfeld des Bounded Context hilft uns die so genannte Context Map dabei, die Interaktionen und Abhängigkeiten zwischen einzelnen Systemen, zum Beispiel Microservices zu beschreiben. Im Gegensatz zu den hinlänglich bekannten Beschreibungsstilen für Liefer- und Leistungsbeziehungen, geht die Idee der Context Map jedoch weiter: sie betrachtet, wie Models von einem Kontext in den nächsten Kontext propagiert werden. Anders formuliert: anhand der Context Map kann man sehr einfach feststellen, wie die echten Abhängigkeiten zwischen einzelnen Systemen / Microservices sind. Domain Driven Design sieht hierfür sieben Interaktionsarten vor:

Abb. 1: Context Map
Abb. 1: Context Map

Die Abhängigkeitsform des Shared Kernel besagt, dass sich Teams, die unterschiedliche Anwendungen entwickeln, ein Subset des Domänen Modells teilen. Wobei es hierbei irrelevant ist, ob das Modell auf Code- oder auf Datenebene geteilt wird. So können bereits geteilten Tabellen in einer Datenbank dazu führen, dass die Teams mit Shared Kernel arbeiten.

Bei Customer / Supplier besteht eine Liefer- und Leistungsabhängigkeit hinsichtlich der Modelle zweier Teams, allerdings geht diese Abhängigkeit so weit, dass das konsumierende Team, der Customer, ein Veto Recht im Hinblick auf das anbietende Team (Supplier) hat. Dies kann dazu führen, dass das Supplier-Team massiv vom Customer-Team an Weiterentwicklungen gehindert werden kann.

Conformist ist ähnlich zu Customer / Supplier gelagert, allerdings hat hier das konsumierende Team eine untergeordnete Rolle: es passt sein internes Model, dem Model des Supplier-Teams an.

Die bisherigen Interaktionsmuster adressieren eher eng gekoppelte Systeme. Ein erster Schritt in Richtung loser Kopplung ist der Anticorruption Layer. Hierbei besitzt ein System eine dedizierte Komponente oder Schicht um ein externes Model in ein internes Model zu übersetzten. Änderungen am externen Model ziehen sich somit nicht durch die gesamte Anwendung sondern können an einer dedizierten Stelle abgefangen werden.

Die losest mögliche Kopplung zwischen zwei Systemen ist Separate Ways. Hierbei gibt es keinerlei Beziehungen zwischen den Systemen. Jedes Team kann somit seine eigene Lösung für die jeweilige Domäne finden. Eine klassische „SOA-Interaktionsform“ ist Open / Host Service, bei dem ein System spezielle Schnittstellen publiziert, die für externe Kommunikation ausgerichtet sind. Meist wird hierbei auch mit einem Daten-Transfer-Model gearbeitet.

Schlussendlich sieht Domain Driven Design noch eine konzeptionelle, sprachliche Kopplungsform vor: die Published Language. Hierbei ist das Model in Form der Ubiquitous Language festgelegt und zwei Bounded Contexts halten sich an den in der Ubiquitous Language beschriebenen Kontrakt. Bei der Published Language sollte man jedoch im Projekt darauf achten, dass dieses nicht plötzlich in Richtung eines abstrakten Unternehmensdatenmodells abdriftet.

Betrachtet man nun die gerade erwähnten Interaktionsformen, so kann man feststellen, dass es hierbei einen direkten Bezug zu Conways Law gibt: Shared Kernel, Customer / Supplier und Conformist sind primär Patterns, die eine sehr starke Kopplung und Kommunikation zwischen Teams erfordern. Auf der anderen Seite fördern Anticorruption Layer, Separate Ways, Open / Host Service und Published Language eine losere Kopplung und somit auch weniger 1:1 Kommunikation zwischen den Teams.

Interne Bausteine (Internal Building Blocks)

Das Kapitel zu internen Bausteinen ist der wohl bekannteste Teil aus Domain Driven Design. In diesem Kapitel geht es um Entitäten, Value Objects, Aggregate oder Services, also um Patterns für die interne Ausgestaltung eines Bounded Contexts bzw. Microservices.

Entitäten stellen hierbei zentrale Geschäftsobjekte eines Domänenmodells dar. Sie haben eine konstante Identität und einen eigenen Lebenszyklus. Im Gegensatz dazu sind Value Objects primär im Hinblick auf die Ausprägung ihrer Attribute von Interesse. Im Gegenzug zu Entitäten haben Value Objects auch keinen eigenständige Lebenszyklus. Dieser hängt immer vom Lebenszyklus der Entität ab, die das Value Object referenziert. Allerdings ist die Entscheidungsfindung ob ein Objekt nun als Entität oder Value Object gehandelt werden soll nicht immer eindeutig. Betrachten wir beispielsweise eine Anschrift bestehend aus Name, Straße, Postleitzahl und Stadt. Im Kontext eines einfachen Onlineshops namens „Michaels Plattenladen“ wird die Anschrift wahrscheinlich nur für den Druck von Versandetiketten benötigt. Hier wäre eine Einordnung als Value Object, welches an der Entität Bestellung als Versandadresse hängt, korrekt. Auf der anderen Seite kann die Anschrift auch als Entität betrachtet werden und zwar in einem Logistik Kontext, bei dem auf Basis dieser Zentralen Entität Auslieferungsrouten berechnet werden. Kurzum: es hängt immer vom (Bounded) Kontext ab.

Das interessanteste Pattern im Bereich der internen Bausteine ist das Aggregat, welches Entitäten in fachlichen Teilbereichen gruppiert. Es wäre sehr unpraktikabel, wenn jede Entität wirklich einen eigenen Lebenszyklus hätte und noch unstrukturierter wäre es wenn jede Entität beliebige andere Entitäten referenzierten könnte. Aggregate fassen fachlich zusammengehörige Entitäten zusammen und definieren in einem solchen fachlichen Block eine so genannte „Root Entität“. Diese Root Entität hat zwei Kerneigenschaften. Zum einen steuert sie den gesamten Lebenszyklus des Aggregats, inklusive der darin enthaltenen Entitäten. Weiterhin ist die Root Entität die einzige Entität des Aggregats, die von ausserhalb eines Aggregats referenziert werden darf.

Abb. 2: Aggregat Beispiel
Abb. 2: Aggregat Beispiel

Aggregate sind wie Bounded Contexts hervorragend dafür geeignet, die Granularität von Microservices zu bestimmen. Ein Microservice sollte minimal so groß wie ein Aggregat, maximal aber so groß wie ein Bounded Context sein.

Domain Driven Design sieht im Bereich der internen Bausteine noch weitere Patterns vor. So werden Services verwendet um Geschäftslogik, die mehrere Services und Aggregate betrifft, zu kapseln. Factories sind für die Erzeugung komplexer Objektgraphen verantwortlich und Respositories kapseln den Datenzugriff. Ein weiteres, vor allem im Bereich Microservices interessantes, Konstrukt sind Events, welche in Domain Driven Design als Auslöser für die Ausführung von Logik betrachtet werden. Mit Hilfe von Events lassen sich reaktive, lose gekoppelte Microservices implementieren, die ohne Orchestriereungslogik auskommen.

Große Strukturen und Destillierung

Natürlich werden Microservices und entsprechende Landschaften wachsen, weshalb im Team oder in der Organisation frühzeitig darauf geachtet werden sollte, dass dieses Wachstum planvoll verläuft. Das bedeutet allerdings nicht, dass mit Hilfe rigider Entwicklungs- und Architekturguidelines eine natürliche Entwicklung bzw. Evolution im Keim erstickt werden soll. Im Gegensatz: Domain Driven Design sieht dank der Ideen der Evolving Order und den Responsibility Layers eine gesunde Evolution vor. Die Evolving Order steht für ein planvolles Wachstum entlang von Leitplanken, die Freiheitsgrade vorsehen. Weiterhin betont die Evolving Order, dass sich große Strukturen primär an Bounded Contexts orientieren sollten. All diese Punkte werden häufig in der Microservice Community als etablierte Best Practices genannt.

Domain Driven Design sieht zudem so genannte Responsibility Layers vor, welche dazu dienen, einzelne Bounded Contexts in unterschiedliche, fachlich getriebene Schichten zu strukturieren. So wird man in einer Microservice Landschaft durchaus Services vorfinden, die beispieslweise kundenzentrisch sind, daneben werden Microservices existieren, die eher eine unterstützende Fachlichkeit besitzen. Als Beispiel hierfür kann man Batch-zentrische Dienste nennen.

Abschließend liefert uns Domain Driven Design noch Anregungen für die Extraktion von Microserivces aus einem bestehenden Monolithen. Der Bereich der Destillation sieht hierfür vier Schritte vor:

  1. Identifikation einer Subdomäne
  2. Extraktion aus dem Kern
  3. Feinschliff der Extraktion
  4. Internes Refactoring

Diese vier Schritte werden durch zwei Dokumente begleitet: Das Vision Statement beschreibt was konkret in einem Microservice enthalten ist und was nicht. Bei dieser Beschreibung geht es nicht um technische, sondern um rein fachliche Aspekte. Die technischen Aspekte finden im Destillation Document eine Heimat. Hier wird beschrieben, wie die Sub-Domäne bzw. der neu entstandene Microservice vom Rest der Anwendung technisch getrennt sind. An dieser Stelle geht es konkret um Kommunikationsmuster, Schnittstellen und Implementierungsdetails.

Zusammenfassung

Wie Sie sehen, passen Domain Driven Design und Microservices hervorragend zusammen. Eric Evans lieferte in seinem Buch aus dem Jahre 2003, weit vor dem Microservice Hype, sehr hilfreiche Ratschläge und Patterns, die heute mehr Relevanz denn je besitzen.

Links & Literatur