Chapter 03 of 27 · Part 2: Decomposition

Designing business logic

How to organise the logic inside one service: DDD aggregates, domain events and bounded contexts.

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

Two styles of business logic

Simple apps can use a procedure per request. Complex ones need an object model.

Two ways to organize business logicTransaction scriptOne procedure per requestData classes with no behaviorFine for simple logicGets messy when logic growsDomain modelSmall classes with state + behaviorMirrors real-world conceptsEasy to test and extendUse DDD to organize it
DDD tactical patternsDDD building blocksEntityhas an identityValueobjectjust valuesFactorybuilds objectsRepositoryloads and savesAggregateunit of changeAggregates are the building block that matters most for microservices.

Why aggregates exist

Without clear boundaries two people can change different parts of the same order and break a rule that covers the whole order.

The fuzzy boundary problemSamlowers samosastotal still OKMarylowers naantotal still OKBoth commiteach saw old totalOrder is invalidbelow the minimumUpdating parts of an order directly can break a rule that spans the whole order.
An aggregate and its boundaryThe Order aggregate holds line items, delivery and payment. It refers to Consumer and Restaurant only by id.Order aggregateOrderaggregate rootLine itemsvalue objectsDeliveryvalue objectPaymentvalue objectOnly the root is touched from outside.Consumer aggregateConsumerreferenced by consumerIdRestaurant aggregateRestaurantreferenced by restaurantId
A load, update or delete acts on the whole aggregate.

The aggregate rules

Four rules make an aggregate a self-contained unit, and a natural fit for a service.

Aggregate rulesAggregate ruleWhy it helps1. Reference only the rootthe root enforces the rules2. Refer to others by primary keyloose coupling, easy to split3. One transaction, one aggregatefits one service / NoSQL4. Need several? Use a sagaone aggregate per local txn
Aggregate granularitySmall aggregates (preferred)More parallel updatesFewer conflictsEasier to split into servicesLarge aggregatesAtomic updates across more dataUpdates serializeHard to decompose

Where the logic lives

A service stays thin. The aggregate holds the rules and has a life cycle of states.

Business logic of a serviceInbound adapterREST API, command handlersService (OrderService)thin: load, call, saveAggregate (Order)most business logic lives hereRepository + DB adapterloads and saves aggregatesSagas and the event publisher sit beside the service.
Order aggregate life cycleAPPROVALPENDINGcreatedAPPROVEDsaga doneCANCELPENDINGcancellingCANCELLEDfinalA failed create saga moves the order to REJECTED. Revising uses REVISION_PENDING the same way.

Domain events

An aggregate announces a state change with an event named in the past tense, such as OrderCreated.

Why publish domain eventsDomain eventan aggregate changedSagaschaining stepsCQRS viewskeep copies freshNotifyemail, SMSWebhooksother systemsAnalyticsmonitoring
Generating and publishing eventsServiceloads aggregateAggregateupdates statereturns eventsServicesaves aggregatePublishersends eventsThe aggregate creates events. The service, which has the messaging API, publishes them.
Event enrichmentThin eventOrderCreated with just orderIdStable event classConsumers must call backEnriched eventCarries items, address, paymentConsumers need no extra callEvent class changes more often
Event storming workshop1. Brainstorm eventsorange notes on a timeline2. Find triggersuser, system, time, event3. Find aggregateswho handles each command

Speak the domain's language

Ubiquitous language means using the same words in code that the business uses. Newman tells of a bank team that built a corporate-liquidity product on a generic data model where everything was an "arrangement": a loan, a share, a credit card. The product owner talked about haircuts and end-of-day sweeps, and none of those words were in the code, so analysts spent their time translating.

Generic model versus domain languageGeneric modelArrangementArrangement type: loanArrangement type: shareDomain languageCorporate liquidityHaircutEnd-of-day sweepWhen code uses the product owner's words, a developer can read a story and understand it.

The same words then flow into API names, event names and service boundaries.

From Building Microservices (ch. 2, Just Enough Domain-Driven Design).

Bounded contexts and shared models

A bounded context is an explicit boundary around a part of the business. It offers some things to the rest of the system and hides the rest. At MusicCorp, finance and the warehouse are two contexts.

Two bounded contexts and what they shareWarehouseFinancePickerShelvesStock Item (with shelf location)LedgerPayrollStock Count (a number only)sharedinternal details stay hiddenneeds stock to value the firmShare only what the other side needs, and give the shared view its own name.

A concept can also live in both contexts with a different meaning. Finance cares about a customer's payments and refunds. The warehouse cares about the parcels sent to them, and may call the same person a recipient. Both link to one global customer id.

One person, two modelsFinanceCustomerPayments, refundsWarehouseRecipientParcels sentSharedGlobal customer idName, email lookup

From Building Microservices (ch. 2, Bounded Context).

Aggregates, services and references

Aggregates and bounded contexts both make good service boundaries. Start with a service per whole context. Later you can split it, but one aggregate must stay with one service. A service may own several aggregates.

Hiding a split behind one APIWarehouse boundary (what callers see)Warehouse APIone coarse interfaceInventory serviceShipping serviceOwns Stock Itemand ShipmentEach aggregate is owned by one service.

Aggregates also guard themselves. If an outsider asks for a state change, the aggregate can say no, so illegal transitions are impossible. A single aggregate is changed one request at a time, while different aggregates run in parallel. Smaller aggregates therefore give more throughput.

When an aggregate in one service refers to another in a different service, make the link explicit. A bare CustID column hides what it points to. A URI such as a REST address, or a scheme like soundcloud:tracks:123, shows the target and can be followed.

An explicit cross-service referenceFinance ledgerrow: customer refReferencecustomer/2346Customer serviceowns the aggregateStore a reference you can follow, not a number whose meaning lives only in code.

From Building Microservices (ch. 2) and Practical Event-Driven Microservices Architecture (ch. 3).

Inside one service: N-tier or clean

Pick one internal layout and keep it across services so that anyone can change any service. The book compares two. From Practical Event-Driven Microservices Architecture, ch. 3.

N-tier versus clean architectureIn N-tier the domain depends on data access. In clean architecture dependencies point inward to the domain.N-tier: dependencies point downPresentationAPI, event handlersApplication and domainData accessDatabaseClean: dependencies point inPresentation / infrastructureApplicationDomainknows nothing outside

In N-tier the business logic ends up depending on data access, so the database shapes the logic and testing needs a database. In clean architecture (also called hexagonal or onion) the domain sits in the core and frameworks, drivers and the broker client live in the outer layer. Upgrading the messaging library touches only the outside. The author is pragmatic: the real win is that database and framework quirks stay out of the business rules, not that you can swap databases.

Coarse services first, finer ones laterStart with one service per boundary and split it later if needed.Start: one serviceOrder servicedomain, APIread model, ACLLater: same boundary, finer servicesDomain serviceAnti-corruption layerRead modelAPI serviceMany tiny services early on means lockstep releases and compounding distributed-systems problems.

Recap

Quick check

Why should one transaction update only one aggregate?
Show answer

It keeps each transaction inside a single service and matches the limited transactions of NoSQL stores. Changes across aggregates use a saga instead.