Domain-Driven Design (DDD) in Microservices
Wenn Unternehmen von einer monolithischen Architektur zu Microservices wechseln, stehen sie vor einer kritischen, hochriskanten Frage: Wie ziehen wir die Grenzen unserer Services?
Theoretisch sollten Microservices lose, entkoppelte Einheiten sein, die unabhängig voneinander entwickelt, bereitgestellt und skaliert werden können. In der Praxis bauen viele Teams jedoch letztendlich einen verteilten Monolithen auf – ein System, in dem Dienste so eng miteinander verbunden sind, dass eine einzelne Geschäftsänderung die gleichzeitige Änderung und Bereitstellung mehrerer Dienste erfordert, was zu Netzwerklatenz und Bereitstellungsblockaden führt.
Um diese Falle zu vermeiden, greifen Softwarearchitekten auf Domain-Driven Design (DDD) zurück. DDD wurde erstmals 2003 von Eric Evans eingeführt und ist eine Softwareentwicklungsmethode, die Codestrukturen an den komplexen Geschäftsdomänen ausrichtet, die sie darstellen.
In diesem Artikel werden wir untersuchen, wie DDD die strategischen und taktischen Blaupausen für die Gestaltung sauberer, entkoppelter und hoch wartbarer Mikrodienste bereitstellt.
1. Der strategische Entwurf: Die Grenzen des Dienstes ziehen
DDD ist in zwei Hauptphasen unterteilt: Strategisches Design und Taktisches Design. Beim strategischen Design geht es um die Modellierung von Geschäftskontexten und die Definition von Servicegrenzen. Es bietet die „Makro“-Ansicht der Architektur.
A. Allgegenwärtige Sprache
Innerhalb eines Unternehmens verwenden verschiedene Abteilungen dieselben Wörter, um völlig unterschiedliche Bedeutungen zu haben. Zum Beispiel:
- Für das Vertriebsteam ist ein „Benutzer“ ein Lead oder ein potenzieller Kunde.
- Für das Sicherheitsteam ist ein „Benutzer“ eine Reihe von Anmeldeinformationen.
- Für das Versandteam ist ein „Benutzer“ eine physische Empfängeradresse.
Der Versuch, ein einziges, einheitliches Datenbankmodell für einen „Benutzer“ zu erstellen, das alle zufriedenstellt, führt zu einer riesigen, verworrenen Codebasis. DDD löst dieses Problem durch die Etablierung einer allgegenwärtigen Sprache – eines gemeinsamen Vokabulars, das sowohl von Geschäftsbereichsexperten als auch von Entwicklern innerhalb einer bestimmten Grenze definiert und verwendet wird.
B. Begrenzte Kontexte
Ein Begrenzter Kontext ist die explizite Grenze, innerhalb derer ein Domänenmodell gilt. Innerhalb der Grenze haben alle Begriffe in der Ubiquitous Language eine einzige, eindeutige Bedeutung.
Anstelle eines einzelnen „Benutzer“-Modells definieren wir separate Modelle innerhalb ihrer jeweiligen begrenzten Kontexte:
- Im Bestellkontext haben wir ein
Order-Modell, das Kundenkontaktinformationen enthält. - Im Identitätskontext haben wir ein
Credentials-Modell. - Im Versandkontext haben wir ein
DeliveryAddress-Modell.
Die goldene Regel von Microservices: Ein begrenzter Kontext wird direkt einem Microservice zugeordnet.
Durch die Partitionierung des Systems in Bounded Contexts stellen wir sicher, dass die Microservices lose gekoppelt sind und unterschiedliche Geschäftsfunktionen darstellen.
C. Kontextzuordnung: Wie Dienste kommunizieren
Begrenzte Kontexte existieren nicht isoliert; sie müssen interagieren. Eine Kontextkarte definiert die Beziehungen und Übersetzungsmechanismen zwischen Kontexten. Zu den Schlüsselmustern gehören:
- Gemeinsamer Kernel: Zwei Kontexte teilen sich eine kleine Teilmenge des Domänenmodells und der Datenbank (von Microservices wird aufgrund der Kopplung normalerweise abgeraten).
- Kunde-Lieferant: Ein Kontext (Lieferant) muss einem anderen (Kunden) Daten bereitstellen. Der Lieferant muss die Freigabepläne mit dem Kunden abstimmen.
- Anti-Corruption Layer (ACL): Eine Übersetzungsschicht, die eingehende Daten von einem externen System in das interne Domänenmodell des Kunden übersetzt und so verhindert, dass externe Modelle die interne Architektur verunreinigen.
2. Der taktische Entwurf: Strukturierung der Microservice-Codebasis
Sobald die Dienstgrenzen mithilfe des strategischen Designs gezogen wurden, stellt Tactical Design eine Reihe von Entwurfsmustern bereit, um den Code innerhalb eines einzelnen Microservices zu strukturieren.
A. Entitäten und Wertobjekte
Innerhalb eines Microservices werden Modelle in zwei Kategorien unterteilt:
- Entitäten: Objekte, die eine eindeutige Identität haben, die über die Zeit hinweg bestehen bleibt, auch wenn sich ihre Attribute ändern. Beispiele hierfür sind
OrderoderProduct. - Wertobjekte: Objekte, die keine eindeutige Identität haben und vollständig durch ihre Attribute definiert werden. Sie sind unveränderlich. Beispiele hierfür sind
ShippingAddressoderProductDimensions. Wenn zwei Wertobjekte die gleichen Attribute haben, gelten sie als gleich.
B. Aggregate und Aggregatwurzeln
Ein Aggregat ist ein Cluster zusammengehöriger Entitäten und Wertobjekte, die bei Datenänderungen als eine einzige Einheit behandelt werden. Jedes Aggregat verfügt über einen Aggregate Root, der den einzigen Einstiegspunkt darstellt, über den externe Objekte mit dem Aggregat interagieren können. Der Stamm stellt sicher, dass alle Geschäftsinvarianten (Regeln) durchgesetzt werden.
Zum Beispiel im Diagramm oben:
- Im Bestellservice ist
Orderder Aggregate Root. Es enthältOrderItem(Entität) undShippingAddress(Wertobjekt). Externe Dienste könnenOrderItemnicht direkt ändern; Sie müssen eine Methode im StammverzeichnisOrderaufrufen (z. B.order.AddItem()), die bestätigt, dass die Bestellung noch nicht versendet wurde.
C. Domänenereignisse
Ein Domänenereignis ist etwas, das in der Domäne passiert ist und für die sich Geschäftsexperten interessieren. Es wird verwendet, um Änderungen asynchron über Bounded Contexts hinweg zu kommunizieren.
Wenn ein Aggregat einen Befehl ausführt, veröffentlicht es ein Domänenereignis (z. B. OrderCreated). Andere Dienste hören auf dieses Ereignis und aktualisieren ihren Status entsprechend, um letztendliche Konsistenz ohne synchrone API-Abhängigkeiten sicherzustellen.
3. Ein konkretes Beispiel: Bestellservice vs. Lagerservice
Schauen wir uns eine vereinfachte Implementierung des taktischen DDD in Go für den Order Service an und zeigen, wie der Aggregate Root Geschäftsregeln koordiniert.
package domain
import (
"errors"
"time"
)
// Value Object (Immutable, identity-less)
type ShippingAddress struct {
Street string
City string
ZipCode string
}
// Entity (Has identity, mutable)
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Aggregate Root (Enforces transactional boundaries)
type Order struct {
ID string
Items []OrderItem
Address ShippingAddress
Status string
CreatedAt time.Time
}
// NewOrder creates a new Order Aggregate Root
func NewOrder(id string, address ShippingAddress) *Order {
return &Order{
ID: id,
Items: []OrderItem{},
Address: address,
Status: "PENDING",
CreatedAt: time.Now(),
}
}
// AddItem enforces business rules before mutating state
func (o *Order) AddItem(productID string, qty int, price float64) error {
if o.Status != "PENDING" {
return errors.New("cannot add items to a finalized or cancelled order")
}
if qty <= 0 {
return errors.New("quantity must be greater than zero")
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: qty,
Price: price,
})
return nil
}
Wenn die Bestellung erfolgreich gespeichert wurde, veröffentlicht der Dienst ein Ereignis an einen Nachrichtenbroker:
{
"event_id": "evt_98231",
"event_type": "OrderCreated",
"timestamp": "2026-06-26T00:15:00Z",
"payload": {
"order_id": "ord_5521",
"items": [
{ "product_id": "prod_88", "quantity": 2 }
]
}
}
Der Inventory Service lauscht auf dieses Ereignis, aktualisiert seine lokale StockLevel-Entität und schließt den Fluss asynchron ab.
4. Strategisches vs. taktisches DDD in Microservices
| Phase / Aspekt | Strategisches DDD | Taktisches DDD |
|---|---|---|
| Umfang | Globale Systemarchitektur (Makro) | Innerhalb einer einzelnen Service-Codebasis (Mikro) |
| Hauptziel | Zeichnen Sie saubere, entkoppelte Servicegrenzen | Modellieren Sie umfangreiche Geschäftslogik und erzwingen Sie Invarianten |
| Schlüsselkonzepte | Begrenzte Kontexte, allgegenwärtige Sprache, Kontextkarte | Entitäten, Wertobjekte, Aggregate, Domänenereignisse |
| Publikum | Architekten, Produktmanager, Entwickler | Softwareentwickler, Codeprüfer |
| Auswirkungen auf Microservices | Bestimmt die Anzahl und den Umfang der Leistungen | Bestimmt Datenbanktransaktionen und Verzeichnisstruktur |
Fazit: Beginnen Sie zuerst mit der strategischen DDD
Die Anwendung von Domain-Driven Design auf Microservices ist eine leistungsstarke Möglichkeit, um sicherzustellen, dass Ihre Architektur zu Ihrer Geschäftsstruktur passt. Allerdings machen Teams oft den Fehler, sich zu sehr auf taktische Muster zu konzentrieren (wie das Schreiben von Repository-Schnittstellen und Wertobjekten) und dabei das strategische Design zu ignorieren.
Wenn Sie die Grenzen nicht richtig festlegen, kann kein sauberer taktischer Code Ihr System davor bewahren, sich in einen verteilten Monolithen zu verwandeln. Beginnen Sie immer mit einer strategischen Zuordnung: Definieren Sie Ihre Ubiquitous Language, gruppieren Sie Logik in begrenzte Kontexte, bilden Sie die Kommunikationsflüsse ab und lassen Sie die Grenzen Ihrer Microservices auf natürliche Weise aus diesen Definitionen hervorgehen.