Bounded Contexts
IntermediateA bounded context defines the boundary within which a particular domain model is valid and consistent; each microservice ideally maps to one bounded context.
Overview
A Bounded Context is the central DDD (Domain-Driven Design) pattern for defining service boundaries. It is a logical boundary within which a specific domain model — its entities, value objects, aggregates, and business rules — is valid and internally consistent. The same word can mean different things across different bounded contexts: a "Customer" in the Order context might be just an ID and shipping address, while in the CRM context it is a rich object with interaction history and credit score. Bounded contexts communicate via well-defined APIs or domain events, and each context owns its data. In a microservices architecture, each service ideally maps to one bounded context. Getting boundaries wrong — either too tight (nanoservices) or too loose (distributed monolith) — is the primary cause of microservices complexity.
The Ubiquitous Language — Same Word, Different Meanings
Within a bounded context, the team uses a ubiquitous language — a shared vocabulary that is consistent between code and business conversation. The entity names, method names, event names, and database tables all use the same business terms.
The critical insight: the same term can have radically different meanings across contexts. An "Order" in the Fulfilment context cares about warehouse bins and shipping labels; an "Order" in the Finance context cares about invoices and revenue recognition. Forcing both meanings into one shared model creates an anemic, over-loaded entity that serves no context well. The solution is to maintain separate models with a translation layer (Anti-Corruption Layer) between them.
// ── Order context — cares about fulfilment ──────────────────────
package com.example.fulfilment.domain;
@Entity
public class Order {
private String orderId;
private String warehouseId; // fulfilment-specific
private List<PickItem> pickList; // fulfilment-specific
private ShippingLabel label; // fulfilment-specific
private FulfilmentStatus status; // PICKING, PACKED, SHIPPED
}
// ── Finance context — cares about billing ────────────────────────
package com.example.finance.domain;
@Entity
public class Order {
private String orderId;
private BigDecimal netAmount; // finance-specific
private BigDecimal taxAmount; // finance-specific
private String invoiceId; // finance-specific
private RevenueStatus revenueStatus; // DEFERRED, RECOGNISED
}
// Same concept, different bounded contexts → different models
// Communication: Fulfilment publishes OrderShippedEvent
// Finance subscribes and recognises revenueContext Map — Relationships Between Bounded Contexts
A context map documents how bounded contexts integrate. Key relationship patterns:
**Shared Kernel** — two contexts share a subset of the domain model (dangerous, couples teams).
**Customer/Supplier** — downstream (customer) consumes what upstream (supplier) provides; upstream must not break the downstream.
**Anti-Corruption Layer (ACL)** — a translation layer that converts the upstream model into the local model, protecting your domain from external concepts leaking in.
**Published Language** — the upstream defines a public, stable schema (OpenAPI spec, Avro schema) that multiple downstreams consume without needing to understand the upstream internals.
// Anti-Corruption Layer — translates external model to local model
// Fulfilment service consumes Order events from Order service
// but translates them to Fulfilment's own domain model
@Component
public class OrderEventTranslator {
// Order service publishes: {"orderId":"123","lineItems":[...],"shippingAddress":{...}}
// Fulfilment model needs: PickList with warehouseLocations
public FulfilmentOrder translate(OrderPlacedEvent externalEvent) {
// ACL: maps external schema → local domain model
List<PickItem> pickItems = externalEvent.getLineItems().stream()
.map(item -> PickItem.builder()
.sku(item.getProductSku())
.quantity(item.getQuantity())
.warehouseLocation(warehouseService.locate(item.getProductSku()))
.build())
.collect(Collectors.toList());
return FulfilmentOrder.builder()
.orderId(externalEvent.getOrderId())
.pickList(pickItems)
.status(FulfilmentStatus.PENDING)
.build();
}
}Bounded Context → Microservice Boundary
In practice, a bounded context maps to a microservice (or a module within a larger service). The boundary tells you: what data this service owns, what API it exposes, and what events it publishes/consumes. Wrong boundaries are the #1 source of microservices problems. Two failure modes:
**Too fine-grained (nanoservices)**: every method becomes a service; all calls are remote; latency and complexity skyrocket.
**Too coarse-grained (distributed monolith)**: services share databases, call each other synchronously for every operation, and cannot be deployed independently. All the cost of microservices with none of the benefits.
// Signs of a well-bounded service:
// ✅ Owns its database — no other service reads/writes its tables directly
// ✅ Can be deployed independently without coordinating with other teams
// ✅ Has a small, stable API surface (OpenAPI spec fits on one page)
// ✅ Changes to its internals do not require changes in other services
// ✅ Business language is consistent internally (ubiquitous language)
// Signs of a poorly-bounded service (distributed monolith):
// ❌ Service A calls Service B synchronously for every request
// ❌ Two services share the same database/schema
// ❌ Deploying one service requires deploying 5 others simultaneously
// ❌ The "Order" entity is 50 fields serving 8 different use cases
// Starting heuristic: if a business capability can be described in one sentence
// and owned by one team, it is likely a good bounded context.
// Examples:
// - "Manages the lifecycle of customer orders from placement to payment" → Order + Payment context
// - "Tracks warehouse inventory and fulfils pick lists" → Fulfilment context
// - "Manages customer profiles, preferences, and history" → CRM contextKey Points to Remember
- 1A bounded context defines a boundary within which a domain model is valid; same business term can mean different things across contexts.
- 2Each microservice should ideally map to exactly one bounded context — it owns its data and exposes a stable API.
- 3Use an Anti-Corruption Layer (ACL) to translate external models into your local domain model — prevents coupling to other contexts.
- 4Context map patterns: Shared Kernel (risky coupling), Customer/Supplier, ACL (isolation), Published Language (stable public schema).
- 5Distributed monolith: services that share a database or require synchronous calls for every operation — worst of both worlds.
- 6Start with fewer, larger contexts and split when a context grows too complex — wrong splits are costlier than delayed splits.
Interview Questions
Sign in to ask AriaWhat is a bounded context and why is it important in microservices design?
What is an Anti-Corruption Layer and when would you use one?
What is a distributed monolith and how do you identify one?
How do you decide where to draw the boundary between two bounded contexts?
The same word "Customer" appears in three different services with different meanings. Is this a problem? How do you handle it?
Ask Aria about Bounded Contexts
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.