Guide

Choosing where to split is the hardest part of microservices. The most useful tool comes from domain-driven design (DDD), a method described in Eric Evans's 2003 book. A bounded context is a boundary inside which one model and one vocabulary stay consistent. A service, or a module in a modular monolith, usually wraps one context.

Concept

A domain model is the set of classes and rules that represent a business concept in code. A service boundary is the point where one team's model stops and another team's begins.

The big idea

The word "product" means different things in different parts of an online shop:

  • In Catalog, a product has a title, description, images and categories.
  • In Inventory, a product is a SKU with a quantity in each warehouse.
  • In Pricing, it has a base price, discounts and currency rules.
  • In Shipping, it is a weight and dimensions.

A single shared Product class with every field, used everywhere, couples every team to every other. Separate contexts, each with its own model and only an ID in common, let Pricing change its discount rules without touching Shipping.

Splitting by business capability works. Splitting by technical layer almost always fails. A "database service" or "validation service" means every feature touches every service.

Walking through it

Shipping only needs the weight of each item, so its context keeps a small model of its own:

TypeScript
type ShippingItem = { productId: string; weightGrams: number }

// Shipping only needs weights. It never sees prices or titles.
const parcelWeight = (items: ShippingItem[]) =>
  items.reduce((sum, item) => sum + item.weightGrams, 0)

console.log(parcelWeight([
  { productId: "sku-1", weightGrams: 450 },
  { productId: "sku-7", weightGrams: 1200 },
]))

Output:

1650

Four heuristics help draw good boundaries:

  • High cohesion inside, low coupling outside. Things that change together belong together. If every Orders change also needs a Payments change, they may be one context.
  • Each boundary owns its data. If two services must share tables to work, the boundary is in the wrong place.
  • Few, coarse crossings. A boundary crossed ten times per user request is a bad boundary.
  • One team per context. One team can own several contexts. A context should not be split across teams.

Uber shows the same idea at a much larger scale. Around 2020, they described roughly 2,200 critical microservices. Their response was to group services into domains, each with a single gateway, a structure they called DOMA. Even with thousands of services, the fix was more structure at the boundaries, which is bounded contexts layered over services.

Common mistakes

  • One shared model for everyone. It couples every team, and every change ripples outward.
  • Drawing boundaries by layer. Every feature then crosses every boundary.
  • Splitting one context across teams. Nobody owns its model.
  • Ignoring chatty boundaries. If a request crosses the same boundary many times, the boundary is probably wrong.

Check yourself

  1. Why does splitting by technical layer usually fail?

    Every feature then touches every service, so one change crosses all the boundaries.

  2. A user request crosses the same boundary ten times. What does that suggest?

    The boundary is probably in the wrong place. Good boundaries are crossed rarely, and in coarse calls.

Key takeaways

  • A bounded context is a boundary where one model and one vocabulary hold.
  • Split along business meaning, not technical layers.
  • Each context owns its data, and one team owns each context.
  • Good boundaries are crossed rarely and coarsely.

Further learning

Next: service communication, which covers how the services you draw talk to each other.