What are Bounded Contexts?
A Bounded Context is a central pattern in Domain-Driven Design. It defines the boundary within which a particular domain model applies. Inside a bounded context, every term in the ubiquitous language has a precise and unambiguous meaning. The same term can mean different things in different bounded contexts — and that is perfectly fine.
Why Bounded Contexts?
- Linguistic clarity: "Customer" means one thing in Sales (a prospect) and another in Shipping (a delivery address)
- Model independence: Each context can evolve its model independently without breaking others
- Team autonomy: Different teams own different contexts and can work independently
- Technical flexibility: Each context can choose its own technology stack, database, and architecture
Identifying Bounded Contexts
// The same concept has different models in different contexts
// Sales Context: Customer = prospect with purchase history and preferences
interface SalesCustomer {
customerId: string;
name: string;
email: string;
tier: "bronze" | "silver" | "gold" | "platinum";
lifetimeValue: number;
preferences: CustomerPreferences;
purchaseHistory: PurchaseRecord[];
assignedSalesRep: string;
}
// Shipping Context: Customer = delivery recipient
interface ShippingCustomer {
recipientId: string;
name: string;
phone: string;
address: DeliveryAddress;
deliveryInstructions: string;
accessCode?: string;
}
// Billing Context: Customer = billable entity
interface BillingCustomer {
accountId: string;
legalName: string;
taxId: string;
billingAddress: Address;
paymentMethods: PaymentMethod[];
creditLimit: number;
outstandingBalance: number;
}
// Support Context: Customer = support ticket creator
interface SupportCustomer {
userId: string;
displayName: string;
email: string;
subscriptionPlan: string;
openTickets: number;
satisfactionScore: number;
}
Context Mapping
A Context Map visualizes the relationships between bounded contexts. It shows how contexts communicate, which team owns each context, and the nature of their integration. This is a strategic tool for understanding system boundaries and team dynamics.
Context Mapping Relationships
| Relationship | Description | Example |
|---|---|---|
| Shared Kernel | Two contexts share a common model — both teams must agree on changes | Shared User ID type between Auth and Billing |
| Customer-Supplier | Upstream (supplier) provides data, downstream (customer) consumes it | Product Catalog supplies data to Order Management |
| Conformist | Downstream adopts the upstream model as-is, with no translation | Small team uses a large platform's data model directly |
| Anti-Corruption Layer | Downstream translates upstream's model to protect its own domain | New system integrating with legacy system |
| Open Host Service | Upstream provides a well-defined public API for multiple consumers | Payment service used by orders, subscriptions, refunds |
| Published Language | A well-documented, shared language for integration (often paired with OHS) | JSON Schema, Protocol Buffers, OpenAPI |
Integration Between Contexts
// Integration via events (recommended for loose coupling)
// Order Context publishes
interface OrderPlacedIntegrationEvent {
eventType: "order.placed";
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
totalAmount: number;
shippingAddress: {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
};
occurredAt: string;
}
// Shipping Context translates to its own model
class ShippingOrderEventHandler {
constructor(private readonly shippingService: ShippingService) {}
async handleOrderPlaced(event: OrderPlacedIntegrationEvent): Promise {
// Translate from Order context model to Shipping context model
const shippingRequest: CreateShipmentRequest = {
externalOrderId: event.orderId,
recipient: {
// Only takes what Shipping needs — not the full customer model
id: event.customerId,
address: this.translateAddress(event.shippingAddress),
},
packages: this.calculatePackages(event.items),
priority: this.determinePriority(event.totalAmount),
};
await this.shippingService.createShipment(shippingRequest);
}
private translateAddress(orderAddress: OrderPlacedIntegrationEvent["shippingAddress"]): ShippingAddress {
return {
line1: orderAddress.street,
city: orderAddress.city,
region: orderAddress.state,
postalCode: orderAddress.zipCode,
countryCode: this.toISO3166(orderAddress.country),
};
}
}
// Integration via synchronous API with Anti-Corruption Layer
class OrderToInventoryACL {
constructor(private readonly inventoryClient: InventoryApiClient) {}
async checkAvailability(items: OrderItem[]): Promise {
// Translate from Order context to Inventory context
const inventoryRequest = items.map(item => ({
sku: item.productId, // Order calls it productId, Inventory calls it sku
requestedQty: item.quantity,
warehouseId: "default",
}));
const inventoryResponse = await this.inventoryClient.checkStock(inventoryRequest);
// Translate back to Order context model
return inventoryResponse.map(inv => ({
productId: inv.sku,
available: inv.availableQty >= inv.requestedQty,
availableQuantity: inv.availableQty,
estimatedRestockDate: inv.nextReplenishment ?? null,
}));
}
}
Context Boundary Guidelines
- One team per context: A bounded context should be owned by one team — shared ownership creates friction
- Separate databases: Each bounded context should own its data — no shared databases across contexts
- Explicit integration: Communication between contexts should be through well-defined APIs or events, never through shared code or direct database access
- Allow duplication: It is OK to duplicate data across contexts — prefer duplication over inappropriate coupling
- Size matters: A bounded context should be small enough for one team but large enough to be cohesive