Service boundaries
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 19 min · 5 figures

    Finding service boundaries with DDD

    How to draw service boundaries that hold: subdomains, ubiquitous language and bounded contexts, context maps, aggregates as consistency boundaries, EventStorming and practical heuristics, worked end to end on a food-delivery platform.

    A service boundary decides which code, data and decisions live together and which must talk over the network. Domain-driven design (DDD) gives you the tools to draw it well: split the business into subdomains, find the bounded contexts inside which one model and one vocabulary hold, map how those contexts relate, and keep each transaction inside one aggregate. Get the boundaries right and services change and deploy alone; get them wrong and you build a distributed monolith.

    Picture it first: one patient, five desks

    Walk through a big hospital. At registration, “the patient” is a name, a phone number and an insurance policy. At the lab, the patient is a barcode on a sample and a list of tests. At the pharmacy, it is prescriptions and allergies. On the ward, it is a bed number and a care plan. At billing, it is an invoice and a payer. Every desk keeps its own file in its own jargon, and the files are linked only by the hospital’s patient ID.

    Imagine forcing all five desks onto one giant shared form. Every new field needs every desk to agree, the lab can’t change its process without asking billing, and nobody knows what “status” means any more. The hospital works because each desk owns its own file, and hands over clear slips (“these tests are ordered”, “this patient was discharged”) when it needs another desk to act.

    Map it back. Each desk is a bounded context; its jargon is the ubiquitous language; the shared patient ID is how contexts reference each other by identity; the slips are domain events and commands. Medical care is the core domain; the canteen and the car park are generic subdomains that the hospital could happily outsource.

    The problem: boundaries in the wrong place

    Most bad microservice designs come from one of two splits.

    By technical layer. A “UI service”, an “API service” and a “database service”. Every feature needs a new screen, a new endpoint and a new table, so every feature changes all three services, and they release together.

    By noun. One service per entity: User, Restaurant, MenuItem, Order, Payment, Rider. Each is a thin create-read-update-delete wrapper round a table, so the business logic ends up in whoever calls them. The Order service becomes a hub everyone calls, and it is really a shared database behind an HTTP API.

    Take checkout on a Swiggy- or Zomato-style food-delivery app, split by noun. The checkout handler calls User, Address, Restaurant (is it open and does it deliver here?), Coupon, Order, Payment and Rider (for an ETA) once each, and MenuItem once per item. A four-item cart makes 7 + 4 = 11 synchronous calls. Six of them form a chain, because each needs the previous answer: user → address → restaurant → menu prices → coupon → payment. If each service is up 99.9% of the time and failures are independent, the chain succeeds 0.999⁶ ≈ 99.4% of the time. Over a 30-day month (43,200 minutes) that is 0.6% × 43,200 ≈ 259 minutes, about 4.3 hours of failed checkouts, against 43 minutes for one service (monolith vs microservices has the full arithmetic).

    A good boundary has three properties. Azure’s guidance defines two of them: services are loosely coupled if you can change one without updating others at the same time, and cohesive if each has a single, well-defined purpose. The third is data ownership: each fact has exactly one service that writes it. Put together, a well-bounded service changes for one business reason.

    Checkout on a food-delivery app, split two ways. Left: one service per noun, all called synchronously. Right: one service per bounded context; ordering keeps a local copy of menu prices and announces the order as an event.Notice The left design needs eight services up at the same moment for one checkout; the right needs two, and kitchen, dispatch and notifications can be down without blocking a single order.

    Domain and subdomains: where to invest

    The domain is the business you are building software for: getting food from restaurants to hungry people. It breaks into subdomains, areas of the business with their own experts, rules and goals. Azure’s guidance, following DDD practice, sorts them into three kinds:

    • Core subdomains are why you win. For a food-delivery platform that is rider dispatch and ETA prediction, and the ordering experience. Evans’s advice: keep the core small, put your best people on it, and model it deeply.
    • Supporting subdomains are necessary and specific to you, but not a differentiator: restaurant onboarding, menu management, support tooling. Build them, simply.
    • Generic subdomains are problems the industry has already solved: login, SMS and push notifications, payment processing, tax invoices. Evans suggests off-the-shelf solutions, and keeping your core developers away from them.

    The classification decides where effort goes. If your strongest engineers are building an in-house notification framework while the dispatch algorithm is a tangle of if statements, you have it backwards.

    Ubiquitous language and bounded contexts

    Ubiquitous language is Evans’s term for a rigorous vocabulary shared by developers and domain experts and used everywhere: in conversation, in documents and in the code. If the kitchen staff say “ticket” and the code says OrderPrepRequest, every conversation needs a translator. Evans adds that domain experts should object to awkward or inadequate terms, and developers should watch for ambiguity.

    The catch is that a big business has no single vocabulary. Martin Fowler recalls an electricity utility where “meter” meant the connection to a location, the connection to a customer, or the physical device, depending on who you asked; “customer” and “product” trip teams up again and again. DDD’s answer is the bounded context: an explicit boundary within which one model and its words have exactly one meaning. Evans asks you to set that boundary in team organisation, in which part of the application uses the model, and in physical form: code bases and database schemas. Fowler notes that the dominant force drawing these boundaries is human culture: when the language changes, you need a new model.

    In food delivery, “order” is the classic polyseme. Each context keeps only the fields it needs and links to the others by the order ID.

    One order ID, five models. Each bounded context gives “the order” its own name, fields and lifecycle.Notice No field appears in every card. A single shared Order table would carry all of them, and every team would need every other team’s permission to change it.

    Context mapping: how contexts relate

    Contexts still have to cooperate, and the relationship between two contexts is as much about teams as about code. A context map draws every context and labels each link. Evans’s vocabulary starts with direction: in an upstream–downstream relationship, the upstream team’s actions affect the downstream team’s success but not the other way round, like two cities on one river where only the upstream city’s pollution flows down.

    Pattern What it means Use it when Example here
    Partnership Two teams plan and integrate together They succeed or fail together Kitchen and Dispatch time “food ready” against rider arrival
    Shared kernel A small, explicitly shared slice of model and code, changed only by agreement Two close teams would otherwise duplicate core types Money and ID types shared by Ordering and Payments
    Customer–supplier The upstream plans for downstream needs; joint acceptance tests guard the interface The upstream team is willing to accommodate you Ordering is the customer of Payments
    Conformist The downstream adopts the upstream model as it is The upstream won’t change for you and its model is good enough Dispatch uses the maps provider’s coordinates and routes
    Anticorruption layer (ACL) The downstream translates the upstream model into its own at the boundary The upstream is legacy, messy or external Payments in front of the gateway and the finance ERP
    Open host service + published language The upstream offers one documented protocol and format to everyone Many consumers need the same thing Catalogue’s menu API and event schema
    Separate ways No integration at all Integrating costs more than it gives Rider training content

    The anticorruption layer is the one you will use most at the edges. A payment gateway has its own vocabulary of status codes, retry flags and settlement files; a finance team’s legacy ERP has ledger codes and vendor records from another era. Evans’s warning is that a large interface to such a system can slowly bend your model to resemble theirs. The ACL is an isolating layer, owned by the downstream context, that calls the other system through its existing interface and translates in both directions, so the rest of Payments only ever sees PaymentAuthorised, PaymentFailed and RefundIssued. If the gateway changes or you add a second one, only the ACL changes. The same layer is a key tool when replacing legacy systems (strangler fig and anticorruption layer).

    A context map for the food-delivery platform. Lines are relationships, not data flows; U and D mark the upstream and downstream ends.Notice Both external systems reach Payments only through an anticorruption layer, while Dispatch simply conforms to the maps provider: the choice depends on how much the outside model would hurt yours.

    Aggregates: the consistency boundary inside a context

    Bounded contexts split the business; aggregates split the data inside a context. Fowler’s summary: an aggregate is a cluster of domain objects treated as one unit, such as an order and its line items. One object is the root, outside code may hold references only to the root, you load and save whole aggregates, and transactions should not cross aggregate boundaries. Evans adds the distributed rule: inside an aggregate, apply consistency rules synchronously; across aggregates, handle updates asynchronously.

    Vaughn Vernon’s Effective Aggregate Design turns this into four rules of thumb:

    1. Model true invariants in consistency boundaries. An invariant is a business rule that must always hold, such as “the order total equals the sum of its lines”. Only the objects that one invariant ties together belong in one aggregate.
    2. Design small aggregates. Large ones lock more, load more and collide more. Vernon cites a team that built about 70% of its aggregates as a single root entity with value properties, and the rest with two or three entities.
    3. Reference other aggregates by identity. The order holds restaurantId, not a restaurant object, which keeps aggregates small and lets them live in different stores or services.
    4. Use eventual consistency outside the boundary. If one command must also change other aggregates, change one now and let the others follow through events. A well-designed bounded context modifies one aggregate instance per transaction.

    This has two consequences for service boundaries. Azure’s guidance puts the first neatly: a microservice should be no smaller than an aggregate and no larger than a bounded context. The second: things that must change together atomically, by a true invariant, live in one aggregate, so they can never be split across services.

    Whose job is it?

    When you can’t decide between one transaction and eventual consistency, Vernon passes on Eric Evans’s tie-breaker: if it is the job of the user running this use case to make the data consistent, keep it transactional. If it is another user’s job or the system’s, let it be eventually consistent. The kitchen accepting an order is the restaurant’s job, not the customer’s, so OrderPlaced and the kitchen ticket can be seconds apart.

    Domain events: the glue between contexts

    A domain event records something domain experts care about: “a delivery was cancelled” qualifies, “a row was inserted” doesn’t. Azure distinguishes internal events that stay inside a context from integration events that cross boundaries, published through a broker after the originating transaction commits. Publishing safely needs the transactional outbox; a multi-step business process across contexts becomes a saga, not a distributed transaction. Events versus commands, choreography and event design are covered in event-driven architecture.

    The Order aggregate in the Ordering context: one root, its lines and one invariant. Other aggregates are referenced by ID, and the kitchen’s aggregate is updated by an event in a separate transaction.Notice The two transactions: the order and its outbox row commit together, and the kitchen ticket is created a moment later. No transaction ever spans both contexts.

    Finding boundaries in practice

    EventStorming

    Alberto Brandolini’s EventStorming is a workshop for exploring a business domain quickly. Put the people with the questions and the people with the answers in one room, give them an effectively unlimited wall (a long paper roll), and start from domain events: things that happened and matter to the business, written in the past tense on orange sticky notes and placed along a timeline. Then explore where each event comes from: a command (blue) issued by a user, an external system or the passing of time (purple), or another event.

    The Big Picture variant, as the DDD Crew’s cheat sheet describes it, works with 10–30 people and adds a few more notes:

    • Pivotal events: the few most significant events in the flow, often the ones most people care about. For an online shop: Order Placed, Payment Received, Order Shipped, Order Delivered.
    • Hotspots (neon pink): conflicts, questions, pain points and disagreements, often inconsistencies in language.
    • Actors (small yellow) and policies (lilac): “whenever X happens, we do Y”.

    Boundaries show up in two places. Pivotal events split the flow into phases that different people own. And the language changes: Brandolini points out that conflicting interpretations of a term are the trigger for drawing a boundary between models. When the restaurant manager starts saying “ticket” where the customer side said “order”, you have crossed into another context. The DDD Crew calls the result emerging bounded contexts: a first draft to refine, not a final answer.

    A slice of a Big Picture EventStorming wall for food delivery, regrouped by emerging bounded context. Each column starts with the command that triggers its events.Notice The boundaries fall at pivotal events (★) and where the vocabulary changes: the kitchen talks about accepting, the rider app about picking up.

    Heuristics that move the line

    EventStorming gives you candidate seams. Test each one against these questions:

    1. Business capability. Does it match something the business does and could describe in a sentence (“assign riders”), rather than a table or a layer?
    2. Language. Do the words change meaning across the line? If “order” means the same thing on both sides, the line may be wrong.
    3. Data ownership. Who writes this data? Every fact should have one writer. Two contexts writing the same table means one boundary is missing.
    4. Rate of change. Pricing rules that change weekly shouldn’t share a release cycle with a tax module that changes yearly.
    5. Team ownership. Each context has one owning team. Azure says to revisit the boundaries or the team structure if one team owns several unrelated contexts, or one context needs many teams to coordinate (Conway’s law).
    6. Different non-functional needs. Rider location pings need write throughput; card data needs a small compliance scope. Different scaling or security needs justify a split inside a context too.
    7. Transactional consistency. If two things must change atomically, a true invariant, they belong in one aggregate and therefore on the same side.

    Then apply Azure’s validation checklist: each service has a single responsibility, there are no chatty calls between services, each is small enough for one small team, nothing must be deployed together, and the boundaries avoid data-consistency problems. When in doubt, start coarse: splitting one service later is easier than moving functionality across several.

    Worked example: a food-delivery platform

    Run a Big Picture session with product, operations, restaurant partnerships, finance and support. After a couple of hours the wall reads, left to right: Restaurant Onboarded, Menu Published, Item Marked Out of Stock, Item Added to Cart, Coupon Applied, Order Placed, Payment Authorised, Order Accepted, Rider Assigned, Food Ready, Picked Up, Delivered, Rating Submitted, Complaint Raised, Refund Issued, Payout Settled. The hotspots cluster round refunds (who pays when food arrives cold?) and round “order status”, which means something different to every group.

    Group the events by who owns them and where the language shifts, and six contexts emerge, plus generic ones such as identity and notifications:

    Context Owns Subdomain
    Catalogue Restaurants, menus, prices, stock flags Supporting
    Ordering Cart, checkout, the order’s lifecycle Core
    Payments Authorisation, capture, refunds, payouts; gateway and ERP behind ACLs Supporting, with a generic gateway
    Kitchen Accepting, preparing, food ready Supporting
    Dispatch Rider assignment, ETA, tracking, proof of delivery Core
    Support Complaints, cases, compensation decisions Supporting

    Now decide how they talk. The rule: synchronous only where a person is waiting for the answer, events everywhere else.

    • Ordering → Payments: one synchronous call to start payment, because the customer is staring at the payment screen. The result comes back as PaymentAuthorised or PaymentFailed.
    • Ordering publishes OrderPlaced. Kitchen turns it into a ticket; Notifications sends the confirmation.
    • Kitchen publishes OrderAccepted. Dispatch has a policy: whenever an order is accepted, find a rider.
    • Dispatch publishes Delivered. Payments captures the payment and accrues the restaurant’s payout.
    • Support → Payments: a command, “refund ₹180 for order 7781”, because Support needs to know whether it worked; RefundIssued follows as an event.
    • Copies, not calls. Ordering keeps prices from Catalogue’s MenuPublished events and Dispatch keeps restaurant locations, so neither calls Catalogue on the hot path. Support’s order history is a read model built from events, or API composition for rarer screens.

    Checkout drops from 11 synchronous calls across eight services to one call plus one event, and from 0.999⁶ ≈ 99.4% to 0.999² ≈ 99.8% availability: about 86 minutes of failure a month instead of 259. Kitchen, Dispatch and Notifications can all be down for ten minutes without one checkout failing; their events wait in the queue. The full product design is in design food delivery.

    A bounded context is not automatically a microservice

    The bounded context is a logical boundary for a model; a microservice is a deployment unit. They often coincide, but not always:

    • One context, several services, when non-functional needs differ inside it. Azure’s drone example splits a Delivery History service out of delivery because analytical storage differs from in-flight storage. Here, Dispatch might run location ingestion separately from rider assignment.
    • Several contexts, one deployable, early on. Build them as modules of a modular monolith, each with its own schema and a public interface enforced in CI. Moving a boundary between modules costs a refactor; between services it costs a data migration.
    • Never smaller than an aggregate. Splitting an aggregate across services turns its invariant into a distributed transaction.

    So the default path is: find contexts, build them as modules, and extract a service only when a seam has proved itself and something (scaling, team autonomy, a compliance scope) needs the split (monolith vs microservices, choosing an architecture style).

    In the interview

    Say the distinction out loud: “Bounded contexts are how I find the seams; services are how I deploy them. A service should be no smaller than an aggregate and no larger than a context, and I’d start with the contexts as modules in one deployable.”

    Comparing ways to split

    By technical layer By entity (noun) By business capability By bounded context By volatility or team
    Example UI, API and database services Order, Customer, Restaurant services Ordering, Payments, Dispatch Where one model and language hold Fast-changing pricing; one team’s area
    Cohesion Low: a feature spans every layer Low: logic scattered across callers High High: one model, one vocabulary Medium; high if it matches capabilities
    Coupling Every feature changes every service Hub entities called by everyone Loose, mostly events Loose, explicit on a context map Isolates change, may cut through a model
    Data ownership One shared database Tables split, meaning shared One owner per fact One owner per model Varies
    Calls per feature All layers, every time Many synchronous CRUD calls Few Few Depends
    Typical failure Lockstep releases Chatty calls, distributed transactions Capabilities drawn too vaguely Too many tiny contexts too early Boundaries follow the org chart, not the domain
    Five splitting strategies. Each row is the typical outcome; discipline can rescue a weak strategy and a careless team can spoil a strong one.

    Going deeper: anti-patterns and how to fix a boundary

    Anti-patterns

    • The shared database. Services that read and write the same tables are one system with extra network hops: every schema change is a cross-team negotiation (monolithic persistence).
    • Entity services. CRUD wrappers per noun push logic into callers and make every use case a chain of calls, as in the checkout above.
    • Nano-services. Services smaller than an aggregate, such as an OrderLine service, turn every invariant into a distributed transaction.
    • The leaky “common” library. A shared core-models package holding Order and Customer classes is a shared kernel nobody agreed to: one change forces every service to upgrade together. Share technical utilities and published contracts, not domain models.
    • The god context. One “Order Management” context that owns ordering, kitchen, dispatch and refunds because they all mention orders. Its language is vague and its team is the bottleneck.
    • Splitting too early. Boundaries drawn before anyone understands the domain are usually wrong, and wrong boundaries between services are expensive to move.
    • Chatty synchronous chains and distributed transactions. If a request needs five services in a row, or a two-phase commit across contexts, the contexts are probably cut in the wrong place.

    Signs a boundary is wrong

    Features keep changing the same two or three services together; one request makes several synchronous calls between the same pair; two services write the same table or keep the same field in sync by hand; teams can’t describe a service without naming another one. Any of these means the seam is in the wrong place.

    Moving a boundary

    Boundaries are a hypothesis; Azure calls boundary evaluation an ongoing effort. To move one:

    1. Merge before you re-split. If two services always change together, combining them is often the cheapest fix.
    2. Extract a module first. Inside the existing code, pull the new context behind an interface with its own tables, and enforce the boundary in CI.
    3. Strangle the old path. Route callers to the new context one use case at a time, with an ACL at the seam (strangler fig).
    4. Move the data last, with change data capture or a dual-read period, and only then delete the old tables (online schema and data migrations).

    Cheat sheet

    Term Remember it as
    Subdomain A part of the business: core (invest), supporting (build simply), generic (buy)
    Ubiquitous language One rigorous vocabulary shared by experts and code, within a context
    Bounded context The boundary where one model and its words mean one thing
    Context map Relationships between contexts: U/D, partnership, shared kernel, customer–supplier, conformist, ACL, OHS/PL, separate ways
    Aggregate Consistency boundary: one root, small, referenced by ID, one per transaction
    Domain event A business fact in the past tense; integration events cross contexts after commit
    Finding seams EventStorming: pivotal events and language changes; check ownership, change rate, team, consistency
    Service size No smaller than an aggregate, no larger than a bounded context
    Sync vs async Synchronous only where a person waits; events elsewhere; copies, not calls
    Arithmetic 6 synchronous hops at 99.9% ≈ 99.4%; 2 hops ≈ 99.8%

    How to say it in the interview

    “I wouldn’t split by layer or by table; that gives a distributed monolith where checkout calls eight services. I’d run an EventStorming session with the domain experts, lay out events like OrderPlaced, OrderAccepted, PickedUp and Delivered, and draw boundaries at pivotal events and wherever the language changes: ordering, payments, kitchen, dispatch, support. Each context owns its model and data, and I’d check every seam against who writes the data, how often it changes and which team owns it. Inside a context, aggregates are the transaction boundary. Between contexts, I reference by ID, keep local copies, and use one synchronous call only where the user waits, like starting a payment; everything else is events through an outbox. I’d build these as modules first and extract services where scaling or team ownership demands it.”

    How do you decide service boundaries? Start from business capabilities and language, not entities. Use EventStorming to find pivotal events and vocabulary changes, then test each seam: single writer for the data, similar rate of change, one owning team, no invariant split across it, and few synchronous calls across it.

    What’s a bounded context vs a microservice? A bounded context is a logical boundary for one model and vocabulary; a microservice is a deployment unit. A service should be no smaller than an aggregate and no larger than a context; a context can start as a module in a monolith or become several services for scaling.

    How do contexts share data? By ID references, events and local read-only copies. The owner publishes facts; consumers keep what they need. Synchronous calls are for when someone is waiting for the answer. Never a shared table.

    What’s an anticorruption layer? A translation layer the downstream context owns in front of a legacy or external system. It calls the system’s existing interface and converts its model into yours, so the payment gateway’s codes become PaymentAuthorised and never leak into your domain.

    How big should an aggregate be? As small as the true invariants allow, often a single entity. Reference other aggregates by ID, change one aggregate per transaction, and let the rest follow through domain events.

    Where to go next

    References and further reading

    1. Domain-Driven Design Reference: Definitions and Pattern Summaries (Eric Evans, 2015)domainlanguage.comBounded context, ubiquitous language, aggregates, domain events, the context-map patterns (partnership to separate ways), upstream and downstream, core domain and generic subdomains.
    2. BoundedContext (Martin Fowler, bliki)martinfowler.comWhy one unified model of a large domain isn’t feasible, polysemes such as “meter”, “customer” and “product”, and language change as the main force that draws boundaries.
    3. DDD_Aggregate (Martin Fowler, bliki)martinfowler.comAn aggregate as a cluster treated as one unit, outside references only to the root, and transactions that don’t cross aggregate boundaries.
    4. Effective Aggregate Design, Part I: Modeling a Single Aggregate (Vaughn Vernon, 2011)dddcommunity.orgModel true invariants in consistency boundaries, design small aggregates, one aggregate instance per transaction, and the report that about 70% of one team’s aggregates were a single root entity.
    5. Effective Aggregate Design, Part II: Making Aggregates Work Together (Vaughn Vernon, 2011)dddcommunity.orgReference other aggregates by identity, use eventual consistency outside the boundary, and the “whose job is it?” test for choosing between them.
    6. Use domain analysis to model microservices (Azure Architecture Center)learn.microsoft.comLoose coupling and cohesion defined, core, supporting and generic subdomains, bounded contexts and context maps in the drone-delivery example.
    7. Use tactical DDD to design microservices (Azure Architecture Center)learn.microsoft.comAggregate rules for microservices, internal versus integration events, and “no smaller than an aggregate and no larger than a bounded context”.
    8. Identify microservice boundaries (Azure Architecture Center)learn.microsoft.comFrom bounded contexts and aggregates to services, the validation checklist (no chatty calls, independent deploys), and starting coarse-grained.
    9. Introducing EventStorming (Alberto Brandolini, 2013)ziobrando.blogspot.comThe original workshop format: domain events on a timeline, commands and external triggers, aggregates found outside-in, and contexts emerging where terms conflict.
    10. EventStorming Glossary and Cheat Sheet (DDD Crew)github.comBig Picture EventStorming, pivotal events, hotspots, policies, actors and emerging bounded contexts.