Softwaresanierung mit Domain Driven Design
Große, über die Zeit gewachsene und zusammengeführte Systeme bieten nach einiger Zeit das Problem einer schwer beherrschbaren Komplexität. Das manifestiert sich nicht selten in übergroßen ER Diagrammen, korrupten Daten, Referenzzyklen oder starken Latenzen, ausgelöst durch weite Requestkaskaden in Microservices.
Bei dem Unterfangen ein unüberblickbares Gewirr von einzelnen Services und weiten Datenstrukturen zu trennen und beherrschbar zu machen, können die Elemente des Domain Driven Designs (DDD) helfen, Grenzen zu ziehen.
Die Domain beschreibt zunächst im Sinne von DDD, alle Elemente, die zum Erfüllen einer Unternehmung nötig sind. Ein bestehendes Softwaresystem ist dabei immer nur ein Teil des Ganzen, bildet aber nie das Ganze ab. Zur Domain gehören auch die Menschen, die es bedienen, das implizite Wissen und was sonst noch um die Software herum existiert.
Für eine erste Auftrennung der Software helfen die Begriffe der Subdomain und des Bounded Context. Eine Subdomain ist ein Teil der Domain, z.B. eine Abteilung, ein Fachteam, aber auch ein Prozess. So wie das bestehende System einen Teil der Domain abbildet, bildet nun ein Bounded Context den fachlich hinreichenden Teil der Subdomain ab, um eine Software darin zu entwickeln.
Abbildung 1: Gewachsenes System innerhalb einer Domain
Die Grenze zwischen zwei Bounded Contexts zu ziehen ist nicht trivial und bedarf der regelmäßigen Überprüfung und Anpassung. Zu Nennen sind hier z.B. bestehende, reale Grenzen wie Abteilungen, Teams und Prozesse. Eine Komponente, welche von zwei Teams parallel bearbeitet wird, aber auch Komponenten mit merklich zwei fachlichen Abhängigkeiten sind gute Kandidaten einer Trennung. Aber auch neu hinzugekommene Komponenten und Datenquellen können mit Bounded Contexts schnell in ein bestehendes System eingepasst werden. Die Grenzen bestehen hier ja bereits technisch und meist auch fachlich.
Die Größe eines Bounded Context sollte immer so gewählt werden, dass er von einem Team bearbeitet werden kann. Denn eine Grenze zu ziehen, bedeutet immer auch Datenstrukturen und technische Systeme zu trennen. Innerhalb eines Bounded Context gilt immer ein fachlicher Kern, eben den der Subdomain. Da nur ein Team für einen Context verantwortlich ist, bildet sich durch die Konzentration und der Auseinandersetzung mit diesem fachlichen Domainkern und seinen Experten die „Ubiquitous Language“ und darauf aufbauend das Modell des Kontext. Hat man sich so mit seinem Context auseinandergesetzt, so lassen sich die Grenzen zu den Nachbarkontexten besser erkennen und beschreiben.
Abbildung 2: Eine Contextmap mit Grenzen und Verbindungen zwischen Bounced Context
Diese Grenzen und Verbindungen kann man in einer Contextmap visualisieren und somit Aussagen über die Art der technischen, aber auch organisatorischen Kopplung machen. Dem so genannten Strategic Design. Vier der gängigsten Pattern sind:
Customer-Supplier:
Zwei Teams stehen in direkter Beziehung als Versorger und Konsument. Die Kommunikation stimmt und man kann sich auf eine gemeinsame Schnittstellensprache einigen und kommuniziert Änderungen zeitnah und oft.
Conformist Pattern:
Wie beim Customer-Supplier, nur liefert das versorgende System sein eigenes Modell aus und besitzt keine Motivation zur Änderung. Z.B. ein zugekauftes System, Partnerunternehmen, etc. Das konsumierende Team nimmt die Schnittstelle wie sie ist.
Shared Kernel:
Zwei Kontexte teilen sich eine gemeinsame Codebasis. Eine Bibliothek oder Teile eines alten, abzulösenden, Systems. Die Sprache dieser Codebasis muss nichts mit dem Modell des jeweiligen Kontexts zu tun haben.
Anti Corruption Layer:
Im Falle eines Shared Kernels oder einer Conformist-Beziehung möchte das Team sein Modell „schützen“. Von außen gegebene Fremdmodelle werden zunächst in das eigene Modell übersetzt und validiert, bevor es in den eigenen Kontext gegeben wird.
Dieses Pattern mag eines der unscheinbarsten, aber wichtigsten sein, denn bildet es die Modellgrenze zwischen dem eigenen und einem anderen Context. Hier können bereits frühzeitig falsche, oder ungültige Daten aussortiert und überwacht werden.
Abbildung 3: Drei Kontexte mit eigenen Modellen und Anti Corruption Layern (ACL)
Abbildung 3 zeigt als Beispiel drei Kontexte mit eigenen Modellen. „Basket“ und „Checkout“ empfangen dabei Daten von „Inventory“, wandeln die Daten aber über ein Anti Corruption Layer in ihr eigenes Modell um und können sie dabei validieren, verwerfen, erweitern, überwachen und in den eigenen Kontext zur weiteren Verarbeitung überführen.
Gerade bei Verbindungen nach dem Conformist Pattern, bei denen man ggf. keine genauen Aussagen über die Datenqualität tätigen kann, ist ein Anti Corruption Layer essentiell um sein eigenes Modell konsistent zu halten.
Der letzte Schritt nach der organisatorischen Entkopplung besteht in der technischen Entkopplung. Gerade zwischen zwei Kontexten bieten sich eine asynchrone Kommunikation per Messagebus an. Der große Vorteil besteht in der Intransparenz der erzeugenden und der empfangenen Kontexten.
„Inventory“ emittiert für jedes neue Produkt, Produktänderung oder einem Delisting ein neues Event auf dem Bus. Welche Kontexte sich nun für diese Produktdaten interessieren, weiß „Inventory“ dabei nicht. „Basket“ und „Checkout“ interessieren sich dagegen sehr für Produkte und konsumieren die Events auf dem Bus. Von wem genau diese Events nun aber genau emittiert werden, ist nicht wichtig.
Abbildung 4: Entkoppelte Kontexte
Zusammenfassend kann man sehen, dass mit den DDD Bausteinen des Bounded Context, Subdomain und dem Strategic Design eine Softwaresanierung ein gangbarer Weg ist. Natürlich kann eine Herangehensweise auf so hoher Ebene nur mit anderen Bausteinen, wie einem ausgereiften CI/CD System, Testabdeckung oder Cleancode erfolgreich sein.