Skip to content
intermediate Phase 5 · Microservices Criteria and Service Boundaries

Service Boundary Decomposition

Carve services along business capabilities rather than technical layers.

1h
0 problems
Topic Progress 0%

Not 'The Validation Service': Cutting by Business Capability Instead of Technical Layer

A team draws its first microservice diagram and produces: validation-service, notification-service, integration-service, data-service, gateway-service. Six months later, placing an order requires synchronous hops through validation, then data, then integration — every business operation pays a tax to reach a layer that used to be a method call. The split looked technical because the code was organized technically; the network had merely inherited the layer chart.

Decomposing by business capability keeps each service aligned with a product outcome. A business capability is something the company does that a product manager can name, budget, and measure: take payments, price a cart, allocate inventory, dispatch a shipment, calculate tax. Each one has its own data, its own rules, its own change cadence — and, crucially, an owner who answers "did this work?" with a metric (payment authorization rate, not "validation uptime").

The contrast, side by side:

Technical split (avoid) Capability split (prefer) Why the technical cut hurts
Validation service Order placement owns its own rules Every feature now needs a deploy of two services to change one rule
Notification service Retention owns email triggers; billing owns dunning emails One service becomes a bottleneck queue for every team's messages
Data service / shared DB API Each capability owns its tables and exposes queries The data team becomes a ticket-taking middleware
Integration service The consuming capability implements its own adapter (ACL) Nobody knows which business rule lives where

The failure is subtle because each technical service is individually reasonable. Validation is a concern! But it is not a reason to change: when a discount rule changes, the commit belongs to pricing and no one else; when a tax rule changes, tax law changed. Grouping by concern puts code that changes for different reasons into one deployable, which is the anemic-layer problem of Layered Architecture reappearing with a network hop in front of it — technical splits recreate layer coupling across the network, where every previously local call is now a latency budget, a timeout policy, and a joint deploy risk.

A practical way to find the real capabilities: list the use cases on your system's critical path as verb-noun pairs (place order, authorize payment, reserve stock, ship parcel, issue refund), then group each use case by which rules and data it must read and write in one transaction. The groups are your candidate services. authorize payment touches payment methods, risk scores, and the ledger; it never needs the shipping label printer. The verb-noun framing has a second benefit: it keeps you from creating a service with no use case — if you cannot name three use cases the service serves, it is not a service yet, it is a library waiting to happen.

Interviewers ask "how would you decompose this system?" constantly. The answer that scores: start from business capabilities and their metrics, reject technical splits explicitly with the reason above, and tie each candidate boundary to the subdomain classification from the next chapter — the shape of the cut matters as much as the fact of cutting.

Where the Data Sits: Co-change, Transactions, and the Joins You Will Regret

The surest predictor of a bad service cut is not the org chart — it is the data. Two tables updated in the same transaction, by the same use case, for the same invariant, are one thing wearing two names. Split them across services and every query that joined them becomes a choreography: fetch here, correlate there, reconcile forever.

Boundaries drawn around data that never changes together rarely need cross-service joins. Run the analysis before drawing anything:

  1. Transaction co-occurrence. Which tables are written in the same DB transaction? orders + order_lines commit together because the total must equal the sum — one service. orders and shipments are written by different flows minutes apart — a clean split.
  2. Read patterns. Which queries join across the candidate line? The order-history page joining orders JOIN payments JOIN shipments is a warning: either those live in one service, or the page needs a read model built by events from each owner (see the CQRS topic later in the roadmap).
  3. Change coupling. Does a schema migration to table A regularly require a migration to table B? If yes, one team owns both invariants and will ship them in lockstep regardless of your diagram.

When a legitimate boundary still needs a join, there are exactly three honest answers, in preference order:

1. Redesign        — move the field, or accept duplication of a stable fact
                     (customer's display name copied at order time: fine,
                      customer's balance copied: never)
2. Query at source — the owning service exposes the query (GET /shipments?order=...);
                     the caller correlates. Simple, but adds a runtime dependency.
3. Local projection — subscriber maintains its own denormalized read copy from events.
                     No runtime dependency; pays in eventual consistency.

What you must not do — and what production systems do under schedule pressure — is reach into the other service's database "just for a read." It recreates a shared schema across the network with none of a shared schema's speed: no transactions, no referential integrity, no owner, and a silent coupling that surfaces as an outage when the other team adds a column.

The subdomain classification sharpens granularity decisions. Domain-driven design sorts subdomains into core (the differentiator — for a delivery company, route optimization), supporting (needed but not differentiating — warehouse management), and generic (commodity — email delivery, address validation). Core subdomains deserve fine-grained, independently evolving services because their change rate is high and their team is invested. Generic subdomains deserve the opposite: buy it, wrap it conformist, give it as little of your model as possible. Spending architectural brilliance on a commodity subsystem while the core lives in a shared monolith is inverted investment — a failure mode that shows up in roadmaps far more often than bad diagrams.

Granularity traps round out the checklist. Nanoservices: one service per entity or per class, so a use case fans out to nine hops; rule of thumb — a service should own at least a complete aggregate and a deploy pipeline worth of value. Premature duplication: splitting one capability into orders-v1 and orders-v2 "for safety" while both co-change. Org-shaped cuts: services mirroring job titles rather than capabilities. Validate each candidate cut with three questions: does one team own it? does it own data that changes together? can its failure be tolerated alone? A cut that fails two of three is a module, not a service — and Modular Monolith already told you what to do with modules.

A Two-Day Decomposition Workshop That Produces a Cut You Can Defend

Boundary decisions fail when they happen in a whiteboard afternoon by two architects and get announced as fact. The alternative is a short, evidence-producing workshop whose artifacts — not opinions — carry the decision into review. Here is a sequence that fits two days.

Day 1 morning: map the flow, not the services. With product and engineering in one room, lay out domain events in temporal order on a wall (or a Miro board): OrderPlaced, PaymentAuthorized, StockReserved, LabelPurchased, OrderShipped. Every event gets an actor and the data it carries. The event names come from the business language — this is where Bounded Contexts and the capability list meet, because the noun that appears consistently in an event's payload is the language of one context.

Day 1 afternoon: draw candidate boundaries and test them. Group events into clusters, then run each cluster through three tests:

invariant test   — can all rules for this cluster be stated without asking who asks?
data test        — do these tables get written in one transaction / one use case?
change test      — does git history show these files co-changing? (same command as
                   the cohesion chapter, run on the monolith before it splits)

Clusters passing all three are candidate services. Clusters failing the data test usually mean you split an aggregate; failing the change test means two services will co-change into a distributed monolith — merge them or move the seam.

Day 2: assign the non-functional profile. For each candidate, write down scale factor, latency budget, availability need, and failure tolerance ("if pricing is down, can checkout proceed with last-known prices?"). This step converts a structural argument into an operational one: two candidates with identical profiles are often better merged; wildly different profiles (the 10× CPU document generator from the extraction triggers) are strong evidence for a cut. Then classify each subdomain core/supporting/generic and attach an owner team — an unowned service in the diagram becomes an unowned service in production, which is the single most reliable predictor of an outage nobody can explain.

The artifacts that survive the meeting: a one-page map (clusters, arrows, event names, owner teams), the three test results per boundary (so the reasoning is auditable), and an explicit open questions list ("shipping/fulfillment seam disputed — validate with co-change data next sprint"). Disputed seams get data, not votes.

Common workshop failure modes: letting the existing package structure anchor the clusters (start from events instead — packages encode history, not truth); drawing ten services for a system with four capabilities (map only the critical path first); skipping the failure-tolerance column, which is where cross-service dependencies get exposed as "everything needs everything"; and letting the loudest team claim the shared names (every team wants customer-service — the word is a homonym, send it back through the context exercise).

For interviews, compress this into a repeatable narrative: events → clusters → three tests → non-functional profile → owners. Then do what most candidates cannot: name what would change your mind. "If the change test showed orders and payments co-changing weekly, I would keep them in one service until the history disagreed." A decomposer who can be wrong with specific data is exactly who teams want making cuts that are expensive to reverse — and your final sentence in the interview should be that seam decisions are cheap while they are modular and dear once they are network calls, which sets up the next topic: how those services will actually talk.

Quiz

1. A proposal creates a shared `validation-service` that all other services call before writing data. What is the primary architectural objection?

Question 1 options

2. Two candidate services write `orders` and `order_lines` in the same transaction for every use case, and their schema migrations always ship together. What should you conclude?

Question 2 options

3. The order-history page needs fields owned by orders, payments, and shipments. Which approach is most sound?

Question 3 options

4. In subdomain classification, how should a generic subdomain (e.g., address validation) be treated?

Question 4 options

Flashcards

Question

What is a business capability, and why must service cuts follow capabilities rather than technical layers?

Answer

Something the business does that a PM can name and measure (take payments, dispatch a parcel), with its own data, rules, and change cadence. Technical groupings (validation, integration, data services) are concerns, not reasons to change — so unrelated teams' features now need joint deploys over a network, recreating layer coupling with latency attached.

Question

Name the three boundary tests applied to a candidate cluster.

Answer

Invariant test: can all its rules be stated without asking who is asking? Data test: are its tables written together in one transaction/use case? Change test: does git history show these files co-change? Passing all three = candidate service; failing data = split aggregate; failing change = future distributed monolith, merge or move the seam.

Question

When a legitimate boundary still needs a join, what are the three honest answers in preference order?

Answer

1) Redesign — move the field or duplicate a stable fact copied at write time (never a live balance). 2) Query at the owner's API and correlate — simple, adds runtime dependency. 3) Maintain a local denormalized projection from the owner's events — no runtime dependency, pays eventual consistency. Reaching into the other service's database is not on the list.

Question

Core, supporting, generic subdomains — how should investment differ?

Answer

Core (the differentiator): fine-grained, independently evolving services, best engineers, high change tolerance. Supporting: solid but standard engineering. Generic (commodity — email, address validation): buy or wrap conformist with minimal model exposure. Inverted investment — bespoke brilliance on generic code while core sits in a shared blob — is a common roadmap failure.

Question

Name two granularity traps in service decomposition and their fixes.

Answer

Nanoservices (one service per entity/class → use case fans across nine hops): require each service to own a complete aggregate plus real pipeline value. Org-shaped cuts (services mirroring job titles): re-cut along capabilities. Also: unowned services → assign an owning team; clusters that co-change weekly → merge back.

Revision Notes

Key Takeaways

  • 1. Cut by business capabilities — things a PM can name with their own data and metrics — because technical splits move layer coupling onto the network
  • 2. Predict the cut's quality from data: same-transaction writes, co-changing files, and lockstep migrations all say 'one unit' regardless of the diagram
  • 3. Cross-service joins get three honest answers (redesign, query the owner, project locally) — foreign-table reads recreate a shared schema with none of its guarantees
  • 4. Subdomain classification is investment strategy: fine-grain the core, buy or conformist-wrap the generic
  • 5. A short evidence-producing workshop (events → clusters → tests → profile → owners) yields a defensible map; disputed seams get co-change data, not votes

Interview Tips

  • • 'How would you decompose this system?' — verb-noun use cases → cluster by data and change → three tests → assign owners; explicitly reject layer-shaped services with the joint-deploy reason
  • • 'Where do you put validation?' — inside the capability whose rule it is; validation is a concern, not a reason to change, and a shared validation service is the layer anti-pattern with latency
  • • 'What if a page needs data from three services?' — local projection from events or per-owner queries with bounded staleness; never cross-database reads; mention eventual-consistency tolerance explicitly
  • • 'How do you decide service granularity?' — aggregate + transaction + ownership as the floor, failure tolerance and scale profile as the split test; name nanoservices as the failure mode you watch for

Cheat Sheet

Service Boundary Decomposition

Capability vs technical cut:

avoid: validation / notification / integration / data / gateway services
prefer: take payments · price cart · allocate stock · dispatch parcel
        (verb-noun use cases, each with data + rules + metric + owner)

Technical splits = layer coupling over the network: every rule change needs two deploys.

Data analysis before cutting: same-transaction writes = one service · queries joining across the line = redesign or project · schema migrations shipping together = one team, one unit.

Join across a boundary — in order: redesign (copy stable facts at write time) → owner's API + correlate → local event-built projection (accept staleness). Never query the neighbor's tables.

Subdomains: core = invest + fine-grained · supporting = standard · generic = buy/wrap conformist.

Two-day workshop: events on wall (business language) → cluster → three tests (invariant / data / co-change) → per-candidate non-functional profile (scale, latency, availability, failure tolerance) → owner team + open-questions list. Artifacts: one-page map, test results, disputes deferred to data.

Traps: nanoservices · org-shaped cuts · unowned services · anchoring on existing packages.

Interview narrative: events → clusters → three tests → profile → owners → 'here is what would change my mind.'

Continue Learning