A bounded context defines where one domain model and vocabulary apply. Ordering and Fulfillment can each have an Order with different data and rules. In .NET, express those boundaries through separate modules or services, keep model ownership explicit, and translate between contexts through agreed contracts instead of sharing domain entities.
Most teams instead try to build one unified domain model, and it slowly turns into a class with 50 properties that nobody owns. Bounded Contexts prevent this by giving each part of the business its own focused model, with explicit boundaries and explicit integration points.
What Is a Bounded Context?
A Bounded Context is a boundary within which a specific domain model applies. Inside a Bounded Context, every term has a precise, unambiguous meaning.
The word "Order" means different things to different parts of a business:
- In Ordering, an Order is a customer's purchase request with line items and pricing
- In Fulfillment, an Order is a picking list with warehouse locations and shipping labels
- In Billing, an Order is an invoice with payment terms and tax calculations
Each of these is a different model of "Order" - with different properties, different behaviors, and different rules.
A Bounded Context makes this explicit: each context has its own Order class, and they don't need to match.
Martin Fowler's bounded-context explanation describes why unifying those models can create contradictions.
The DDD getting-started guide connects this boundary decision to the implementation patterns.
Why Bounded Contexts Matter
Without Bounded Contexts, teams try to build a single unified model - one Order class that serves Ordering, Fulfillment, and Billing.
This "God Model" approach creates:
- Classes with 50+ properties - most irrelevant to any single use case
- Coupling - a change for Billing breaks Fulfillment
- Unclear ownership - who owns the
Orderclass? - Slow development - every team coordinates on the same model
Bounded Contexts solve this by giving each team their own model. Models are simpler, focused, and independently evolvable.
Bounded Contexts vs Subdomains
These are related but different:
- A Subdomain is a problem space concept - a natural division of the business domain
- A Bounded Context is a solution space concept - a boundary you define around a model
Ideally, each Bounded Context aligns with one subdomain. In practice, one subdomain might span multiple Bounded Contexts, or you might start with a single Bounded Context covering multiple subdomains and split later.
Identifying Bounded Contexts
Look for these signals:
Different language. If the Sales team and the Fulfillment team use the same word to mean different things, there's a boundary. This is where the Ubiquitous Language earns its keep.
Different data. If two teams need different properties for the same concept, they need different models.
Different rules. If the same operation has different validation rules in different parts of the system, those parts are different contexts.
Organizational boundaries. Teams that work independently usually represent different Bounded Contexts.
For a typical e-commerce system, you might identify:
Bounded Contexts in Code
In a Modular Monolith, a module can make a bounded context explicit. This is a useful design choice, not a rule that every folder or deployment must be a separate context:
Modules/
Sales/
Domain/
Product.cs ← Sales-specific product (pricing, promotions)
Application/
Infrastructure/
Ordering/
Domain/
Order.cs ← Ordering-specific order (line items, status)
Product.cs ← Just ProductId + Name + Price, not the full catalog
Application/
Infrastructure/
Fulfillment/
Domain/
Order.cs ← Fulfillment-specific order (warehouse, shipping)
Application/
Infrastructure/
Each module has its own Product and Order - different classes, different properties, different rules.
The Sales Context Product
These simplified read models show the data each context needs; aggregate behavior is separate. Sales represents the product as an offer:
namespace Sales.Domain
{
public sealed record Product(
Guid Id, string Name, decimal BasePrice, string Currency, bool IsActive);
}
The Fulfillment Context Product
namespace Fulfillment.Domain
{
public sealed record Product(
Guid Id, string Sku, string WarehouseLocation, decimal WeightInKg,
int QuantityInStock);
}
Same real-world concept. Completely different models. This is intentional.
Communication Between Bounded Contexts
Contexts usually communicate through well-defined integration points instead of sharing their entire models. Those relationships belong on a context map, where the technical integration and the team dependency are both explicit.
Integration Events
The most common approach: publish events that other contexts consume.
public sealed record OrderItemDto(Guid ProductId, int Quantity);
public sealed record OrderPlacedIntegrationEvent(
Guid EventId,
Guid OrderId,
Guid CustomerId,
IReadOnlyList<OrderItemDto> Items,
decimal TotalAmount,
string Currency);
public sealed class CreateFulfillmentOrderHandler(
OrderPlacedTranslator translator,
IFulfillmentOrders orders)
{
public Task Handle(OrderPlacedIntegrationEvent message, CancellationToken ct)
{
var order = translator.Translate(message);
return orders.SaveIfAbsentAsync(order, ct);
}
}
public interface IFulfillmentOrders
{
// Atomically insert once per OrderingOrderId, using a database unique key.
Task SaveIfAbsentAsync(FulfillmentOrder order, CancellationToken ct);
}
The integration event is a contract - a shared schema that both contexts agree on. It's not a domain event (which is internal to a context).
For reliable delivery, use the Outbox pattern. The handler above is the consumer body, called by your message transport. Its repository implementation must make the insert idempotent with a unique source-order key, because delivery can happen more than once. A separate existence check followed by an insert is not sufficient under concurrent delivery.
Anti-Corruption Layer
When consuming data from another context, use an Anti-Corruption Layer to translate external models into your context's language:
public sealed record PickingItem(Guid ProductId, int Quantity, string WarehouseLocation);
public sealed record FulfillmentOrder(
Guid OrderingOrderId, IReadOnlyList<PickingItem> Items);
public interface IWarehouseLocations
{
string GetLocation(Guid productId);
}
public sealed class OrderPlacedTranslator(IWarehouseLocations locations)
{
public FulfillmentOrder Translate(OrderPlacedIntegrationEvent message)
{
if (message.OrderId == Guid.Empty || message.Items.Count == 0)
throw new ArgumentException("An order ID and items are required.");
var pickingItems = new List<PickingItem>();
foreach (var item in message.Items)
{
if (item.ProductId == Guid.Empty || item.Quantity <= 0)
throw new ArgumentException("Invalid order item.");
var location = locations.GetLocation(item.ProductId);
ArgumentException.ThrowIfNullOrWhiteSpace(location);
pickingItems.Add(new PickingItem(item.ProductId, item.Quantity, location));
}
return new FulfillmentOrder(message.OrderId, pickingItems.AsReadOnly());
}
}
The ACL translates Ordering data into Fulfillment terms and enriches it from a local warehouse lookup. The records keep this example focused on translation; production aggregate constructors should enforce the remaining fulfillment rules. A missing warehouse location must follow an explicit retry or review policy in the consumer, rather than silently creating an unpickable order.
Data Isolation
Each Bounded Context should own its data:
- Separate schemas -
sales.products,fulfillment.products - Separate databases - in microservices architectures
- No direct dependency on another context's operational tables - use its contract or a separately owned reporting model
When one context needs data from another, one option is a local copy updated through integration events. A synchronous API is another option when the caller needs current data and can accept the availability dependency. The Fulfillment Context doesn't query the Sales database for product details - it maintains its own product data, synced through events.
This is the Database per Module pattern.
Context Mapping
Context Mapping describes the relationships between Bounded Contexts:
- Partnership - two teams cooperate and evolve together
- Customer-Supplier - one team provides, the other consumes
- Conformist - the consuming team accepts the supplier's model as-is
- Anti-Corruption Layer - the consuming team translates the supplier's model
- Shared Kernel - two contexts share a small, co-owned model
- Separate Ways - no integration, contexts are fully independent
For a deeper dive into relationships between modules, see my article on module communication patterns.
Common Mistakes
1. Single shared model across contexts. The "God Model" anti-pattern. If every context uses the same Order class, you have coupling, not boundaries.
2. Too many contexts. Each context has overhead (separate data store, integration events, mapping). Start with fewer, larger contexts and split when needed.
3. Contexts aligned to technical layers. "Database Context" and "API Context" are not Bounded Contexts. Contexts align to business capabilities.
4. Uncoordinated table sharing. If either context changes shared tables without agreement, it can break the other's model. A deliberate shared kernel is an exception: the teams jointly own a small shared model and its associated storage, and coordinate changes to it. Keep that shared portion explicit and small, as described in Evans's DDD Reference.
Summary
Bounded Contexts are the foundation of strategic Domain-Driven Design. They define clear boundaries where specific domain models apply, giving teams the freedom to model their part of the business independently.
Start by listening to the language. When the same word means different things to different people, you've found a boundary.
Thanks for reading.
And stay awesome!
Frequently Asked Questions
What is a bounded context in DDD?
A bounded context is a boundary within which a specific domain model applies. Inside the boundary, every term has one precise meaning. The same real-world concept, like Order, can have a completely different model in each context.
What is the difference between a bounded context and a subdomain?
A subdomain is a problem space concept, a natural division of the business. A bounded context is a solution space concept, a boundary you draw around a model. Ideally they align one to one, but in practice they can diverge.
How do you identify bounded contexts?
Listen to the language. When the same word means different things to different teams, or two teams need different data and rules for the same concept, you have found a boundary. Organizational boundaries are another strong signal.
Can two bounded contexts share a database?
Yes. Contexts can use the same database while owning separate schemas or tables. A deliberate shared kernel can also include jointly managed storage, but changes to that shared part require coordination between the teams.
How many bounded contexts should a system have?
As few as you can justify. Each context adds overhead: separate data, integration events, and translation. Start with fewer, larger contexts and split when the language or the team structure forces it.



