الدرس 1 من 2
Finding a Boundary That Will Hold
Boundaries drawn around nouns produce chatty services that change together. Boundaries drawn around capabilities and owned data produce services that can change alone.
Every failed microservices migration failed at the boundary. The technology is well understood, the tooling is mature, and none of it helps if the cut runs through the middle of a business operation. Getting the boundary right is most of the work and almost none of the discussion.
The same operation across two boundary choices
Cutting around nouns puts the network inside one business operation. Cutting around capabilities keeps the operation inside one service.
On the left, placing an order requires synchronous calls to a customer service, a product service, an order service and an invoice service, each with its own database. On the right, an ordering capability owns orders, lines and pricing snapshots and completes the operation locally, publishing an event that a separate fulfilment capability consumes.
Cut by noun
Customer service
called for every order
Product service
called per line
Order service
orchestrates all three
Invoice service
called synchronously
Cut by capability
Ordering
owns orders, lines, price snapshot
Fulfilment
reacts to OrderPlaced
Billing
reacts to OrderPlaced
The test is more subtle than it looks, because the same-sounding data is often several facts with different owners. A customer's delivery address is owned by the profile capability when it is "where this person lives" and by the ordering capability when it is "where order 4471 was sent" — the second is a historical fact that must never change when the first does. Teams that miss this end up either with orders that silently rewrite their own history, or with a profile service being asked for an address as it was eighteen months ago.
Change coupling: evidence from history
The strongest evidence about a proposed boundary is already in your version control. If two areas of the codebase have changed together in most commits for the last year, separating them across a network will convert every one of those changes into a coordinated release. That measurement takes an afternoon and is worth more than a week of discussion.
# For each commit in the last year, list the top-level module directories it
# touched. Pairs that appear together often are poor candidates for a split.
git log --since="1 year ago" --name-only --pretty=format:"---%H" \
| awk '/^---/ {print ""} /^src\/features\// {split($0,p,"/"); print p[3]}' \
| awk 'NF' \
| sort -u \
> /dev/null
# In practice, use a tool that computes this properly. What you want is a
# matrix: for each pair of modules, the percentage of commits touching one
# that also touched the other. Above roughly 30% is a warning; above 50%
# means these two are one thing and a network between them will hurt.| Change coupling | Interpretation | Action |
|---|---|---|
| Under 10% | The two areas are genuinely independent | A boundary here is likely to hold |
| 10-30% | Some shared concerns, possibly a cross-cutting one | Investigate which changes overlap and why |
| 30-50% | Substantial shared reasons to change | Do not split until you understand and reduce it |
| Over 50% | These are one capability wearing two names | Merge, or move the boundary; splitting will produce coordinated releases |
In practice
A boundary moved before it was built
A team planned to split shipping and billing into separate services. Change-coupling analysis showed they had changed together in 46% of commits over a year, which surprised everyone since the domains felt unrelated.
Constraints
- Twelve months of history across roughly 900 commits
- Both areas owned by different teams already
- The split was funded and scheduled for the next quarter
- Neither team could explain the overlap
Decision
Investigate before splitting. The overlap was almost entirely surcharge rules — fuel, remote-area and oversized-item charges — which shipping calculated and billing re-implemented for invoicing. Extract surcharges into its own capability first, then re-measure: coupling fell to 11% and the split proceeded.
Why
The 46% was a symptom of a concept with no home rather than of a wrong boundary between shipping and billing. Splitting first would have produced two services that both needed changing whenever a surcharge rule changed — several times a year, mandated by carriers — and the coordination would have been blamed on microservices rather than on the missing concept.
What it cost
Extracting surcharges added a third service and a synchronous call on the shipping quote path, which added about 20 milliseconds to quote latency. The team accepted that, and it is worth noting they got this right only because they measured: both teams' intuition said the domains were unrelated, and both were wrong.
Proposing the first service to extract
As a developer
Chooses the module with the cleanest interface and the fewest dependencies, since that is the easiest to extract and gives the migration an early success.
As an architect
Agrees that ease matters and adds two constraints. The first extraction should also be genuinely valuable, so the organisation learns whether services deliver anything here, and it should be reversible — a component that can be merged back in a week if the operational cost turns out to be higher than expected. An easy but pointless extraction teaches the organisation that services are cheap and useless, which is the worst lesson available.
Your proposed `Notification Service` will be called synchronously by six other services whenever anything notable happens. Is this a good boundary?RevealHide
The capability is right and the interaction style is wrong. Notification is a genuine capability with its own data — templates, preferences, delivery history — and one clear owner, so it is a reasonable service. Being called synchronously by six others makes it a hard dependency of all six: when it is slow, six services are slow, and when it is down, six operations fail for something nobody was waiting on. Notifications are the archetypal asynchronous case, because no caller needs the result to proceed. Publish an event and let the notification service consume it, and the same boundary becomes an asset instead of a shared failure point. The general lesson is that a boundary is defined by both where the line is and how traffic crosses it.
Key takeaways
- Cut around capabilities, not nouns; entity services produce chatty systems and operations that cannot be atomic.
- Every fact must have exactly one owning capability, and similar-sounding data is often several facts with different owners.
- Change coupling from version history is cheap, objective evidence about a proposed boundary.
- High coupling sometimes reveals a missing concept rather than a wrong boundary — look for the thing with no home.
- A boundary is defined by where the line is and by how traffic crosses it; synchronous calls make dependencies hard.