Skip to content

    System Design

    Domain-Driven Design and Bounded Contexts: Strategic Design

    Learn strategic domain-driven design: ubiquitous language, bounded contexts, core and generic subdomains and context map relationships, with a Java example.

    By Published 13 min read

    Share

    Standard lessonintermediateBackend System Design

    Before this: Monolith, Modular Monolith and Microservices

    What you'll learn: By the end of this lesson you will be able to explain why one model cannot serve a whole large system, define ubiquitous language and bounded context the way Evans does, sort a domain into core, supporting and generic subdomains, choose a relationship between two contexts (partnership, shared kernel, customer/supplier, conformist, anti-corruption layer, open host service, published language, separate ways), and keep a context separate from a module or a service.

    In a restaurant, "ready" means three different things. For the kitchen it means the dish is plated. For the host it means the table is cleaned and laid. For the cashier it means the bill is printed. Everyone uses the same word, nobody is wrong, and the restaurant works because each person knows which "ready" they are in. Trouble starts when a system built for the kitchen is asked to answer the host's question.

    Software has the same problem at a larger scale. Architecture styles says to split along business boundaries, and ShopStream Stage 8 shows seven contexts and a context map. This lesson is the dedicated treatment of the idea underneath: what a bounded context is, how to find one, and how contexts relate. Turning contexts into services with their own data is in microservice decomposition and data ownership. Aggregates, entities and value objects (tactical design) have a planned lesson, not yet published.

    Verified against Evans's Domain-Driven Design Reference (2015), Fowler's bliki pages and the Azure Architecture Center, on 2026-11-08. The Java example needs Java 16 or later and was run on JDK 26.0.1.

    The core idea

    Strategic design starts from one fact: a large system holds many models, and a model is only meaningful inside a boundary. DDD makes that boundary explicit and gives the language inside it one meaning. Evans defines a bounded context as a boundary, typically a subsystem or the work of one team, within which a particular model is defined and applicable. The rest of strategic design is deciding where to draw boundaries, where to invest, and how the contexts talk to each other.

    Three illustrations run through the lesson, each labelled:

    • Minimal example: two models of the same grocery item and a translation layer, in Java.
    • Realistic example: ShopStream adding a store point-of-sale feed to Inventory, in "In practice".
    • Failure example: one shared status type that every context must upgrade together.

    One word, many models

    Evans notes that multiple models are in play on any large project and that they emerge for many reasons: different user communities with different jobs, teams solving the same problem in different ways, different tool sets. They are inevitable, and combining code based on distinct models makes software buggy and hard to understand. Fowler adds, attributing the point to Evans, that total unification of the domain model for a large system will not be feasible or cost-effective, and that confusion recurs with words such as "Customer" and "Product".

    In ShopStream the word to watch is available:

    Context "Available" means Decided by
    Catalogue The product is listed and can be shown A merchandiser
    Inventory On hand minus reserved, at one store Counts and reservations
    Delivery A courier has capacity in a time slot Courier schedules
    Payments The funds or credit can be used The payment provider
    One word, four models: what 'available' means in each contextCatalogue to Inventory (by productId); Delivery to Payments (by orderId)PRODUCT AND STOCKFULFILMENT AND MONEYCatalogue: listed and sellableCataloguelisted andsellableInventory: on hand minus reservedInventoryon hand minusreservedDelivery: courier has a slotDeliverycourier has aslotPayments: funds can be usedPaymentsfunds can be usedby productIdby orderId

    Figure 1. The same word, four different meanings, each correct in its own context. How to read this: each box keeps its own definition. Contexts refer to each other by identifier and messages, never by sharing one class that tries to mean all four.

    Language and the boundary

    The ubiquitous language is, in Evans's definition, a language structured around the domain model and used by all team members within a bounded context to connect all the activities of the team with the software. The pattern asks the team to use the same words in speech, diagrams, writing and code, and to treat a change in the language as a change to the model: rename classes and methods when the words change. The language is local. A context has one, and the word means one thing inside it.

    Evans says to set the boundary in terms of team organisation, usage within specific parts of the application, and physical manifestations such as code bases and database schemas, and to apply continuous integration inside it so model concepts stay consistent. He also notes that as few as three or four people in one context can see the model fragment. Fowler's summary of the ubiquitous language adds that it must be rigorous because software does not cope well with ambiguity, and that talking with domain experts is part of testing the model.

    Signals that you have found a seam: the same word gets different answers from different experts; a class keeps growing fields that only one use case reads; two groups need different release rhythms or different rules for the same data. The Azure guidance suggests mapping business functions and their connections with domain experts, with event storming as one structured approach, then looking for closely related functions, core versus supporting functions, and the dependency graph between them.

    Core, supporting and generic subdomains

    Not every part of a domain deserves the same effort. Evans's core domain pattern says to define the critical core of the model, make it small, apply top talent to it, and justify investment in any other part by how it supports the core. His generic subdomains pattern says to factor out cohesive subdomains that are not the motivation for the project, give them lower priority, and consider off-the-shelf solutions. The Azure guidance names three categories:

    Kind What it is Typical treatment
    Core A competitive advantage that defines the business Deep modelling, strongest team
    Supporting Needed to operate, but not a differentiator Custom but simpler
    Generic A solved problem Buy or reuse

    For ShopStream, one illustrative answer (each business would decide for itself): the delivery promise and stock accuracy are core, Orders and Notifications are supporting, and sign-in and card processing are generic. The consequence is practical: the most careful modelling goes where it pays, and the team does not hand-build a payment provider.

    DDD is not free. Evans frames it as an approach for complex software and observes that many projects do modelling work without getting much benefit. A mostly create, read, update and delete (CRUD) area, such as a store's opening-hours page, rarely needs it.

    Context maps: how contexts relate

    A context map, in Evans's words, identifies each model in play, names its context, and describes the points of contact between models, with explicit translation for any communication. It maps the existing terrain first and leaves transformations for later. The relationships use two terms: upstream (its actions affect the downstream's success, but not the reverse) and downstream.

    Relationship Evans's idea ShopStream example (illustrative)
    Partnership Two teams that succeed or fail together plan and integrate jointly Cart and Checkout with Orders: both ship for the same release
    Shared kernel A small subset of model and code the teams agree to share, changed only after consultation A tiny shared Money type
    Customer/supplier Downstream needs enter upstream planning, with joint acceptance tests Cart as customer of Catalogue's prices
    Conformist Downstream adopts the upstream model as given, which simplifies integration Notifications taking Orders' events as published
    Anticorruption layer A translation layer that gives you the upstream's function in your own model Inventory around the store POS feed
    Open host service A protocol open to everyone who needs to integrate Inventory's stock API and events
    Published language A well-documented shared interchange language, often combined with an open host service Versioned StockChanged schema
    Separate ways No integration at all, because the benefit is small A seasonal promotions site with no link to stock
    Big ball of mud Draw a boundary around the mess and do not model it The old back-office system, drawn as one box
    flowchart TD accTitle: Choosing a relationship between two contexts accDescr: A decision flow. If the integration is worth little, go separate ways. If the other model is weak, legacy or foreign, build an anticorruption layer. If both sides succeed or fail together, form a partnership. If the shared part is small and both teams can govern it, use a shared kernel. If upstream will take your needs into planning, use customer and supplier. Otherwise conform. S["Two contexts must exchange information"] --> Q0{"Is the benefit of integrating small?"} Q0 -->|"yes"| SW["Separate ways"] Q0 -->|"no"| Q1{"Is the other model weak, legacy or foreign to yours?"} Q1 -->|"yes"| ACL["Anticorruption layer"] Q1 -->|"no"| Q2{"Do both teams succeed or fail together?"} Q2 -->|"yes"| P["Partnership: plan and release together"] Q2 -->|"no"| Q3{"Is the shared part small and governable by both teams?"} Q3 -->|"yes"| SK["Shared kernel"] Q3 -->|"no"| Q4{"Will upstream take downstream needs into planning?"} Q4 -->|"yes"| CS["Customer and supplier"] Q4 -->|"no"| CF["Conformist: adopt the upstream model"]

    Figure 2. A way to pick a relationship. How to read this: the order of questions is a teaching device, not a rule from Evans. Start with whether to integrate at all. Translation is the strongest and most expensive protection, so it comes before the cooperative options.

    Context, module and service are different things

    A bounded context is a boundary for a model. A module and a service are ways to package code. The Azure guidance says a microservice's functionality generally should not span more than one bounded context, and its worked example has one context (Shipping) containing several services. Evans lists code bases and database schemas as physical manifestations of a boundary. In a modular monolith, a context is typically a module with its own schema. When it later becomes a service, the context is the same and only the packaging changes. Hexagonal architecture is a way to structure code inside one context, and has its own planned lesson.

    Minimal example: two models and a translation layer

    The smallest honest example is two records for the same grocery item and a layer that translates a foreign feed into one of them. Catalogue's product is about reading and buying. Inventory's stock item is about counts at one store, with its own available. The upstream store POS speaks its own vocabulary (sku, shop, qty, flag), so Inventory translates, and refuses what it cannot map.

    record CatalogueProduct(String productId, String name, long priceCents) {}
    
    record StockItem(String productId, String storeId, int onHand, int reserved) {
        int available() { return onHand - reserved; }
    }
    
    record PosStockMessage(String sku, String shop, int qty, String flag) {}
    
    static StockItem fromPos(PosStockMessage m, Map<String, String> skuToProduct, int reserved) {
        String productId = skuToProduct.get(m.sku());
        if (productId == null) {
            throw new IllegalArgumentException("unmapped sku " + m.sku());
        }
        int onHand = switch (m.flag()) {
            case "OK" -> m.qty();
            case "DAMAGED_HOLD" -> 0;
            default -> throw new IllegalArgumentException("unmapped flag " + m.flag());
        };
        return new StockItem(productId, m.shop(), onHand, reserved);
    }

    Running a main that builds a product, translates an OK message for 8 units with 3 reserved, a DAMAGED_HOLD message, and a RECALLED message prints (the code is the full program minus its main, run on JDK 26.0.1 with java Contexts.java):

    Catalogue: CatalogueProduct[productId=p-100, name=Whole milk 1 L, priceCents=129]
    Inventory: StockItem[productId=p-100, storeId=s-12, onHand=8, reserved=3] available=5
    Held:      StockItem[productId=p-100, storeId=s-12, onHand=0, reserved=0] available=0
    Refused:   unmapped flag RECALLED

    Two points follow. The product and the stock item share only a productId, so neither model grows the other's fields. And the layer's default branch chooses to refuse an unknown flag instead of guessing: the foreign vocabulary stops at the boundary.

    Failure example: one status type for everyone

    A shortcut that looks like reuse: one OrderStatus library with every status, shared by Orders, Delivery and Notifications. Each context has a different idea of what a status means, so each needs different values.

    flowchart TD accTitle: A shared status type forces lockstep upgrades accDescr: Delivery adds a new status to the shared library. A new library version is released. Orders and Notifications must upgrade at the same time. One of them does not, and its switch statement falls through to a default branch that sends no message. A["Delivery needs a new status, handed to a neighbour"] --> B["Added to the shared status library"] B --> C["New library release"] C --> D["Orders and Notifications must upgrade together"] D -->|"one is missed"| E["Its switch hits the default branch"] E --> F["Shopper gets no message; nobody is paged"]

    Figure 3. How a shared model becomes a coupling. How to read this: the extra status matters only to Delivery, yet the others carry it. A dependency on a shared model is a dependency on every context that edits it.

    What breaks: a shopper is never told the order was left with a neighbour. Detection: a metric for messages that hit a default branch, and an acceptance test that every status an upstream can emit is handled downstream. Prevention: each context owns its own status vocabulary and publishes events in a published language, and each downstream maps what it receives, as the anticorruption layer above does. Evans names the result of unmanaged mixing: models blur, definitions become ambiguous and the software drifts toward a big ball of mud.

    In practice: ShopStream adds a store point-of-sale feed

    This is the realistic example. It is an educational scenario on ShopStream with illustrative details; nothing in it describes a real company.

    This is a realistic scenario, not a case history.

    The situation

    A chain of five partner stores (an illustrative subset of ShopStream's 5,000 stores) already runs its own point-of-sale (POS) system. The stores want ShopStream's stock counts to update from sales at the till, so shoppers do not order sold-out items. The POS sends a stock feed with its own codes. Inventory is the context that must consume it.

    Starting architecture

    ShopStream with a store POS feed: who relates to whomStore POS to Inventory (stock feed, POS codes); Inventory to Catalogue (StockChanged event); Orders to Inventory (reserve, open host service)Store POS: foreign modelStore POSforeign modelInventory: anticorruption layerInventoryanticorruptionlayerCatalogueCatalogueOrdersOrdersstock feed, POS codesStockChanged eventreserve, open host service

    Figure 4. Inventory is downstream of the POS and upstream of its own clients. How to read this: the POS model stops at Inventory's edge. Inventory's own clients see only its published language and API.

    Options considered

    Option Behaviour Cost Risk
    A. Conform to the POS model Inventory stores POS codes as they come Least translation code POS vocabulary spreads to Catalogue and Orders
    B. Shared kernel with the POS vendor One shared model and code Joint governance The vendor will not co-own ShopStream's model; changes stall
    C. Anticorruption layer in Inventory Translate into Inventory's model, refuse the unknown A mapping to write and maintain New vendor codes need a decision, not a silent guess

    The decision

    Option C. The POS is a foreign model that the stores control, and the five stores are few enough that a mapping table is cheap. Inventory exposes its own stock API and a versioned event schema, so consumers never see POS codes.

    sequenceDiagram accTitle: A POS message passing through the anticorruption layer accDescr: The store POS sends a stock message with its own codes. The anticorruption layer maps the SKU to a product id and the flag to an on-hand count. A mapped message updates the Inventory model and publishes StockChanged. An unmapped flag is parked for review and an alert is raised. participant P as Store POS participant A as Anticorruption layer participant I as Inventory model participant E as Event log P->>A: sku, shop, qty, flag alt sku and flag are mapped A->>I: StockItem with onHand I->>E: publish StockChanged else unmapped flag or sku A->>A: park message, count it, alert end

    Figure 5. Translate or park, never guess. How to read this: the top branch is the normal path. The bottom branch is the reason the layer exists: the foreign vocabulary changes without warning, and the layer decides what that means for Inventory.

    Rollout and verification

    Start with one store. Compare Inventory's available counts with a count taken at the shelf for a sample of items, and track the share of messages mapped, parked and refused. Add stores one by one and keep the mapping table in version control with a test per code.

    The 2 a.m. failure

    The page reads: stock for one product at three stores is frozen and shoppers order items the stores do not have. The POS vendor released a new flag, RECALLED, and the layer parks every message that carries it, so those counts stop updating. Checks: parked-message depth, the list of unmapped codes, and a comparison with a shelf count. Mitigation: confirm the flag's meaning with the vendor, map it (here, to zero on hand), and replay the parked messages. The lasting fix is an alert on the first unmapped code, a contract test against the vendor's code list, and an agreed default for unknown codes.

    Architect's notes

    • Name the contexts and the words. A one-page glossary per context beats a shared class.
    • Invest by subdomain. Strong modelling in the core, purchase in the generic.
    • Map before you change. Evans says to map the existing terrain and take up transformations later.
    • Prefer a translation layer to a shared model. Translation costs code once; sharing costs coordination every time.
    • Treat boundaries as hypotheses. Revise them when the language shows a new seam.

    Common mistakes

    • One model for everything. The "Product" class that serves four contexts.
    • Using DDD on CRUD. Heavy modelling where the domain is simple.
    • Letting foreign vocabulary in. Storing a vendor's codes in your domain.
    • A shared library of domain objects. It couples every context that imports it.
    • Confusing context with microservice. One context may be several services, or a module.

    Key takeaways

    • Large systems contain many models; a bounded context is the boundary within which one model and its language apply.
    • Ubiquitous language is per context, and a change in the language is a change to the model.
    • Core, supporting and generic subdomains decide where modelling effort goes.
    • A context map records relationships: partnership, shared kernel, customer/supplier, conformist, anticorruption layer, open host service, published language, separate ways.
    • Translate at the boundary and refuse what you cannot map.
    • Context, module and service are different: the context is the model boundary, the others are packaging.

    Practice exercises

    Exercise 1 (30 min). Pick a word from your own system that means different things to two teams. Write each team's definition in one sentence and name the contexts.

    Exercise 2 (45 min). Classify ShopStream's seven contexts as core, supporting or generic for a business of your choosing, and say what you would buy.

    Exercise 3 (1 hour). Extend the Java example with a second foreign feed, from a delivery partner, and decide for each unknown code whether to refuse, map or hold.

    Exercise 4 (2 hours). Draw the context map for a system you know, mark upstream and downstream on every arrow and the relationship type, and list where a translation layer is missing.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments