Bounded Contexts in Domain-Driven Design in der Praxis
Praktische Strategien für Bounded Contexts: Identifikation, Umsetzung und Integration mit Context Mapping, Anti-Corruption Layers und Shared Kernel.

Bounded Contexts sind das wichtigste taktische Muster in Domain-Driven Design, und doch werden sie am häufigsten missverstanden. Teams zeichnen entweder Kästchen auf Whiteboards, die nie Einfluss auf den Code haben, oder sie spalten jedes Konzept in einen eigenen Microservice auf und ertrinken in Integrationskomplexität. Der pragmatische Mittelweg startet mit der Sprache, die die Leute verwenden, und lässt Grenzen aus echten Modellierungsgesprächen entstehen.
Ein Bounded Context ist eine Grenze, innerhalb der ein bestimmtes Modell definiert und anwendbar ist. Dasselbe Wort — „Order“ — bedeutet in Vertrieb, Fulfillment und Buchhaltung unterschiedliche Dinge. Statt ein universelles Order-Modell zu erzwingen, definiert jeder Context sein eigenes, und explizite Übersetzungen finden an den Grenzen statt.
Bounded Contexts über Sprache entdecken
Das stärkste Signal für Context-Grenzen ist, wenn dasselbe Wort für verschiedene Gruppen unterschiedliche Bedeutungen hat. Event-Storming- oder Domain-Storytelling-Sessions machen das sichtbar.
// ❌ One "Product" model serving all contexts
interface Product {
id: string;
name: string;
sku: string;
price: number;
weight: number;
dimensions: Dimensions;
stockLevel: number;
reorderPoint: number;
description: string;
seoTitle: string;
images: string[];
supplier: string;
costPrice: number;
taxCategory: string;
// 30 more fields — every team adds their concerns
}
// The catalog team doesn't care about reorderPoint
// The warehouse team doesn't care about seoTitle
// Changes for one team's needs affect every other team// ✅ Each context defines "Product" in its own terms
// Catalog Context — what customers see
interface CatalogProduct {
id: string;
name: string;
description: string;
seoTitle: string;
images: string[];
price: Money;
availability: "in-stock" | "low-stock" | "out-of-stock";
}
// Inventory Context — what the warehouse tracks
interface InventoryItem {
sku: string;
stockLevel: number;
reorderPoint: number;
location: WarehouseLocation;
weight: Weight;
dimensions: Dimensions;
}
// Procurement Context — what purchasing manages
interface ProcurableGood {
sku: string;
supplier: SupplierId;
costPrice: Money;
leadTimeDays: number;
minimumOrderQuantity: number;
}Jedes Modell ist schlank und fokussiert. Das Katalog-Team kann sein CatalogProduct weiterentwickeln, ohne Inventarbelange zu berühren. Das Lager-Team kann sein InventoryItem umstrukturieren, ohne den Storefront zu beeinflussen. Die explizite Trennung zwingt dazu, darüber nachzudenken, wie Informationen zwischen Contexts fließen, statt anzunehmen, dass eine gemeinsame Datenbank alles synchron hält.
Context-Mapping-Muster
Sobald Bounded Contexts identifiziert sind, musst du definieren, wie sie zueinander in Beziehung stehen. Context Mapping gibt dir ein Vokabular für diese Beziehungen.
// Anti-Corruption Layer: translate external models to your own
// Used when integrating with a context you don't control
class CatalogAntiCorruptionLayer {
// Translate from Inventory context's model to Catalog's
translateAvailability(
inventoryItem: {
sku: string;
stockLevel: number;
reservedQuantity: number;
}
): "in-stock" | "low-stock" | "out-of-stock" {
const available =
inventoryItem.stockLevel -
inventoryItem.reservedQuantity;
if (available <= 0) return "out-of-stock";
if (available < 10) return "low-stock";
return "in-stock";
}
// Don't leak inventory's internal representation
// into catalog's domain
translateProduct(
externalProduct: Record<string, unknown>
): Partial<CatalogProduct> {
return {
availability: this.translateAvailability(
externalProduct as any
),
// Only extract what this context needs
};
}
}
// Shared Kernel: deliberately shared model between
// two closely-related contexts
// Used when two teams co-own a small, stable model
// shared-kernel/money.ts — owned by both Catalog
// and Pricing contexts
interface Money {
amount: number;
currency: Currency;
}
type Currency = "USD" | "EUR" | "GBP";
function addMoney(a: Money, b: Money): Money {
if (a.currency !== b.currency) {
throw new Error("Cannot add different currencies");
}
return { amount: a.amount + b.amount, currency: a.currency };
}// Published Language: a well-documented schema for
// inter-context communication
// Used for events that multiple contexts consume
// Domain events as the published language
interface OrderPlacedEvent {
type: "order.placed";
version: "2.0";
data: {
orderId: string;
customerId: string;
items: Array<{
sku: string;
quantity: number;
unitPrice: { amount: number; currency: string };
}>;
placedAt: string; // ISO 8601
};
}
// Each consuming context interprets the event
// through its own lens
// Inventory context: reserves stock
function handleOrderPlaced_Inventory(
event: OrderPlacedEvent
): ReservationCommand[] {
return event.data.items.map((item) => ({
type: "reserve-stock",
sku: item.sku,
quantity: item.quantity,
orderId: event.data.orderId,
}));
}
// Accounting context: creates receivable
function handleOrderPlaced_Accounting(
event: OrderPlacedEvent
): AccountingEntry {
const total = event.data.items.reduce(
(sum, item) =>
sum + item.unitPrice.amount * item.quantity,
0
);
return {
type: "accounts-receivable",
customerId: event.data.customerId,
amount: total,
currency: event.data.items[0]?.unitPrice.currency ?? "USD",
reference: event.data.orderId,
};
}Context-Grenzen im Code umsetzen
Bounded Contexts erfordern keine Microservices. In einem Monolithen setzen Modulgrenzen und Abhängigkeitsregeln die Context-Trennung durch.
// ❌ Contexts sharing database tables and imports
// catalog/ProductService.ts
import { InventoryRepository } from "../inventory/repo";
import { PricingEngine } from "../pricing/engine";
// Direct coupling between contexts
// ✅ Contexts communicate through defined interfaces
// Each context exposes a public API module
// inventory/public-api.ts
export interface InventoryQueryService {
getAvailability(
skus: string[]
): Promise<Map<string, AvailabilityStatus>>;
}
// catalog/dependencies.ts
// Catalog depends on the interface, not the implementation
export interface CatalogDependencies {
inventory: InventoryQueryService;
}
// catalog/CatalogService.ts
class CatalogService {
constructor(private deps: CatalogDependencies) {}
async getProductWithAvailability(
productId: string
): Promise<CatalogProduct> {
const product = await this.productRepo.findById(
productId
);
const availability =
await this.deps.inventory.getAvailability([
product.sku,
]);
return {
...product,
availability:
availability.get(product.sku) ?? "out-of-stock",
};
}
}// Module structure enforcing boundaries
// src/
// contexts/
// catalog/
// public-api.ts ← Only this is importable
// internal/ ← Module-private
// CatalogService.ts
// ProductRepository.ts
// models.ts
// inventory/
// public-api.ts
// internal/
// InventoryService.ts
// StockRepository.ts
// models.ts
// shared-kernel/
// money.ts
// types.ts
// ESLint rule to enforce boundaries
// eslint-plugin-boundaries configuration
const boundaryRules = {
"boundaries/element-types": [
"error",
{
default: "disallow",
rules: [
{
from: "catalog",
allow: ["shared-kernel", "catalog"],
},
{
from: "inventory",
allow: ["shared-kernel", "inventory"],
},
// Contexts can only import from shared-kernel
// and their own internals
],
},
],
};Integration von Bounded Contexts testen
Integrationspunkte zwischen Contexts brauchen Contract Tests, um sicherzustellen, dass Events und APIs kompatibel bleiben, während sich Contexts unabhängig weiterentwickeln.
// Contract test: verify event schema compatibility
import Ajv from "ajv";
const orderPlacedSchema = {
type: "object",
required: ["type", "version", "data"],
properties: {
type: { const: "order.placed" },
version: { const: "2.0" },
data: {
type: "object",
required: ["orderId", "customerId", "items"],
properties: {
orderId: { type: "string" },
customerId: { type: "string" },
items: {
type: "array",
minItems: 1,
items: {
type: "object",
required: ["sku", "quantity", "unitPrice"],
},
},
},
},
},
};
describe("Order context event contracts", () => {
const ajv = new Ajv();
it("OrderPlacedEvent matches published schema", () => {
const event: OrderPlacedEvent = createTestOrderPlacedEvent();
const validate = ajv.compile(orderPlacedSchema);
expect(validate(event)).toBe(true);
});
});
describe("Inventory context event consumption", () => {
it("handles OrderPlacedEvent v2.0", () => {
const event = createTestOrderPlacedEvent();
const commands = handleOrderPlaced_Inventory(event);
expect(commands).toHaveLength(event.data.items.length);
for (const cmd of commands) {
expect(cmd.type).toBe("reserve-stock");
expect(cmd.sku).toBeTruthy();
expect(cmd.quantity).toBeGreaterThan(0);
}
});
});Wichtige Erkenntnisse
Bounded Contexts werden anhand von Sprachunterschieden identifiziert — wenn dasselbe Wort für verschiedene Teams unterschiedliche Dinge bedeutet, hast du eine Context-Grenze gefunden, die im Code explizit sein sollte. Das Anti-Corruption-Layer-Muster übersetzt Modelle an den Context-Grenzen und verhindert, dass interne Konzepte eines Contexts in einen anderen durchsickern und starke Kopplung erzeugen. Shared Kernel sollten klein, stabil und bewusst gemeinsam im Besitz sein; wenn sich ein gemeinsames Modell häufig ändert, sollten die Contexts separate Modelle mit expliziter Übersetzung besitzen. Bounded Contexts erfordern keine Microservices — die Durchsetzung von Modulgrenzen über Abhängigkeitsregeln und öffentliche API-Schnittstellen erreicht Isolation innerhalb eines Monolithen. Domain Events dienen als Published Language zwischen Contexts: der produzierende Context definiert das Event-Schema, und jeder konsumierende Context interpretiert es durch sein eigenes Domain-Modell und wandelt es in Context-spezifische Commands um. Contract Tests an Integrationspunkten verifizieren, dass Event-Schemas und API-Schnittstellen kompatibel bleiben, während sich Contexts unabhängig weiterentwickeln, und fangen Breaking Changes ein, bevor sie die Produktion erreichen.


