Domain-Driven Design: Bounded Contexts in der Praxis
Bounded Contexts in echten Codebases: klare Subdomänen-Grenzen, Context Mapping zwischen Teams und keine geteilten Domain-Modelle über Services.

Domain-Driven Design klingt akademisch, bis du auf das Problem stößt, das es löst: dein "User"-Modell hat 47 Felder, weil jedes Team seine eigenen Anforderungen hinzufügt. Das Billing-Team braucht Zahlungsmethodendetails, das Shipping-Team braucht die Adresshistorie, das Marketing-Team braucht Engagement-Metriken und das Produkt-Team braucht Feature Flags. Änderungen an diesem gemeinsamen Modell schlagen durch die gesamte Codebase durch, und niemand kann es anpassen, ohne sich mit drei anderen Teams abzustimmen.
Bounded Contexts lösen das, indem sie akzeptieren, dass unterschiedliche Teile deines Systems unterschiedliche Modelle desselben realen Konzepts brauchen. Ein "Customer" im Billing-Kontext ist ein anderer als ein "Customer" im Shipping-Kontext. Sie teilen eine Identität, haben aber völlig unterschiedliche Attribute, Verhaltensweisen und Invarianten. Der Versuch, sie in einem Modell zu vereinheitlichen, erzeugt Kopplung, die jedes Team ausbremst.
Was ein Bounded Context wirklich ist
Ein Bounded Context ist eine Grenze, innerhalb der ein bestimmtes Domain-Modell definiert und gültig ist. Innerhalb der Grenze hat jeder Begriff eine präzise, eindeutige Bedeutung. Außerhalb der Grenze kann derselbe Begriff etwas anderes bedeuten.
// ❌ One shared User model used everywhere
interface User {
id: string;
name: string;
email: string;
// Billing concerns
stripeCustomerId: string;
paymentMethods: PaymentMethod[];
billingAddress: Address;
// Shipping concerns
shippingAddresses: Address[];
preferredCarrier: string;
// Marketing concerns
emailPreferences: EmailPreferences;
lastEngagementDate: Date;
segmentIds: string[];
// Auth concerns
passwordHash: string;
mfaSecret: string;
lastLoginAt: Date;
// Product concerns
featureFlags: Record<string, boolean>;
plan: string;
trialEndsAt: Date;
}
// ✅ Separate models per bounded context
// Each context owns its own definition of the domain
// Billing Context
interface BillingCustomer {
customerId: string; // Same real person
stripeId: string;
paymentMethods: PaymentMethod[];
billingAddress: Address;
currentPlan: Plan;
}
// Shipping Context
interface ShippingRecipient {
recipientId: string; // Same real person, different name
shippingAddresses: Address[];
preferredCarrier: Carrier;
deliveryInstructions: string;
}
// Identity Context
interface AuthIdentity {
identityId: string;
email: string;
passwordHash: string;
mfaEnabled: boolean;
sessions: Session[];
}Grenzen von Bounded Contexts identifizieren
Die richtigen Grenzen zu finden erfordert Verständnis der Geschäftsdomain, nicht der technischen Architektur. Zwei Heuristiken, die funktionieren: Orientiere dich an der Teamstruktur der Organisation und achte auf Wörter, die für verschiedene Personen unterschiedliche Bedeutungen haben.
## Boundary identification heuristics
### 1. Linguistic boundaries
When the same word means different things to different teams,
you've found a context boundary.
"Order" in the sales context:
- A quote that could be modified or cancelled
- Has line items with negotiated prices
- Belongs to a sales rep
"Order" in the fulfillment context:
- An instruction to pick, pack, and ship items
- Has warehouse locations and pick paths
- Belongs to a fulfillment queue
"Order" in the billing context:
- An invoice to collect payment for
- Has payment terms and tax calculations
- Belongs to an accounts receivable process
### 2. Team boundaries (Conway's Law)
If different teams own different aspects of a concept,
each team probably needs its own model.
### 3. Rate of change
Parts of a model that change together should be in
the same context. Parts that change independently
should be in different contexts.
Billing rules change when pricing changes.
Shipping rules change when logistics partners change.
These change for different reasons → different contexts.Context Mapping: Wie Bounded Contexts zusammenhängen
Bounded Contexts leben nicht isoliert. Sie müssen Informationen austauschen. Die Context Map definiert, wie sich Kontexte zueinander verhalten und wie Daten zwischen ihnen fließen.
// Common context relationship patterns
// 1. ANTI-CORRUPTION LAYER
// Protects your context from another context's model
class ShippingAntiCorruptionLayer {
// Translate from the order context's model to shipping's model
translateOrder(salesOrder: ExternalSalesOrder): ShippingRequest {
return {
recipientId: salesOrder.customerId,
items: salesOrder.lineItems.map((item) => ({
sku: item.productSku,
quantity: item.quantity,
// Shipping doesn't care about price — that's billing's job
weight: this.lookupWeight(item.productSku),
dimensions: this.lookupDimensions(item.productSku),
})),
priority: this.mapPriority(salesOrder.shippingTier),
address: this.validateAddress(salesOrder.shippingAddress),
};
}
private mapPriority(
tier: 'standard' | 'express' | 'overnight'
): ShippingPriority {
const mapping: Record<string, ShippingPriority> = {
standard: ShippingPriority.NORMAL,
express: ShippingPriority.HIGH,
overnight: ShippingPriority.CRITICAL,
};
return mapping[tier] ?? ShippingPriority.NORMAL;
}
}
// 2. SHARED KERNEL
// A small, explicitly shared model between two contexts
// Both teams must agree on changes
interface SharedOrderId {
value: string;
format: 'ORD-YYYYMMDD-XXXXX';
}
// 3. PUBLISHED LANGUAGE
// A well-documented integration format
interface OrderPlacedEvent {
eventType: 'order.placed';
version: '2.0';
orderId: string;
customerId: string;
items: Array<{
sku: string;
quantity: number;
unitPriceCents: number;
}>;
totalCents: number;
currency: string;
timestamp: string;
}Grenzen im Code umsetzen
Die physische Struktur deines Codes sollte die Grenzen der Bounded Contexts widerspiegeln. Das funktioniert sowohl in einem Monolithen als auch über Services hinweg.
// Monolith with clear bounded context boundaries
// src/
// ├── contexts/
// │ ├── billing/
// │ │ ├── domain/
// │ │ │ ├── BillingCustomer.ts
// │ │ │ ├── Invoice.ts
// │ │ │ └── PaymentMethod.ts
// │ │ ├── application/
// │ │ │ ├── CreateInvoice.ts
// │ │ │ └── ProcessPayment.ts
// │ │ ├── infrastructure/
// │ │ │ ├── StripeGateway.ts
// │ │ │ └── BillingRepository.ts
// │ │ └── api/
// │ │ └── billingRoutes.ts
// │ ├── shipping/
// │ │ ├── domain/
// │ │ ├── application/
// │ │ ├── infrastructure/
// │ │ └── api/
// │ └── identity/
// │ ├── domain/
// │ ├── application/
// │ ├── infrastructure/
// │ └── api/
// └── shared/
// └── kernel/
// ├── OrderId.ts
// └── Money.ts
// ❌ Cross-context imports break boundaries
// In shipping/application/CreateShipment.ts:
import { Invoice } from '../../billing/domain/Invoice'; // NO!
// ✅ Communicate through events or explicit integration points
// In shipping/application/CreateShipment.ts:
import { OrderPlacedEvent } from '../../shared/kernel/events';
class CreateShipment {
async handle(event: OrderPlacedEvent): Promise<void> {
// Transform event data into shipping domain objects
const acl = new ShippingAntiCorruptionLayer();
const request = acl.fromOrderPlacedEvent(event);
const shipment = Shipment.create(request);
await this.shipmentRepo.save(shipment);
}
}Kommunikation zwischen Kontexten
Bounded Contexts kommunizieren über Domain Events, API-Aufrufe oder gemeinsame Datenbanken (letzte Option). Events sind der am stärksten entkoppelte Ansatz.
// Event-driven communication between contexts
// Sales context publishes when an order is placed
class PlaceOrder {
constructor(
private orderRepo: OrderRepository,
private eventBus: EventBus
) {}
async execute(command: PlaceOrderCommand): Promise<void> {
const order = Order.create(command);
await this.orderRepo.save(order);
// Publish event — other contexts react independently
await this.eventBus.publish({
eventType: 'order.placed',
version: '2.0',
orderId: order.id,
customerId: command.customerId,
items: order.items.map((item) => ({
sku: item.sku,
quantity: item.quantity,
unitPriceCents: item.price.cents,
})),
totalCents: order.total.cents,
currency: order.total.currency,
timestamp: new Date().toISOString(),
});
}
}
// Billing context subscribes and creates an invoice
class OrderPlacedHandler {
async handle(event: OrderPlacedEvent): Promise<void> {
const customer = await this.customerRepo.findById(
event.customerId
);
const invoice = Invoice.createFromOrder({
orderId: event.orderId,
customerId: customer.customerId,
lineItems: event.items.map((item) => ({
description: item.sku,
quantity: item.quantity,
unitPrice: Money.fromCents(item.unitPriceCents, event.currency),
})),
paymentTerms: customer.currentPlan.paymentTerms,
});
await this.invoiceRepo.save(invoice);
}
}
// Shipping context subscribes and creates a shipment
class OrderPlacedShippingHandler {
async handle(event: OrderPlacedEvent): Promise<void> {
const recipient = await this.recipientRepo.findById(
event.customerId
);
const shipment = Shipment.create({
orderId: event.orderId,
recipient,
items: event.items.map((item) => ({
sku: item.sku,
quantity: item.quantity,
})),
});
await this.shipmentRepo.save(shipment);
}
}Wichtige Erkenntnisse
Ein Bounded Context ist eine Grenze, innerhalb der ein Domain-Modell eine präzise, eindeutige Bedeutung hat: Der "Customer" im Billing-Kontext (mit Zahlungsmethoden und Rechnungen) ist ein anderes Modell als der "Customer" im Shipping-Kontext (mit Adressen und bevorzugtem Versanddienstleister), und der Versuch, sie in einem Modell zu vereinheitlichen, erzeugt Kopplung, die jedes Team ausbremst. Finde Grenzen, indem du auf Wörter achtest, die für verschiedene Teams unterschiedliche Bedeutungen haben, den Besitzerlinien der Teams folgst und Dinge gruppierst, die aus demselben Grund geändert werden – diese linguistischen und organisatorischen Signale zeigen natürliche Domänengrenzen zuverlässiger als technische Analyse. Nutze Anti-Corruption Layers, um zwischen Kontexten zu übersetzen, damit das Modell jedes Kontexts sauber bleibt: Wenn der Shipping-Kontext ein Order-Event aus dem Sales-Kontext erhält, übersetzt er die Daten über eine explizite Transformationsschicht in seine eigenen Domain-Objekte, anstatt direkt vom Modell des Sales-Kontexts abzuhängen. Kommuniziere zwischen Kontexten über Domain Events für maximale Entkopplung: Der Sales-Kontext veröffentlicht "order placed" und Billing und Shipping reagieren unabhängig voneinander, indem sie das Event jeweils durch ihre eigene Domain-Brille interpretieren, ohne voneinander zu wissen oder sich um die Existenz des anderen zu kümmern.


