Domain-Driven Design — Patterns & Practices
Wann DDD verwenden
Verwenden, wenn:
- Komplexe Geschäftslogik über CRUD hinaus
- Mehrere Fachexperten mit unterschiedlichen Vokabularen
- Das Domänenmodell ist der Wettbewerbsvorteil
- Unternehmenskritische Anwendungen
Nicht verwenden, wenn:
- Einfache CRUD-Anwendungen
- Hackathon/MVP (Overkill)
- Kein Fachexperte verfügbar
Strategisches Design
Bounded Contexts
Das wichtigste DDD-Konzept. Ein Bounded Context ist eine Grenze, innerhalb derer ein bestimmtes Modell definiert und anwendbar ist.
Beispiel — E-Commerce:
[Bestellkontext] [Zahlungskontext] [Versandkontext]
- Bestellung - Zahlung - Sendung
- Bestellposition - Transaktion - Sendungsnummer
- Kunde (Ref) - Rückerstattung - Adresse
- Adresse (Wert) - Rechnung - Spediteur
Kunde bedeutet in jedem Kontext etwas Anderes:
- Bestellkontext: Name, E-Mail, Versandpräferenz
- Zahlungskontext: Rechnungsdaten, Zahlungsmethoden
- Support-Kontext: Ticketverlauf, Zufriedenheitswert
Kontextkarte
[Bestellung] ←→ [Zahlung] # Partnerschaft
[Bestellung] → [Versand] # Kunde-Lieferant
[Bestellung] → [Legacy CRM] # Anti-Corruption Layer
Taktisches Design
Entitäten
Objekte mit Identität. Zwei Entitäten mit denselben Attributen aber unterschiedlichen IDs sind unterschiedlich.
class User {
readonly id: UserId;
name: string;
email: Email; // Value Object
}
Value Objects
Objekte, die durch ihre Attribute definiert werden, nicht durch Identität. Unveränderlich.
class Email {
constructor(readonly value: string) {
if (!isValidEmail(value)) throw new InvalidEmailError(value);
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
Aggregate
Ein Verbund von Entitäten und Value Objects mit einer einzelnen Wurzelentität (Aggregate Root). Jeder Zugriff erfolgt über die Wurzel.
class Order { // Aggregate Root
private items: OrderItem[] = [];
addItem(product: ProductRef, quantity: number): void {
// Geschäftslogik HIER, nicht in einem Service
if (quantity <= 0) throw new InvalidQuantityError();
this.items.push(new OrderItem(product, quantity));
}
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
}
Aggregat-Regeln:
- Verweise auf andere Aggregate nur per ID
- Ein Aggregat pro Transaktion
- Eventuelle Konsistenz zwischen Aggregaten
Domain Events
Etwas, das in der Domäne passiert ist und für Fachexperten relevant ist.
class OrderPlaced implements DomainEvent {
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly total: Money,
readonly occurredAt: Date
) {}
}
Domain Services
Geschäftslogik, die natürlicherweise nicht zu einer Entität oder einem Value Object gehört.
class PricingService {
calculatePrice(order: Order, customer: Customer, promotions: Promotion[]): Money {
// Komplexe Preislogik unter Einbeziehung mehrerer Aggregate
}
}
Repositories
Abstraktion des Datenzugriffs. Ein Repository pro Aggregate Root.
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
Verzeichnisstruktur (DDD)
src/
├── modules/
│ └── orders/ # Bounded Context
│ ├── domain/
│ │ ├── entities/
│ │ │ └── order.ts # Aggregate Root
│ │ ├── value-objects/
│ │ │ └── money.ts
│ │ ├── events/
│ │ │ └── order-placed.ts
│ │ ├── services/
│ │ │ └── pricing.ts
│ │ └── repositories/
│ │ └── order.repository.ts # Interface
│ ├── application/
│ │ ├── commands/
│ │ │ └── place-order.ts
│ │ ├── queries/
│ │ │ └── get-order.ts
│ │ └── handlers/
│ │ └── place-order.handler.ts
│ └── infrastructure/
│ ├── persistence/
│ │ └── order.repository.impl.ts # Implementierung
│ └── api/
│ └── orders.controller.ts
Die Clean-Architecture-Abhängigkeitsregel
In DDD MÜSSEN Abhängigkeiten streng nach innen zeigen:
[ Frameworks & Treiber (Web, DB, UI) ]
└──▶ [ Interface Adapter (Controller, Gateways) ]
└──▶ [ Anwendung (Use Cases, CQRS Handler) ]
└──▶ [ Domäne (Entitäten, Value Objects) ]
- Die Domänenschicht hat NULL Abhängigkeiten von ORMs (Prisma, TypeORM), HTTP-Frameworks (Express, NestJS) oder externen SDKs.
- Repositories werden als Interfaces in der Domänen-/Anwendungsschicht definiert und in der Infrastrukturschicht implementiert.
Domain Events vs. Integration Events
- Domain Events: Stellen Zustandsänderungen innerhalb eines einzelnen Bounded Context dar.
- Werden direkt innerhalb des Aggregate Root ausgelöst (
order.addItem(...)löstOrderItemAddedaus). - Werden prozessintern vor dem Transaktions-Commit ausgeliefert.
- Werden direkt innerhalb des Aggregate Root ausgelöst (
- Integration Events: Werden über Bounded-Context-Grenzen hinweg veröffentlicht, um mit anderen Diensten zu kommunizieren.
- Werden über das Transactional-Outbox-Muster an Message Broker gesendet.
- Müssen rückwärtskompatible Schemas mit Versionierung verwenden.
Anti-Corruption Layer (ACL)
Wenn Daten aus einem externen Bounded Context oder einer Drittanbieter-API (z. B. Stripe, Salesforce) konsumiert werden:
- NIEMALS externe Domänenmodelle direkt in die eigene Domäne importieren.
- Einen ACL-Übersetzer/Adapter in der Infrastrukturschicht erstellen, um externe DTOs in die eigenen Value Objects und Entitäten zu konvertieren.
Anti-Patterns
- [FAIL] Anämisches Domänenmodell – Entitäten haben nur Getter/Setter, alle Logik in Services
- [FAIL] Großes Aggregat – Aggregate sollten klein sein und sich auf Invarianten konzentrieren
- [FAIL] Aggregat-übergreifende Transaktionen – eventual consistency verwenden
- [FAIL] DDD überall – DDD nur dort einsetzen, wo die Komplexität es rechtfertigt
- [FAIL] ORM-Entitäten, die in die Domäne auslaufen – Domänenentitäten dürfen nicht von
@Entity()oder ORM-Dekorateuren abhängen
Codebeispiele
Siehe EXAMPLES.md für detaillierte Codebeispiele.
Validierungs-Checkliste
Was während der Überprüfungsphase vor Abschluss der Aufgabe zu prüfen ist.
Häufige Fehler
Anti-Patterns und Dinge, die explizit vermieden werden sollten. Siehe TROUBLESHOOTING.md.
Integrationshinweise
Wie dieser Skill mit anderen Skills interagiert.
ddd Fehlerbehebung & Häufige Fehler
1. God-Aggregate
- Symptom: Der Aggregate Root enthält 20 Kind-Entitäten und das Laden erfordert das Verknüpfen Dutzender Tabellen.
- Ursache: ERD-Tabellen als Aggregatgrenzen behandeln, anstatt als transaktionale Konsistenzeinheiten.
- Lösung: Kleine Aggregate entwerfen. Auf andere Aggregate nur per ID verweisen, nicht per Objektreferenz.
2. Infrastruktur in die Domänenschicht auslaufen lassen
- Symptom: Domänenentitäten importieren Prisma, TypeORM-Dekorateure oder Express-Request-Objekte.
- Ursache: Clean-Architecture-Grenzen umkehren.
- Lösung: Die Domänenschicht muss reines TypeScript ohne externe Framework-Abhängigkeiten sein.
3. Transaktion, die mehrere Aggregate umspannt
- Symptom: Hohe Datenbank-Sperrkonflikte und Deadlocks bei parallelen Transaktionen.
- Ursache: Mehrere Aggregate Roots innerhalb derselben Datenbanktransaktion ändern.
- Lösung: Faustregel: Genau ein Aggregate Root pro Transaktion ändern. Domain Events für eventual consistency zwischen anderen Aggregaten verwenden.