Ch. 10 · Microservices

How to Define Microservice Boundaries Around Business Capabilities

Drawing service boundaries around business capabilities, not entities or layers, with change history as evidence and signs a split is wrong.

~5 min readbeginnerupdated Oct 7, 2026

Most of the pain people associate with microservices comes from boundaries in the wrong place. Split a system well and teams ship independently, failures stay contained, and each service is small enough to understand. Split it badly and you get a distributed monolith: every feature touches four services, every request makes twelve network calls, and deployments need a coordination meeting.

“How would you split this system into services?” is one of the most common design questions for this reason. There’s no single right answer, but there are clear wrong ones, and a good answer shows the reasoning rather than just a list of boxes.

Capabilities, not nouns

The tempting first move is to make a service per entity: CustomerService, ProductService, OrderService, each with create, read, update and delete endpoints. It looks tidy on a diagram. In practice, almost every business operation touches several entities, so a simple “place an order” becomes a choreography of calls across services that each own a fragment of the logic.

A business capability is something the business does, described in its own language: pricing products, taking payments, fulfilling orders, shipping parcels, managing the catalogue. Each capability has its own rules, changes for its own reasons, and is often owned by a recognisable group of people. Capabilities make better boundaries because the logic for one capability tends to stay inside one service.

Splitting by technical layer (a “data service”, a “validation service”, a “UI service”) is worse still. Every feature change cuts across every layer, so every feature needs every team.

The same word, different models

Take “product” in an online shop. It means something different depending on who’s asking:

  • Catalogue: title, description, images, categories, search keywords.
  • Pricing: base price, currency, tax class, active promotions.
  • Inventory: stock per warehouse, reorder level, reserved quantity.
  • Shipping: weight, dimensions, hazardous-goods flag.

A single shared Product model with all of these fields couples every team to every other team’s changes. In domain-driven design, each of these is a bounded context: a part of the system where a term has one precise meaning. Each service keeps its own model of “product”, with only the fields it cares about, linked by a shared product id.

This is a good sign to look for in an interview prompt. When one word carries several meanings across the business, those meanings usually mark different contexts, and probably different services.

Using change history as evidence

Opinions about boundaries are cheap. If you’re splitting an existing system, its version history tells you which parts actually change together. A small analysis over the modules touched by recent commits (the kind of list git log --name-only produces, mapped to top-level modules):

const commits = [
  ['pricing', 'promotions'], ['pricing', 'promotions', 'cart'], ['shipping', 'carriers'],
  ['pricing', 'promotions'], ['orders', 'payments'], ['shipping', 'carriers', 'orders'],
  ['notifications'], ['pricing', 'promotions'], ['orders', 'payments'], ['shipping', 'carriers'],
  ['catalog', 'pricing'], ['orders', 'payments', 'notifications'],
];
// count each pair, and divide by the commits touching either module (Jaccard similarity)
JavaScript

Output from Node 22:

module pairs: commits together, and together / commits touching either
  pricing + promotions       4   80%
  carriers + shipping        3  100%
  orders + payments          3   75%
  cart + pricing             1   20%
  cart + promotions          1   25%
  orders + shipping          1   17%
  carriers + orders          1   17%
  catalog + pricing          1   20%
  notifications + orders     1   20%
  notifications + payments   1   25%
Text

Twelve commits is far too few for real decisions (you’d look at months of history), but the shape is what you’d look for. Three clusters stand out:

  • Pricing and promotions change together in 80% of the commits that touch either. Separating them into two services would turn most pricing changes into coordinated two-service deploys. They’re one capability.
  • Shipping and carriers always change together. Same conclusion.
  • Orders and payments change together 75% of the time. This one deserves a question rather than an automatic merge. Maybe they really are one capability. Or maybe the orders module contains payment logic that leaked across, and fixing that leak is the first job before any split.

The weak pairs (17 to 25%) are normal cross-capability changes. Some coupling between services always exists; the goal is that most changes stay inside one.

Signs a boundary is wrong

After a split, these symptoms usually point at a misplaced boundary:

  • Chatty synchronous calls. Rendering one page requires service A to call B five times, and B calls C for each. The data and the logic using it are on different sides of the line.
  • Lockstep deploys. Two services always deploy together because a change to one breaks the other.
  • Shared tables. Two services read and write the same database tables. That’s one service with two deployment units.
  • Distributed transactions everywhere. If most operations need a saga across the same two services, consider whether they belong together.
  • No clear owner. If nobody can say which team owns a service’s behaviour, it probably doesn’t match a capability anyone recognises.

How big should a service be?

Lines of code is the wrong measure. Useful questions instead: can one team own it fully? Can it be deployed without coordinating with others? Does it own its data? Does it hold together around one capability?

A very common early mistake is going too fine. Splitting “pricing” into price-calculation, discount-rules and tax-lookup services creates three deployments, three on-call rotations and network calls between things that always change together. Starting with fewer, larger services and splitting further when there’s evidence (a part changes much more often, needs to scale differently, or a new team takes it over) is cheaper than merging services back later.

Conway’s law is worth naming here: systems tend to mirror the communication structure of the organisation that builds them. Boundaries that cut across team lines will be painful; boundaries that match teams tend to stay healthy.

Interview practice: splitting a food delivery platform

“Design service boundaries for a food delivery platform: customers browse restaurants and menus, place orders, pay, restaurants accept and prepare orders, couriers pick up and deliver, and everyone gets notifications. What services would you start with, and why?”

Answer

I’d start from capabilities, not entities. Restaurant catalogue (restaurants, menus, opening hours, search), ordering (cart, order placement, order lifecycle), payments (charging, refunds, payouts to restaurants), restaurant operations (accepting orders, preparation status), delivery (courier assignment, tracking, delivery proof), and notifications.

Some of these I’d keep together at first. Ordering and restaurant operations both manipulate the order lifecycle; I might start them as one service and split later if the restaurant-facing side grows its own team. Delivery is clearly separate: it has its own domain (couriers, routes, geolocation), changes for different reasons and scales differently at peak times.

I’d point out that “order” means different things in different contexts. To payments, it’s an amount and a payment method. To delivery, it’s a pickup location, a drop-off location and a time window. Each service keeps its own model and receives what it needs through events like OrderPlaced and OrderReadyForPickup, rather than querying a shared order table. Each service owns its data. And I’d say up front that these boundaries are a starting point: if delivery and restaurant operations turn out to change together constantly, that’s evidence to revisit them.

Further reading

More in Microservices

esc