Short answers to what readers ask most about this topic.
01What is a bounded context in domain-driven design?
A bounded context is the boundary within which one domain model applies and every term in the ubiquitous language has a single meaning. Outside the boundary the same word can mean something else, such as Item in sales versus inventory. DDD uses contexts instead of one unified model because a single model for a large system is rarely feasible or cost-effective.
02How do you find bounded contexts in an existing system?
Look for four signals that tend to agree: vocabulary that diverges between teams, clusters that emerge from event storming, clear team ownership, and clear ownership of who may change which data. Language and ownership are the strongest, because a boundary cutting through one team's vocabulary is expensive to fix. There is no mechanical process, so expect to revise the map.
03What is the difference between a bounded context and a microservice?
A bounded context is a modelling boundary, while a microservice is a deployment unit. One context per service is often ideal, but one context can span several services, or several contexts can share one for operational simplicity. Start with a module inside a monolith and promote it only when ownership, scaling or release cadence demands it.
04What is an anti-corruption layer and when should I use it?
An anti-corruption layer is a translation layer at the edge of a downstream context that converts an upstream model into its own vocabulary, so foreign or legacy concepts never leak in. Use it when two contexts have different semantics but must communicate, such as a legacy order system. Keep it to translation, and avoid putting business rules inside it.
05Can two bounded contexts share a database table?
They can, but that is effectively an unplanned Shared Kernel, and it recreates the single model that bounded contexts exist to avoid. Every schema change then needs both teams. Prefer each context owning its own tables and exchanging versioned events or an explicit API instead.
Bounded Contexts in Domain-Driven Design and How to Find Them
What a bounded context is, why Item means three things in ERP sales, inventory and accounting, and how to find boundaries and wire contexts with a context map.
A bounded context is the boundary inside which one domain model and one ubiquitous language stay consistent, so a word like Item can legitimately mean different things in sales, inventory and accounting. Find boundaries where vocabulary diverges, through event storming, and by checking team and data ownership, then connect contexts with a context map.
Open almost any ERP schema and you will find a table called items. It looks like the safest name in the system, and it is the one that quietly causes the most arguments: sales wants a price on it, the warehouse wants a bin location, and accounting wants an inventory account. Everyone is right, and no single class can satisfy all three without growing a column for every team.
This post explains bounded contexts, the Domain-Driven Design idea that dissolves that argument. It leans on Martin Fowler's BoundedContext note, the Azure Architecture Center's domain analysis guide, and Eric Evans's context map patterns as summarised on Wikipedia. The ERP example is a worked illustration with made-up round numbers, not a measured case study.
What is a bounded context in domain-driven design?
A bounded context defines the boundary within a domain where a specific domain model applies. Inside the boundary every term has one meaning and the model is internally consistent; outside it, the same term may mean something else. Fowler describes the motivation directly: DDD copes with large models by dividing them into bounded contexts and being explicit about how they relate.
The idea rejects the single unified model. Fowler quotes Evans that total unification of the domain model for a large system will not be feasible or cost-effective. Instead of one Item class that serves everyone, each context gets its own small model with only the attributes it needs. The Azure guide makes the same point with a drone: repair and predictive analysis need mileage and maintenance history, while scheduling needs only whether the drone is available.
Why does the same word mean different things in each context?
Because each team uses the word to do a different job. The language the team speaks with its domain experts is the ubiquitous language, and the Azure guide notes that each bounded context can have its own, which is why a word like account differs between contexts. Fowler calls Customer and Product recurring polysemes. In an ERP, Item is the clearest case.
Context
What Item means here
What it has no reason to know
Sales
A sellable thing with a list price, sold per box
Bin locations, lot numbers, ledger accounts
Inventory
A stockable thing counted in pieces, with reorder point and bin
Customer price, discounts, tax codes
Accounting
A valued asset with an inventory account and a standard cost
Which shelf it sits on, how it was quoted
Catalogue vs billing
The same split for Product: a described offering versus a charged line
Each side ignores the other's attributes
Notice that the three models do not contradict each other. They describe one physical thing from three angles. The trouble only starts when someone forces them into one table, because then a change that accounting needs, such as a new valuation method, has to be negotiated with sales and the warehouse as well. If you are designing the stock side in depth, the posts on ERP inventory management module design and the general ledger's double-entry design cover each model in its own right.
How do you find the boundaries between bounded contexts?
There is no mechanical process that produces the right answer; the Azure guide says so plainly. What you have is four signals that tend to agree when a boundary is real.
Language divergence. When people start prefixing a word, as in sales item or stock item, or when one word needs a different definition per meeting, the vocabulary has already split.
Event storming. In this workshop technique, stakeholders and developers lay domain events out on a wall as orange sticky notes, and clusters that share vocabulary and aggregates point to candidate contexts. Wikipedia credits it to Alberto Brandolini.
Team and ownership. Fowler argues the dominant factor is usually human culture, because the model is the ubiquitous language and you need a different model when the language changes. The Azure guide adds that a context needing many teams, or one team owning unrelated contexts, is a sign to revisit the boundary.
Data ownership. Ask who is allowed to change a fact. If only the warehouse may change on-hand stock, stock lives in inventory, and everyone else holds a copy or a reference.
When the signals disagree, trust the language and the ownership first. Data ownership is usually the easiest signal to repair later; a boundary that cuts through one team's vocabulary is the expensive kind to fix.
A cheap first test: write down every column name two teams use for the same real object. If they differ, or the same name carries different rules, you have found a candidate boundary before drawing a single box.
How do bounded contexts relate on a context map?
A context map documents the relationships between contexts and highlights integration points, as the Azure guide puts it. Evans's relationship patterns, summarised on Wikipedia and in the Azure guide, are the vocabulary for drawing it. The upstream context provides data or services, and the downstream context depends on them.
Pattern
Who adapts
Choose it when
Shared Kernel
Both teams, together
Two teams agree to share a small explicit subset of the model and keep it small
Customer-Supplier
Upstream supplies, and the teams negotiate the contract
The downstream team has real influence over the upstream roadmap
Conformist
Downstream adopts the upstream model as is
A custom interface is unlikely, so conforming simplifies integration
Anti-Corruption Layer
Downstream translates at its own edge
The upstream model is legacy or foreign and must not leak into yours
Open Host Service plus Published Language
Upstream publishes a stable API and format
Many downstream consumers need the same well-defined contract
A sixth pattern, Separate Ways, means two contexts do not integrate at all and each evolves on its own. The Azure guide's anti-corruption layer page adds the costs: extra latency, an extra component to run, and a rule to keep translation logic free of business rules and orchestration. In an ERP the common shape is inventory as an open host publishing events, accounting as a downstream with an anti-corruption layer, and a bought-in payment gateway as a context you simply conform to.
What does one concept look like in two contexts, in code?
The shape to aim for is three small models with no shared Item class, and an explicit translation function at each seam. The sketch below uses illustrative figures: a SKU sold in boxes of 12 pieces means five boxes become 5 times 12, or 60 pieces, at the boundary.
// The same real-world thing, modelled three times. No shared "Item" class.
// sales/domain/sellable-item.ts - what a customer can buy
export interface SellableItem {
sku: string;
displayName: string;
listPriceIdr: number; // price per BOX
unitsPerBox: number; // sales quotes in boxes
}
// inventory/domain/stock-item.ts - what a warehouse can count
export interface StockItem {
sku: string;
onHandPieces: number; // stock is counted in PIECES
reorderPointPieces: number;
binLocation: string;
lotTracked: boolean;
}
// accounting/domain/ledger-item.ts - what the books must value
export interface LedgerItem {
itemCode: string; // same value as sku, different name on purpose
inventoryAccount: string; // e.g. "1-1300"
cogsAccount: string; // e.g. "5-1000"
standardCostIdrPerPiece: number;
}
// sales/acl/to-reservation.ts - the translation function.
// Sales speaks boxes; inventory speaks pieces. Convert at the boundary,
// never inside either model.
export interface SalesOrderLine { sku: string; quantityBoxes: number }
export interface ReserveStockCommand { sku: string; quantityPieces: number }
export function toReservation(
line: SalesOrderLine,
item: SellableItem,
): ReserveStockCommand {
if (!Number.isInteger(line.quantityBoxes) || line.quantityBoxes <= 0) {
throw new Error("quantityBoxes must be a positive integer");
}
return {
sku: line.sku,
quantityPieces: line.quantityBoxes * item.unitsPerBox,
};
}
// Worked example: 5 boxes of a 12-piece SKU
// 5 x 12 = 60 pieces reserved in inventory.
Two details matter more than the syntax. The accounting model calls the identifier itemCode while the others say sku, and that is deliberate: each context names things in its own language and the translation owns the mapping. And the unit conversion lives in one function at the edge, so neither model ever has to know that the other counts differently.
How do contexts talk to each other without sharing a database?
Through published events or explicit APIs, never through each other's tables. Wikipedia's DDD article distinguishes domain events, which stay inside one context and carry light payloads, from integration events, which cross contexts and need fuller, more stable payloads because listeners vary. Below, inventory publishes a versioned integration event and accounting translates it into a balanced journal entry.
// Published by inventory (the upstream). This is its published language:
// small, versioned, and free of inventory internals such as binLocation.
export interface GoodsIssuedV1 {
type: "inventory.goods-issued.v1";
eventId: string; // idempotency key for consumers
occurredAt: string; // ISO 8601, UTC
sku: string;
quantityPieces: number;
}
// accounting/acl/on-goods-issued.ts - the downstream's anti-corruption layer.
// Accounting never imports inventory types; it translates the event into
// its own vocabulary: a balanced journal entry.
export function toJournalEntry(e: GoodsIssuedV1, item: LedgerItem) {
const amount = e.quantityPieces * item.standardCostIdrPerPiece;
return {
sourceEventId: e.eventId, // unique constraint here makes redelivery safe
lines: [
{ account: item.cogsAccount, debit: amount, credit: 0 },
{ account: item.inventoryAccount, debit: 0, credit: amount },
],
};
}
// Worked example: 60 pieces at a standard cost of 8,500 IDR per piece
// 60 x 8,500 = 510,000 IDR debited to COGS and credited to inventory.
// Debits (510,000) equal credits (510,000), so the entry balances.
The arithmetic is the check: 60 pieces at an 8,500 IDR standard cost is 510,000 IDR, debited to cost of goods sold and credited to inventory, so debits equal credits. The eventId doubles as an idempotency key. A unique constraint on sourceEventId in accounting means a redelivered event cannot post the same entry twice, which is what makes asynchronous delivery safe enough to rely on.
A table that two contexts both write to is a Shared Kernel you never agreed to. It looks like the shortcut, but it recreates the single unified model: every schema change now needs both teams, and nobody can tell which context is allowed to change what.
Should a bounded context be a module or a service?
Neither is implied by the idea. Wikipedia's DDD article notes that one context per microservice is typically ideal for clear boundaries and independent deployment, but one-to-many and many-to-one mappings can be appropriate, balancing DDD principles against operational overhead. On a single VPS the cheaper default is to make each context a module with its own folder, its own tables and a public interface, which the modular monolith architecture guide covers, and to isolate its ports as hexagonal architecture describes. Use this checklist before promoting a context to a service.
Does one team own the context end to end, and can it release without asking another team?
Does it have a different scaling, availability or release-cadence need from its neighbours?
Can its callers live with network failure and eventual consistency instead of a local call and one transaction?
Is the operational cost of another deployable, with its own monitoring and release process, worth paying now?
If the answers are mostly no, keep it a module. The boundary is the valuable part, and a well-drawn module can be extracted later because its edges are already explicit. The reverse is not true: services cut along the wrong lines turn every change into a distributed one.
Where do aggregates fit inside a bounded context?
Aggregates are the tactical layer inside a context. The Azure guide separates strategic DDD, which defines the large-scale structure through contexts, from tactical DDD, which gives entities, aggregates and domain services for building the model within one. An aggregate is a cluster of objects treated as a single consistency unit that you change through one root.
That is why the context boundary comes first. You cannot choose sensible aggregates for Item until you know which Item you are modelling: StockItem with on-hand quantity and bins in inventory, LedgerItem with accounts in accounting. Transactions stay inside one aggregate, and anything that crosses an aggregate or a context travels as an event.
Treat a bounded context as a promise that one word means one thing inside it. Find the boundary from vocabulary, ownership and the events people describe, give each context its own small model, and translate at the seams with a function you can test. Pick modules first and services only when the checklist says so.