Suche
Die Renaissance von Domain-driven Design

Microservices lieben Domain-driven Design

Michael Plöd
Untitled2

© IStockphoto.com / David5962

Das Buch „Domain-Driven Design“, das Eric Evans vor gut dreizehn Jahren publizierte, gilt schon seit jeher als herausragende Referenz für die fachlich getriebene Modellierung von IT-Systemen. Mit dem Einzug von Microservices erfährt Domain-driven Design eine Renaissance, denn die beiden Ideen lassen sich wunderbar miteinander kombinieren.

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 Microservices-Landschaften und dem Beschreiben 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 große Strukturen helfen uns bei der Evolution von schnell wachsenden Microservices-Landschaften. Abschließend liefert der Bereich der Destillation Anregungen und Ideen für das Refactoring bestehender Anwendungen in Richtung von Microservices.

Strategisches Design mit Bounded Context

Zentrale Idee des strategischen Designs (Strategic Design) ist der Bounded Context. Dieser ist eine gute Hilfestellung für das Finden 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:

  • die Registrierung von Anmeldungen
  • den Druck der Konferenzpässe für die Besucher
  • eine Kapazitätsplanung für Essen und Sessions

Diese Domänen lassen sich als einzelne Bounded Contexts betrachten. 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 Konferenzpassdrucks kann der Besucher durch die Attribute vollständiger Name, Twitter Handle und Jobtitel 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 Potenzial für geteilte Daten gibt. Idealerweise würden die einzelnen Bounded Contexts über ein Eventsystem miteinander agieren.

Im Umfeld des Bounded Contexts hilft uns die so genannte Context Map dabei, die Interaktionen und Abhängigkeiten zwischen einzelnen Systemen, z. B. 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 Modelle von einem Kontext in den nächsten Kontext propagiert werden. Anders formuliert: Anhand der Context Map kann man einfach feststellen, wie die echten Abhängigkeiten zwischen einzelnen Systemen/Microservices sind. Domain-driven Design sieht hierfür sieben Interaktionsarten vor:

  • Shared Kernel
  • Customer/Supplier
  • Conformist
  • Anticorruption Layer
  • Separate Ways
  • Open/Host Service
  • Published Language
Abb. 1: Die Context Map hilft dabei, die Interaktionen und Abhängigkeiten zwischen einzelnen Systemen zu beschreiben

Abb. 1: Die Context Map hilft dabei, die Interaktionen und Abhängigkeiten zwischen einzelnen Systemen zu beschreiben

Die Abhängigkeitsform des Shared Kernels besagt, dass sich Teams, die unterschiedliche Anwendungen entwickeln, ein Subset des Domänenmodells teilen, wobei es hierbei irrelevant ist, ob das Modell auf Code- oder auf Datenebene geteilt wird. So können bereits geteilte 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 Vetorecht 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 Modell dem Modell 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 Modell in ein internes Modell zu übersetzten. Änderungen am externen Modell 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 Datentransfermodell 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 Eins-zu-eins-Kommunikation zwischen den Teams.

Interne Bausteine für die interne Ausgestaltung

Das Kapitel zu internen Bausteinen (Internal Building Blocks) 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 Gegensatz zu Entitäten haben Value Objects auch keinen eigenständigen 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 korrekt, das an der Entität Bestellung als Versandadresse hängt. Auf der anderen Seite kann die Anschrift auch als Entität betrachtet werden; und zwar in einem Logistikkontext, 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, das Entitäten in fachlichen Teilbereichen gruppiert. Es wäre sehr unpraktikabel, wenn jede Entität wirklich einen eigenen Lebenszyklus hätte. Noch unstrukturierter wäre es, wenn jede Entität beliebige andere Entitäten referenzieren 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. Zum anderen ist die Root-Entität die einzige Entität des Aggregats, die von außerhalb eines Aggregats referenziert werden darf.

Aggregate fassen fachlich zusammengehörige Entitäten zusammen und definieren in einem solchen fachlichen Block eine so genannte Root-Entität

Aggregate fassen fachlich zusammengehörige Entitäten zusammen und definieren in einem solchen fachlichen Block eine so genannte Root-Entität

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 zu kapseln, die mehrere Services und Aggregate betrifft. Factories sind für das Erzeugen komplexer Objektgraphen verantwortlich und Repositories kapseln den Datenzugriff. Ein weiteres, vor allem im Bereich Microservices interessantes Konstrukt sind Events, die in Domain-driven Design als Auslöser für das Ausführen von Logik betrachtet werden. Mithilfe von Events lassen sich reaktive, lose gekoppelte Microservices implementieren, die ohne Orchestrierungslogik auskommen.

Strukturieren und destillieren

Natürlich werden Microservices und entsprechende Landschaften wachsen, weshalb ein Team oder eine Organisation frühzeitig darauf achten sollte, dass dieses Wachstum planvoll verläuft. Das bedeutet allerdings nicht, dass rigide Entwicklungs- und Architektur-Guidelines eine natürliche Entwicklung im Keim ersticken sollen. 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 Microservices-Community als etablierte Best Practices genannt.

Domain-driven Design sieht zudem so genannte Responsibility Layers vor, die dazu dienen, einzelne Bounded Contexts in unterschiedliche, fachlich getriebene Schichten zu strukturieren. So wird man in einer Microservices-Landschaft durchaus Services vorfinden, die beispielsweise 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 Microservices 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 Subdomäne bzw. der neu entstandene Microservice vom Rest der Anwendung technisch getrennt ist. An dieser Stelle geht es konkret um Kommunikationsmuster, Schnittstellen und Implementierungsdetails.

Auch interessant: Domain-driven Design im Experten-Check: Warum ist DDD heute relevanter denn je?

Fazit

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

Geschrieben von
Michael Plöd
Michael Plöd
Michael Plöd ist Principal Consultant bei innoQ. Er hat über zehn Jahre praktische Consultingerfahrung in den Bereichen Softwareentwicklung und -architektur. Sein besonderes Interesse gilt den Bereichen Softwarearchitekturen, Transformation/Modernisierung von Anwendungen und Architekturen sowie polyglotte Persistenz.
Kommentare

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht.