Chapter 02 of 27 · Part 2: Decomposition

Decomposition strategies

How to find service boundaries: decompose by business capability or by subdomain, and keep coupling low.

key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral

What is a service?

A service has a public API of commands (change data), queries (read data) and events (announce changes). Its database is private. Size does not matter much: a good service is one a small team can build and change without waiting on others. Share libraries only for things that rarely change, like a Money class.

Anatomy of a serviceA client calls the public API. Inside are business logic and a private database. The service publishes events.ClientOrder servicePublic APIcommands and queriesBusiness logicPrivate DBEventsorder createdEverything goes through the API. Nobody touches another service's database.

Information hiding, cohesion and coupling

Building Microservices treats a microservice as another form of modular design, one with a network between the modules. So old ideas about good modules still apply. David Parnas showed that modules pay off only when their boundaries hide details. In his words, the connections between modules are the assumptions they make about each other, so make fewer assumptions.

Information hiding, cohesion and couplingInformation hiding gives three benefits. Cohesion is about inside a boundary and coupling about across it.Information hidingfewer assumptions between modulesFaster parallel workEasier to understandEasier to changeJudging a boundaryCohesion (inside)code that changes together stays togetherCoupling (across)a change here should not force a change thereConstantine's law: a structure is stable when cohesion is strong and coupling is low.

Cohesion is the relationship between things inside a boundary. Coupling is the relationship between things across it. A microservice is only independently deployable if its boundary is stable, and that needs strong cohesion and low coupling together.

Four kinds of coupling

Not all coupling is bad, but some kinds are far worse. Building Microservices ranks them using the fictional MusicCorp shop.

Four kinds of coupling, from loose to tightDomain, pass-through, common and content coupling with a fix for each.LooserTighterDomainneeds its functionKeep it smallsend the least dataPass-throughdata only passed onHide itcaller need not knowCommonshared read/write dataOne ownerothers send requestsContenttouches internal dataUse the APInever its tablesTop row: the kind of coupling. Bottom row: the usual fix.Temporal coupling is separate: both services must be up at once. Async messaging relieves it.
KindMusicCorp exampleWhat to do
DomainOrder Processor calls Warehouse to reserve stock and Payment to take moneyUnavoidable. Many downstream calls hint that one service does too much.
Pass-throughOrder Processor sends a Shipping Manifest to Warehouse only so Warehouse can forward it to ShippingLet Warehouse build the manifest itself, so Shipping changes stay hidden.
CommonOrder Processor and Warehouse both update the status column of one Order tableGive one Order service the table. It treats requests as requests and can reject bad transitions.
ContentWarehouse reads and writes the Order service's internal tables directlySend requests to the Order service instead.
A service that is just a thin wrapper around database create, read, update and delete calls is a warning sign. The business rules have leaked out to its callers, so cohesion is weak and coupling is tight.

The three-step process

Start from the requirements. Find the operations the system must handle, group them into services, then work out how services collaborate.

Three steps to define servicesRequirementsuser stories1. Operationswhat must it do2. Servicesgroup by business3. APIswho calls whom
createOrder collaborates with four servicesOrder service: createOrder()Consumerverify consumerRestaurantverify orderKitchencreate ticketAccountingauthorize cardVerbs in user stories become operations. Nouns become domain objects.

Strategy 1: business capabilities

A business capability is something the company does to make money. Capabilities stay stable even when the way of doing them changes, so services built around them stay stable too.

Business capabilities mapped to servicesWhat the business doesServiceCourier managementCourier serviceRestaurant informationRestaurant serviceConsumer managementConsumer serviceOrder managementOrder serviceRestaurant order preparationKitchen serviceCourier availability + deliveryDelivery serviceAccounting (all three kinds)Accounting serviceYour first guess. You can merge or split services later.

Strategy 2: DDD subdomains

Domain-driven design gives each subdomain its own model, called a bounded context. In practice you get nearly the same services as strategy 1.

Two decomposition guidelinesSingle ResponsibilityA service has one reason to changeOrder, kitchen and delivery are separateCommon ClosureThings that change together live togetherCure for the distributed monolith

The god class problem

In FTGO, Order is used by everything, so one class tries to do it all. DDD fixes this by letting each service keep its own small view of an order.

Splitting the Order god classOne bloated Order class with four responsibilities becomes three small models.Order (god class)Order takingRestaurant preparationDeliveryBillingOrder service: Orderstatus, total, itemsKitchen service: Ticketitems, prepare-by timeDelivery service: Deliverypickup, address, courierSame real-world order, three small views.

Domain-driven design in three words

The existing sections used DDD subdomains. Building Microservices highlights three of its ideas that matter most for service boundaries.

IdeaMeaningWhy it helps
Ubiquitous languageUse the same words in code as the business usesGives APIs and events a shared vocabulary. A generic data model that calls every product an "arrangement" hides the real domain.
AggregateA cluster of objects managed as one unit, such as an OrderKeep one aggregate inside one service. Other services ask for changes, and the aggregate may refuse.
Bounded contextAn explicit boundary that exposes some things and hides the restIt is information hiding by design, so it makes a stable service boundary.
Two bounded contexts with a shared ideaFinance and Warehouse each keep private models. They share a customer and a stock count under different names.Finance contextWarehouse contextGeneral ledger (private)CustomerStock countPickers and shelves (private)RecipientStock itemsame personcount onlyAggregate: has state, identity and a life cycle, and can say no to a bad change.Across services, store an explicit reference such as a URI, not a bare ID.
MusicCorp example from Building Microservices.

The same idea can have different names and fields in different contexts. Finance keeps payments for a customer, while Warehouse keeps shipped goods for a recipient. A shared model can also expose less than the internal one, such as a stock count instead of shelf locations. A context can even be split further inside, as long as the outside world still sees one service.

Event storming

Event storming is a workshop for finding aggregates and bounded contexts, as described in Building Microservices. People who understand the business write what happens, then work toward the structure.

Event storming in four stepsEvents first, then commands, then aggregates, then bounded contexts.1 Eventsfacts: Order Placed2 Commandsuser actions3 Aggregatesthe nouns: Order4 Contextsfollow the org chartGet all stakeholders in one room with lots of wall space and sticky notes.Do not let today's code bend the model. That comes later.Events cluster around the aggregate they belong to.

Orange notes are domain events (Order Placed, Payment Received). Blue notes are the commands that cause them. Yellow notes are the aggregates, found by looking at the nouns in the events. Finally the aggregates are grouped into bounded contexts, which often follow the company's structure. The result can feed an event-driven design or a plain request and response one.

Find the events first: event storming

Before designing schemas, find the events. Event storming is a workshop where developers and domain experts stick orange notes on a wall, one per business event in the past tense. From Practical Event-Driven Microservices Architecture, ch. 8.

From sticky notes to service boundariesEvents become commands, actors and aggregates, which then suggest boundaries.EventsOrderCreatedCommandsCreateOrderAggregatesOrder, StockBoundariescandidate servicesRun it with the people who know the business. Pain points and open questions get their own notes.

The wall also shows where event schemas will need to be shared. Treat the output as a draft of your domain model, not as a finished design. Boundaries and aggregates are covered at the end of this lesson.

Other ways to find a boundary

The domain is a good starting point, but Building Microservices warns against one dogma. The goal you pick decides which driver matters.

Other drivers for a service boundaryVolatility, data, technology and organization, with a PCI zone example.Volatilitysplit what changes often: fast time to marketDatalimit the scope of personal or card dataTechnologya different runtime forces a boundaryOrganizationone owning team per serviceExample: PaymentCo splits by dataGateway routes by dataGreen zone servicesnever see card numbers: no PCI auditRed zone serviceshandle card data: fully audited
PaymentCo example from Building Microservices.

Mixing is fine. A domain-shaped Warehouse might still be split further because one part must be written in a different language.

What gets in the way

Splitting is not free. These obstacles come back in later chapters.

Obstacles to decomposition and their fixesObstacleFixNetwork latencyBatch APIs, or merge servicesSync calls hurt availabilityAsync messaging (ch 3)Data consistency across servicesSagas (ch 4)No single consistent viewKeep that data in one serviceGod classesDDD: one model per service

Boundaries, aggregates and when to add a service

Code that changes together should stay together. Domain boundaries are usually the most stable choice, but the book names three more lenses you can combine with it. From Practical Event-Driven Microservices Architecture, ch. 3.

Ways to draw a boundaryDomains first, combined with team structure, likelihood of change and data type.Domain / bounded contextdefault and most stableTeam structureConway's lawLikelihood of changevolatile vs stableType of dataPII, PCI, PHIData-type boundaries keep GDPR and audit work inside the few services that touch sensitive data.

Domain-driven design supplies the vocabulary. A bounded context is the scope of one domain model, with its own ubiquitous language. It contains one or more aggregates, each modelling a domain concept. An entity is one instance. Commands change one aggregate and may be rejected; the change is announced as an event. An anti-corruption layer keeps one context's concepts from leaking into another.

From bounded context to entityA bounded context holds aggregates; an aggregate has entities; commands go in and events come out.Bounded contextOrder managementAggregateOrderEntityorder 231Commands go in to the aggregate and may be rejected. Events come out as facts.One aggregate is changed by one thread at a time; different aggregates change in parallel.

Aggregate size is a performance decision. The book's example: beauty products have variants (sizes or colors) and categories.

Aggregate granularity trade-offA small aggregate gives parallelism and many events; a large one gives consistent bulk changes and slower sequential processing.Variantparallel updatesmany small eventsProductbalanced middleif consumers agreeCategorybulk change atomicslow, large eventssmall: high concurrency, more broker loadlarge: consistent, sequentialAsk: is it a real domain concept? How many events? Does it scale to millions of aggregates?

Match the aggregates of your consumers where you can. If the stock service emits per-variant events but the discount service works per category, the consumer has to do a lot of work to relate them.

Finally, when you add functionality, either extend a service or create a new one:

Add to the existing service whenCreate a new service when
The domain concept is closely related and changes together (order and order lines).The service already depends on many others and carries too much responsibility.
The new feature would otherwise share another service's database (a red flag; consume its event stream instead).Several teams own the existing service with no clear ownership.
The service is anemic: a thin shell around a database with no domain logic.It needs a different technology stack.
It needs to scale independently or has a different usage pattern.
Request-driven and event-driven services in one systemUse synchronous services for strong consistency at low scale; use events for sharing large data.UIa few hundred calls/dayRequest-drivensync API, strong consistencyEvent-drivenstream shares the dataDo not build a mesh of sync services: that is a distributed monolith with cascading failures.

Recap

Quick check

Why does the Kitchen service call its order a Ticket instead of an Order?
Show answer

Each service has its own model. The kitchen only cares about items, prepare-by time and status, so a separate, small Ticket class fits better than the huge shared Order.