Bounded Context in Microservices: Drawing Boundaries That Last
A bounded context in microservices is a clear boundary inside which one model and one vocabulary apply, so every term has exactly one meaning. Outside that boundary, the same word, such as "product" or "customer", may mean something else and is modelled differently. Bounded contexts are the most reliable guide for deciding where one microservice ends and the next begins.
Getting this boundary right decides whether your services can change independently, or whether every release turns into a meeting between three teams.
1. Why One Big Model Fails
Picture an online store that sells office equipment. The catalog team thinks of a product as a title, photos and a price. The warehouse team thinks of it as a weight, a shelf location and a stock count. The finance team thinks of it as a tax category and a VAT rate.
If you build one shared Product class for everyone, it slowly collects every field from every team. Nobody fully understands it, and a change for one team can break another. A rename for the catalog breaks a warehouse report. A new tax rule forces a redeployment of the product search.
The bigger problem is not the extra fields; it is meaning. Ask three teams "is this customer active?" and you may get three different answers, all correct within their own team. One shared model can only hold one of those meanings, so the others end up hidden in comments, flags and special cases.
A bounded context solves this by accepting that one universal model does not exist. Instead, each part of the business gets its own model, sized for its own job, inside a clear boundary.
2. What a Bounded Context Contains
The idea comes from Domain-Driven Design (DDD), an approach that shapes software around the business domain and the language of the people who work in it. A bounded context bundles three things:
- A model: the classes, rules and data that represent the business inside this boundary.
- A ubiquitous language: the shared vocabulary that developers and business experts use inside the boundary. "Ubiquitous" means it is used everywhere within the context: in conversations, in documents and in class and method names.
- An explicit boundary: usually one team's code, one deployable unit and one database schema. Inside, terms are precise. At the edge, data is translated.
The usual way to find contexts is to listen for language changes. When the warehouse people say "item" and mean a physical unit with a barcode, while the catalog people say "item" and mean a listing on the website, you have found a boundary.
3. How Bounded Contexts Talk to Each Other
Contexts still need each other. An order in Sales must eventually become a parcel in Shipping. A context map shows which contexts exist and how they relate.
Three relationship terms appear on almost every context map:
- Upstream and downstream. The upstream context provides data or decisions; the downstream context depends on them. Upstream changes can hurt downstream, never the reverse.
- Open host service and published language. The upstream offers a stable, documented API or event format for everyone, instead of custom integrations for each consumer.
- Anti-corruption layer (ACL). A translation layer on the downstream side that converts the other model into its own, so foreign concepts don't leak in.
In microservices, contexts usually communicate through REST or gRPC calls for questions that need an immediate answer, and through events, such as OrderPlaced, for facts that others react to later.
4. Code Examples
All three programs use only the JDK and Java 21.
4.1 Example 1: The Same Product in Three Contexts
Each context keeps its own Product record with only the fields it needs. They share nothing except the SKU, the stock-keeping unit code that identifies the item everywhere.
import java.math.BigDecimal;
import java.util.List;
public class ProductInThreeContexts {
// Catalog context: a product is something customers browse
static final class Catalog {
record Product(String sku, String title, List<String> imageUrls, BigDecimal listPrice) {}
}
// Warehouse context: the same SKU is a physical item on a shelf
static final class Warehouse {
record Product(String sku, int weightGrams, String binLocation, int unitsOnHand) {}
}
// Billing context: the same SKU is a taxable line item
static final class Billing {
record Product(String sku, String taxCategory, BigDecimal vatRate) {}
}
public static void main(String[] args) {
String sku = "KB-ERGO-02"; // the only thing all three contexts share
var shown = new Catalog.Product(sku, "Split ergonomic keyboard",
List.of("/img/kb-ergo-02-front.webp"), new BigDecimal("129.00"));
var stored = new Warehouse.Product(sku, 1140, "B-07-3", 42);
var taxed = new Billing.Product(sku, "ELECTRONICS", new BigDecimal("0.19"));
System.out.println("Catalog sees: " + shown.title() + " for " + shown.listPrice());
System.out.println("Warehouse sees: " + stored.unitsOnHand() + " units at " + stored.binLocation()
+ ", " + stored.weightGrams() + " g each");
System.out.println("Billing sees: " + taxed.taxCategory() + " taxed at " + taxed.vatRate());
}
}
Catalog sees: Split ergonomic keyboard for 129.00
Warehouse sees: 42 units at B-07-3, 1140 g each
Billing sees: ELECTRONICS taxed at 0.19
In a real system, these three records live in three different services, each with its own database. Notice that none of them is "the real product". Each is correct for its own context.
4.2 Example 2: One Word, Two Meanings
This example shows the problem a shared model would hide. Both contexts look at the same customer facts, but "active" means something different in each.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class ActiveCustomerDemo {
record CustomerFacts(String id, LocalDate lastPurchase, int unpaidInvoices) {}
// Marketing context: "active" means the customer bought something recently
static boolean isActiveForMarketing(CustomerFacts c, LocalDate today) {
return ChronoUnit.DAYS.between(c.lastPurchase(), today) <= 90;
}
// Billing context: "active" means the account is in good standing
static boolean isActiveForBilling(CustomerFacts c) {
return c.unpaidInvoices() == 0;
}
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 10, 28); // fixed date keeps the output repeatable
var customers = new CustomerFacts[] {
new CustomerFacts("C-1", LocalDate.of(2026, 10, 2), 0),
new CustomerFacts("C-2", LocalDate.of(2026, 3, 15), 0),
new CustomerFacts("C-3", LocalDate.of(2026, 9, 30), 2)
};
System.out.println("Customer | Marketing active | Billing active");
for (CustomerFacts c : customers) {
System.out.printf("%-8s | %-16s | %s%n",
c.id(), isActiveForMarketing(c, today), isActiveForBilling(c));
}
}
}
Customer | Marketing active | Billing active
C-1 | true | true
C-2 | false | true
C-3 | true | false
Customer C-2 has not bought anything for months but owes nothing: inactive for marketing, active for billing. C-3 is the reverse. A single Customer.isActive() method in a shared library would be wrong for one of the teams in both cases. This is the kind of bug that tests rarely catch, because each team's tests use its own meaning.
4.3 Example 3: Translating at the Boundary
When Sales publishes an OrderPlaced event, Shipping must not adopt the Sales model. A small translator, the anti-corruption layer, turns the event into Shipping's own Consignment.
import java.util.List;
import java.util.Map;
public class ShippingTranslatorDemo {
// Published language: the event Sales promises to keep stable for other contexts
record OrderPlaced(String orderId, String customerName, String street, String city,
String postcode, List<String> skus) {}
// Shipping's own model: it only cares about parcels and where they go
record Address(String recipient, String street, String city, String postcode) {}
record Consignment(String reference, Address deliverTo, int parcels, int totalWeightGrams) {}
// Anti-corruption layer: the one place that knows the Sales vocabulary
static final class OrderPlacedTranslator {
private final Map<String, Integer> weightBySku;
OrderPlacedTranslator(Map<String, Integer> weightBySku) {
this.weightBySku = weightBySku;
}
Consignment toConsignment(OrderPlaced event) {
int weight = event.skus().stream()
.mapToInt(sku -> weightBySku.getOrDefault(sku, 500)) // unknown SKU: assume 500 g
.sum();
int parcels = (weight + 9_999) / 10_000; // courier limit: 10 kg per parcel, rounded up
return new Consignment("SHP-" + event.orderId(),
new Address(event.customerName(), event.street(), event.city(), event.postcode()), parcels, weight);
}
}
public static void main(String[] args) {
var translator = new OrderPlacedTranslator(Map.of("KB-ERGO-02", 1_140, "DESK-OAK-160", 31_000));
var small = new OrderPlaced("A-501", "Lena Ortiz", "12 Harbour Road", "Bristol", "BS1 5TY",
List.of("KB-ERGO-02"));
var heavy = new OrderPlaced("A-502", "Omar Haddad", "4 Mill Lane", "Leeds", "LS2 7HZ",
List.of("DESK-OAK-160", "KB-ERGO-02", "MOUSE-V3"));
for (OrderPlaced event : List.of(small, heavy)) {
System.out.println(translator.toConsignment(event));
}
}
}
Consignment[reference=SHP-A-501, deliverTo=Address[recipient=Lena Ortiz, street=12 Harbour Road, city=Bristol, postcode=BS1 5TY], parcels=1, totalWeightGrams=1140]
Consignment[reference=SHP-A-502, deliverTo=Address[recipient=Omar Haddad, street=4 Mill Lane, city=Leeds, postcode=LS2 7HZ], parcels=4, totalWeightGrams=32640]
The translator is the only class in Shipping that knows the Sales vocabulary. If Sales changes its internal order model, only the translator may need to change. Shipping's rules, such as the 10 kg limit per parcel, stay safely inside Shipping. The unknown MOUSE-V3 got a default weight of 500 g; in production, you would log or alert on such gaps instead of guessing silently.
5. Bounded Context vs Microservice vs Subdomain
These terms are often mixed up in interviews.
| Concept | What it is | Belongs to | Typical size |
|---|---|---|---|
| Subdomain | A part of the business, such as shipping or billing | The problem: how the business works | Exists whether or not software does |
| Bounded context | A boundary where one model and language apply | The solution: how we design software | Often one team's area |
| Microservice | A separately deployable process | The deployment: how we run software | One context, or part of one |
| Module | A package or Maven module inside one application | The code structure | Can represent a context in a monolith |
A common rule of thumb is one bounded context per microservice. It is a starting point, not a law. A large context may be deployed as a few services owned by the same team, and in a modular monolith several contexts live in one deployable application as separate modules.
6. Common Mistakes and Pitfalls
1. Splitting by nouns instead of language. A CustomerService, ProductService and OrderService that each own "the one true" entity usually create chatty services and shared meanings. Split by business capability and language instead.
2. The shared "common-model" library. Putting domain classes in a shared JAR quietly brings back one big model. Every change forces all services to upgrade together. Share technical code, not business models.
3. Leaking the upstream model. Using another context's DTOs directly in your domain code ties your rules to their naming and structure. Translate at the edge, as in Example 3.
4. Contexts that are too small. If one business change always touches three contexts together, the boundaries are wrong. A context should keep most decisions for a feature inside itself.
5. Treating the boundary as a diagram only. If services still read each other's tables, the boundary exists only on paper. A context must own its data.
7. Best Practices
- Listen to the language. Hold workshops, such as Event Storming, with business experts and note where the meaning of words changes.
- Give each context one owning team, its own code repository or module, and its own database schema.
- Name things in the context's own language, even if it differs from other teams' language.
- Translate at every boundary with small, well-tested translator classes.
- Publish stable contracts: versioned APIs and events that hide internal models.
- Refer to other contexts by ID, such as a SKU or customer ID, never by sharing objects.
- Revisit boundaries as the business changes; moving a responsibility is normal.
8. Summary
- A bounded context in microservices is a boundary where one model and one vocabulary apply.
- The same word can mean different things in different contexts, and that is expected.
- Each context owns its model and data, and is often implemented as one microservice.
- A context map shows upstream and downstream relationships, APIs and events between contexts.
- Anti-corruption layers translate foreign models so they don't leak into yours.
- Avoid shared domain libraries, entity-based splits and contexts that only exist on diagrams.
When you design your next service, start by asking where the language changes. Drawing each bounded context in microservices along those lines gives you boundaries that hold up as the system and the teams grow.
Bounded Context: Interview Questions
Common interview questions on Bounded Context, from basic to scenario-based, with short answers.
Basic questions
Q. Who introduced the term "bounded context", and where?
Answer: Eric Evans introduced it in his 2003 book Domain-Driven Design: Tackling Complexity in the Heart of Software. It is part of what he called strategic design: the decisions about how a large system is divided, as opposed to the tactical patterns, such as entities and aggregates, used inside one model.
Q. Name the common relationship types on a context map.
Answer: The usual ones are partnership, shared kernel, customer–supplier, conformist, anti-corruption layer, open host service, published language and separate ways. They describe both the technical integration and the team relationship, for example whether the upstream team takes the downstream team's needs into account (customer–supplier) or not (conformist).
Q. What is a shared kernel, and why should you use it carefully?
Answer: A shared kernel is a small part of the model that two contexts agree to share and change only together, such as a Money type. It reduces duplication, but every change needs both teams' agreement, so it creates coupling. Keep it tiny and stable, or prefer duplication.
Intermediate questions
Q. How does Event Storming help find bounded contexts?
Answer: In Event Storming, developers and business experts place domain events, such as "Order placed" or "Parcel handed to courier", on a timeline. Clusters of events that use the same vocabulary and are handled by the same people suggest a context. Places where the meaning of a word changes, or where work passes from one group to another, often mark a boundary.
Q. How can you keep bounded contexts separate inside a single Spring Boot application?
Answer: Put each context in its own top-level package or Maven module and only expose a small API from each. Spring Modulith can then verify in a test that no module reaches into another module's internal packages:
ApplicationModules.of(ShopApplication.class).verify();
This gives you a modular monolith that can later be split into services along the same lines.
Q. Can two bounded contexts use the same database server?
Answer: Yes, as long as each context owns its own schema or tables and never reads or writes the other's. The problem is not the server, but shared tables: they couple the models and the release cycles. Many teams start with one server and separate schemas to save cost.
Q. How should one context refer to data owned by another context?
Answer: By identifier only. An order in Sales stores customerId, not a Customer object from the Customer context. When Sales needs customer details, it asks the owning context through its API, or keeps its own copy of just the fields it needs, updated from events.
Q. When is it acceptable to be a conformist instead of building an anti-corruption layer?
Answer: When the upstream model is good enough for your needs and translating would add little value, or when you have no influence over the upstream, as with some well-designed external APIs. Conforming saves code, but your model then changes whenever theirs does. If the upstream model is messy or unstable, the ACL is worth its cost.
Q. How is a bounded context related to an aggregate?
Answer: A bounded context contains many aggregates. An aggregate is a cluster of objects, such as an order and its order lines, that is changed together in one transaction to keep its rules consistent. Contexts set the boundary of the model and language; aggregates set the boundary of consistency inside it.
Tricky and scenario questions
Q. Scenario: Two teams keep breaking each other's releases because both change the shared customer table. What do you do?
Answer: Look at what each team means by "customer"; usually you will find two contexts sharing one model. Give each its own model and data, decide which context owns the core customer record, and let the other keep a copy of the fields it needs, updated from events. Move one access path at a time until no team writes to the other's tables.
Q. Scenario: A new loyalty-points feature needs data from Sales and Billing. Do you add it to one of them or create a new context?
Answer: Check whether loyalty has its own language and rules, such as "tier", "earn rate" and "expiry", and whether a different team or business owner is responsible for it. If yes, create a new Loyalty context that subscribes to events from Sales and Billing. Adding it to Sales would grow a model that already has a clear job.
Q. Can one bounded context be deployed as several microservices?
Answer: Yes. A large context, such as Catalog, might be split into a search service and a catalog management service owned by the same team and sharing the same language. What you should avoid is splitting a single aggregate across services, because its rules then need cross-service consistency.
Q. Is a bounded context the same as a team?
Answer: Not exactly, but they are closely linked. One team can own several contexts, but one context should be owned by only one team, otherwise its language and model drift apart. This follows Conway's law: systems tend to mirror the communication structure of the organisations that build them.