Domain-driven Design (DDD) ist eine Entwicklungsmethode, deren Ziel eine gemeinsame fachliche Sprache innerhalb eines interdisziplinären Teams ist. Gelingt das, können sich alle Beteiligten – vom Fachexperten über den Designer bis hin zum Entwickler – über die gleiche Thematik unterhalten, ohne dass es ständig zu Missverständnissen und impliziten Annahmen kommt.
Die Idee dahinter ist, dass ein Team durch das Überbrücken der ansonsten gegebenen (fach-)sprachlichen Hürden schneller und besser in der Lage ist, die gewünschte Software zu entwickeln. Diese Annahme ist durchaus berechtigt, da es keine Seltenheit ist, dass man zwar miteinander redet, aber dennoch aneinander vorbei. Verwenden jedoch alle die gleichen Begriffe und haben zudem alle auch das gleiche Verständnis dieser Begriffe, verbringt man weniger Zeit mit der Frage nach dem eigentlichen Sinn.
Das ist allerdings zugleich auch der Grund, warum DDD so vielen Entwicklern so schwerfällt: Es hat wenig bis gar nichts mit Technologie zu tun, sondern erfordert, dass man seine persönliche Komfortzone verlässt und sich mit einer bislang fremden Fachthematik auseinandersetzt. Das ist für einen Technologen viel schwieriger, als sich mit den neuen (technischen) Features des jeweils favorisierten Frameworks zu befassen.
LUST AUF NOCH MEHR DOMAIN-DRIVEN DESIGN-TRENDS?
Entdecke Workshops vom 25. - 27. November 2024
Software ist kein Selbstzweck
Software ist aber kein Selbstzweck. Software wird nicht geschrieben, weil es so viel Spaß macht, Software zu schreiben. Letztlich wird Software geschrieben, um fachliche Probleme zu lösen und das Leben von Menschen und/oder Tieren einfacher, sicherer oder komfortabler zu machen. Software ist, auch wenn Entwickler das nicht gerne hören, in den meisten Fällen nur Mittel zum Zweck – und genau das rückt DDD in den Vordergrund.
Fängt man an, sich auf DDD einzulassen, wird man zunächst von einer Reihe wenig verständlicher Begriffe erschlagen. Dinge wie Commands oder Domain Events mögen noch greifbar erscheinen, aber Aggregates oder Bounded Contexts sind da schon deutlich abstrakter. Leider ist auch die gängige Literatur zu DDD dabei keine allzu große Hilfe, da sie sehr abstrakt und akademisch geschrieben ist. Es ist eine gewisse Ironie des Schicksals, dass ausgerechnet eine Methode, die ein besseres Verständnis anstrebt, zu oft daran scheitert, wie sie erklärt wird.
Ohne auf die Details von DDD eingehen zu wollen, kann man jedoch für alle genannten Begriffe einfache, greifbare Definitionen finden:
-
Ein Command ist der Wunsch eines Anwenders, ein bestimmtes Ziel zu erreichen. Commands drücken daher stets eine Intention aus und werden häufig im Imperativ formuliert. Daher stehen die Formulierungen von Schaltflächen und Menüpunkten in taskorientierten Benutzeroberflächen auch häufig in ebendieser Form: Open Game, Move Figure oder Give up sind typische Beispiele, in diesem Fall aus der Fachlichkeit des Schachs.
-
Ein Domain Event ist das Resultat auf ein Command: Es ist ein Fakt, der durch das System als Reaktion auf ein Command entstanden ist. Da Fakten tatsächlich geschehen sind und ohne Zeitmaschine nicht mehr rückgängig gemacht werden können, stehen sie in der Vergangenheitsform: Game opened wäre das Pendant zu einem der zuvor genannten Commands. Während Commands also quasi Requests beschreiben, stehen Domain Events für die Responses.
-
Ein Aggregate ist schließlich das Ding, das den Zustand verwaltet, auf dem Commands und Domain Events gemeinsam operieren: Im Falle einer Partie Schach ist ein laufendes Spiel zum Beispiel ein Aggregate, da die genannten Commands Informationen über das laufende Spiel und den aktuellen Spielstand benötigen, um Entscheidungen zu treffen. Der Bezug zu Objekten in der objektorientierten Programmierung liegt nahe, Aggregates müssen aber nicht zwingend als Objekte implementiert werden.
-
Ein Bounded Context schließlich ist eine Sprachgrenze: Er löst das Problem, dass Sprache zwar eindeutig sein soll, aber keine universelle Sprache gewünscht ist. Stattdessen definiert man Bereiche, innerhalb derer die Sprache eindeutig sein muss. Der gleiche Begriff darf aber durchaus in verschiedenen Bounded Contexts in unterschiedlicher Bedeutung auftreten, nur eben nicht innerhalb eines solchen.
Von DDD zu Microservices
Microservices haben hingegen so gar nichts mit DDD zu tun – sie stehen nicht einmal im Zusammenhang mit Fachlichkeit. Stattdessen sind sie höchst technische Artefakte: Ein Microservice ist ein Prozess (im Betriebssystemsinn), der für eine in sich geschlossene Thematik zuständig ist, diese kapselt, und den Zugriff darauf nur über wohldefinierte Schnittstellen von außen zulässt. Microservices sind technisch und inhaltlich autonom und autark.
Schaut man sich diese Beschreibung an, erinnert das stark an die Beschreibung von Web-Services, wie sie in den 1990er Jahren im Rahmen von SOAP und SOA (Service-oriented Architecture) geläufig war. Tatsächlich stellen Microservices letztlich genau die gleichen Ideen dar wie vor 25 Jahren – nur dieses Mal unter einem anderen Namen: auf Basis von REST, gRPC oder GraphQL als Protokoll und JSON als Datenformat. Der Kerngedanke ist aber nach wie vor der gleiche.
Das wirft die Frage auf, wie Microservices zu schneiden sind: Es liegt auf der Hand, dass nicht die gesamte Logik der Anwendung in einem einzigen Microservice enthalten sein sollte, denn ansonsten hat man wieder einen Monolithen. Wie verteilt man also die Logik der Gesamtanwendung so auf mehrere Dienste, dass sie jeweils sinnvolle Aufgaben erfüllen und losgelöst voneinander agieren können – eben autonom und autark?
Technische Schnitte, fachliche Schnitte
Eine gute Faustregel ist dabei, sich zu überlegen, welchen Teil einer Anwendung man als eigenständiges Geschäftsmodell extrahieren könnte. Verwendet die Anwendung beispielsweise Authentifizierung? Dann spräche (theoretisch) nichts dagegen, dies in einen eigenen Service auszulagern – Anbieter, die Authentication as a Service (oder auch Identity as a Service) offerieren, machen es vor.
Verwendet die Anwendung einen umfangreichen Logger? Wie wäre es mit Logging as a Service? Wie man an diesen beiden Beispielen sieht, fallen einem als Erstes technische Komponenten ein, die orthogonal zur eigentlichen Fachlichkeit der Anwendung stehen, die man eigenständig entwickeln, betreiben und potenziell auch vermarkten könnte.
Das Gleiche gilt jedoch auch für fachliche Aspekte. Auch hier lassen sich Bereiche finden, die losgelöst voneinander entwickelt, betrieben und potenziell vermarktet werden können. Als Beispiel hierfür sei eine Software zur Verwaltung von Urlaubsanträgen genannt. Der Kernbegriff an dieser Stelle ist sicherlich das Wort Urlaub, weshalb man sich die Frage stellen sollte, welche Bedeutung dieses Wort eigentlich hat.
Viele denken bei dem Wort Urlaub an Themen wie Reisen, Entspannung, Strand, Meer, Berge, Wandern, Tanzen, Ausgehen, gutes Essen, … Kaum jemand denkt dabei an Themen wie Urlaubsvertretung oder Sonderurlaub. Das liegt daran, dass man zuerst an den eigenen Urlaub denkt, und nicht daran, welche Auswirkungen der eigene Urlaub auf die Teamkollegen hat oder wie die HR-Abteilung Urlaub verwaltet.
Was ist Urlaub?
Modelliert man eine solche Anwendung nun aber mit DDD, stellt man rasch fest, dass in einem interdisziplinären Team mindestens drei verschiedene Sichtweisen auf das Wort Urlaub bestehen: Es gibt die Rolle desjenigen, der Urlaub nehmen möchte. Es gibt die Rolle derjenigen, die die Urlaubsvertretung gewährleisten müssen. Und es gibt die Rolle desjenigen, der den Urlaub genehmigt. Für diese drei Parteien ist das Wort Urlaub jeweils mit einer anderen Bedeutung behaftet. Es ist sogar so, dass jede dieser Parteien je nach persönlicher Situation eine andere der drei Rollen einnehmen kann (auch ein Vorgesetzter oder jemand aus der HR-Abteilung möchte schließlich irgendwann einmal Urlaub nehmen).
Gemäß DDD ist es also wenig sinnvoll, zu versuchen, die Unschärfe des Begriffs dadurch zu lösen, dass man sich auf eine universelle Sprache verständigt und krampfhaft versucht, für zwei der Begriffe einen mehr oder minder passenden Ersatz zu finden. Stattdessen teilt man die Anwendung schlichtweg in unterschiedliche Kontexte ein:
-
Es gibt zum einen die persönliche Sicht als Bounded Context, in dem der Antrag auf Urlaub gestellt wird. Hier ist mit dem Wort Urlaub tatsächlich das gemeint, woran die meisten Menschen spontan denken.
-
Es gibt zweitens die Teamsicht als Bounded Context, in dem das Wort Urlaub gleichbedeutend ist mit Fehlzeit, die anderweitig ausgeglichen werden muss. Ob das nun erfolgt, weil jemand im Urlaub ist, oder weil derjenige krank ist, spielt für das Team eine untergeordnete Rolle.
-
Drittens gibt es die Verwaltungssicht als Bounded Context, in dem das Wort Urlaub als rein kalkulatorische Einheit gesehen wird. In diesen Zusammenhang fallen dann Begriffe wie Urlaubsanspruch, Resturlaub, Sonderurlaub usw.
Bounded Contexts als Service-Grenzen
Nach dieser Überlegung liegt es nahe, zu sagen, dass man für die verschiedenen Bounded Contexts jeweils einen eigenen Microservice entwickeln könnte:
-
Der erste Microservice fokussiert auf das tatsächliche Stellen und Genehmigen des Urlaubsantrags.
-
Der zweite Microservice kümmert sich um die Planung innerhalb des Teams.
-
Der dritte Microservice übernimmt die organisatorische Verwaltung, welchem Mitarbeiter wie viel Urlaub zusteht.
Es ist leicht denkbar, aus jedem dieser Dienste eine eigene Anwendung zu entwickeln, die man auch gesondert von den übrigen betreiben und – das war die Ausgangsbasis des Ganzen – auch gesondert vermarkten könnte. Wie man sieht, haben hier rein fachliche Überlegungen zu dieser Grenzziehung geführt, keine technischen.
Tatsächlich stellt man nach einer Weile fest, dass Bounded Contexts, die sich durch die Modellierung einer Domäne mit DDD ergeben, auch eine gute Abgrenzung für Microservices sind. Der Grund dafür liegt auf der Hand: Da Bounded Contexts eine in sich geschlossene Sprache beschreiben, die sich mit einer Thematik befasst, lässt sich auch leicht ein entsprechender Microservice entwickeln, der genau diese Thematik und die dazugehörige Sprache aufgreift.
Von DDD zu Microservices, zum Zweiten
Praktischerweise lassen sich auch die anderen genannten Artefakte aus DDD hervorragend auf Microservices mappen: Ein Command ist nämlich letztlich nichts anderes als ein Request an einen Service, ein Event dessen Reaktion darauf – sprich, die Response. Das gilt für alle HTTP-basierten Wege, um Microservices mit einer Schnittstelle nach außen zu versehen. Die Aggregates, die sich logisch innerhalb der Bounded Contexts befinden, gruppieren schließlich innerhalb des Service die Logik und verhindern, dass der Service selbst wieder zum Monolithen wird.
Hat man diesen Zusammenhang zwischen den Bounded Contexts von DDD und Microservices einmal hergestellt, ist es schwer, ihn wieder aus dem Kopf zu bekommen. Letztlich sind beide Konzepte nur zwei Seiten derselben Medaille – nur eben einmal fachlich und einmal technisch formuliert.
CQRS und Event Sourcing lassen grüßen
Im Zusammenhang mit DDD werden häufig auch das Architekturmuster CQRS (Command Query Responsibility Segregation) und der Speichermechanismus Event Sourcing genannt. Mit ihnen verhält es sich im Grunde wie mit DDD: Die gängigen Erklärungen sind durchaus korrekt, aber häufig zu theoretisch und abstrakt. Doch auch sie lassen sich einfach und anschaulich erklären:
-
Die Idee von CQRS ist, das Lesen aus einer Anwendung vom Schreiben in ebendiese zu trennen. Das Ziel davon ist, beide Seiten unabhängig voneinander warten und skalieren zu können. CQRS funktioniert dann besonders gut, wenn Schreibzugriffe als fachliche Aufträge und weniger als Befehle zur Datenmanipulation aufgefasst werden – das ist der Grund, warum CQRS und DDD so gut zusammenpassen: Die Commands aus DDD finden sich in CQRS wieder.
-
Der Ansatz von Event Sourcing ist nicht – wie in relationalen Datenbank üblich –, den Status quo der Daten zu speichern und ihn gegebenenfalls zu überschreiben oder gar zu löschen. Vielmehr gilt es ähnlich wie Git, die Blockchain oder eine Bank bei der Kontoführung, die Deltas stets nur zu speichern, die im Lauf der Zeit zum aktuellen Stand der Dinge geführt haben. Man erhält also eine zeitliche Abfolge von Ereignissen, die sich auch im Nachhinein noch einmal abspielen und neu interpretieren lässt. Dass diese Ereignisse hervorragend mit den Domain Events aus DDD korrelieren, liegt auf der Hand.
Fasst man all das nun zusammen, ergibt sich daraus ein Ansatz, wie sich Schnittstellen von Microservices standardisiert und unabhängig von der zur Implementierung verwendeten Technologie gestalten lassen. Im Grunde gilt es dabei lediglich, nach der gewünschten Technologie für die Kommunikation zu unterscheiden, also REST, gRPC, GraphQL oder etwas anderes. Im Folgenden soll der Fokus auf REST und GraphQL liegen.
Microservices mit REST …
Um es kurz zu machen, passt REST im eigentlichen Sinne nicht gut zu CQRS. Der Grund ist simpel: REST stellt Datenressourcen in den Mittelpunkt, nicht fachliche Prozesse. REST macht genau das, was man, wie zuvor angesprochen, nicht tun sollte, nämlich Commands als Auftrag zur Datenmanipulation ansehen. Das zeigt sich bereits daran, dass jegliche Interaktion mit dem Service auf die immer gleichen vier Verben GET, POST, PUT und DELETE heruntergebrochen wird.
Das passt historisch gesehen wunderbar zu der Denkweise relationaler Datenbanken, da sich die vier Verben auf die passenden SQL-Pendants SELECT, INSERT, UPDATE und DELETE abbilden lassen. Für CQRS ist es jedoch wenig geeignet, und für DDD als Methodik, die Wert auf sprachlichen Ausdruck legt, erst recht nicht. Ein Urlaubsantrag wird nicht created, inserted oder gepostet, er wird abgegeben. Ein Schachspiel wird nicht created, inserted oder gepostet, es wird eröffnet.
Nutzt man REST so, wie es konzeptionell gedacht ist, gibt man all diese Mannigfaltigkeit nicht nur der natürlichen, sondern vor allem auch der Fachsprache auf, und ersetzt sie durch vier Verben, auf die dann als logischer Schluss alle Aktionen heruntergebrochen werden müssen. Wie ausdrucksstark das funktioniert, mag sich jeder selbst ausmalen.
Unabhängig davon ist REST in der Praxis aber ohnehin häufig nicht das, was mit REST in der Theorie gemeint ist: Viele Entwickler verwenden den Begriff REST synonym zu HTTP mit JSON – quasi als Gegenentwurf zu SOAP, was in gewissem Sinne für HTTP mit XML steht.
Wendet man also das Konzept eines einfachen HTTP API an, passt das zu CQRS schon ganz gut: Commands bewirken eine Veränderung und werden deshalb per POST an den Service übermittelt. Domain Events hingegen informieren über Änderungen und müssen daher gelesen werden, werden also über GET abgerufen. Mehr gibt es nicht. Um einen Urlaubsantrag abzugeben, genügt ein Aufruf der Route POST /submit-vacation-request-form, um ein Schachspiel zu eröffnen, POST / open-game.
… und mit GraphQL
Noch passender wird es allerdings, wenn man den Service mit einem GraphQL-Endpunkt ausstattet. GraphQL passt konzeptionell hervorragend zu CQRS, und kennt neben den flexiblen Queries (für die GraphQL im Wesentlichen bekannt ist), auch sogenannte Mutations und Subscriptions.
Eine Mutation ist dabei ein Auftrag an den Server, etwas zu verändern. GraphQL stellt dabei nicht zwingend ein Datenschema in den Mittelpunkt, sondern kann auch Funktionsaufrufe mitsamt Parametern abbilden. Eine Mutation entspricht in GraphQL also eher einem Remote Procedure Call (RPC) – und nichts anderes ist ein Command letztlich. Eine Funktion bildet die Intention eines Commands nämlich viel besser ab, als ein reines Datenobjekt, denn schließlich geht es bei einem Command darum, einen fachlichen Vorgang anzustoßen.
Eine Subscription hingegen ist eine Art Abonnement, mit dem sich der Client vom Server über Dinge, die passiert sind, informieren lassen kann. GraphQL kümmert sich dabei um den technischen Aspekt der langlaufenden Verbindung, aber inhaltlich klingt das genau nach dem, was man für das Zustellen von Domain Events benötigt.
Verlässt man also den allseits bekannten Pfad von GraphQL und schaut, welche Werkzeuge abseits davon mit Mutations und Subscriptions zur Verfügung stehen, stellt man rasch fest, dass GraphQL und CQRS perfekt zusammenpassen. Das Schöne ist, dass eine Entscheidung für GraphQL keine Entscheidung gegen das zuvor beschriebene HTTP API sein muss – beides lässt sich problemlos parallel betreiben.
Event Sourcing in Microservices
All das ist unabhängig von der Frage, ob man Event Sourcing in einem Microservice einsetzt oder nicht. Tatsächlich lässt sich das sogar, je nach Bedarf, von Service zu Service individuell entscheiden.
Tatsache ist aber, dass der Einsatz von Event Sourcing ebenso wie die vorherigen Überlegungen zu HTTP- und GraphQL-basierten APIs nur möglich ist, weil man statt der Technologie die Fachlichkeit und deren Begriffe in den Vordergrund rückt. Und genau das ist das Verdienst von Domain-driven Design.
Es ist also sehr wohl möglich, DDD ohne CQRS und Event Sourcing einzusetzen, und selbstverständlich kann man auch CQRS oder Event Sourcing ohne die jeweils beiden anderen Konzepte nutzen. Doch fügt man diese Bausteine zusammen, ergibt sich als Gesamtes mehr, als es die Summe der Einzelteile vermuten lassen würde.
Fazit
Man kann also beginnen, Microservices weniger als technisches Artefakt zu sehen. Selbstverständlich sind Microservices auch zu einem gewissen Grad technisch motiviert, doch der Kern ist das Kapseln von Fachlichkeit, und das ist der Bereich, in dem sich Microservices und Domain-driven Design überschneiden. Leider ist das, wie erwähnt, auch der Bereich, wo man die eigene Komfortzone verlassen und sich auf Neues einlassen muss. Wenn es aber gelingt, über den eigenen Schatten zu springen und dieses Neue nicht als lästiges Problem, sondern als Chance anzusehen, neue Welten kennenzulernen, öffnen sich ganz neue Türen.
Ob man sich diesem Schritt nun eher von der einen oder von der anderen Seite her nähert, spielt dabei letztlich keine so große Rolle: Am Ende gehören DDD und Microservices untrennbar zusammen, wenn man es denn zulässt. Und wem das gelingt, der hält die Bausteine für ein nachhaltiges Geflecht aus einander ergänzenden fachlichen Diensten in der Hand. Wir müssen uns nur trauen.