The expensive mistake is rarely microservices as such. It is adopting them as the default before the conditions that pay for them exist, and then being unable to go back, because by the time the cost shows up the org chart, the on-call rota and the data ownership have all grown around the service boundaries. This chapter treats microservices as what they are: an organisational scaling tool that charges rent in latency, consistency and operations. The question is never "monolith or microservices". It is whether the rent buys something you need this year.
A service boundary buys four things: independent deployment, independent scaling, fault isolation and technology heterogeneity. Each has a precondition, and in most architecture reviews the precondition is assumed, never measured.
Independent deployment only pays when teams would otherwise contend for the deploy pipeline, and that is a function of team count and change rate, not codebase size. A 2-million-line monolith with four teams and a merge queue has no deploy contention worth the name. Independent scaling only pays when resource profiles differ by an order of magnitude. An idle module in a horizontally scaled monolith costs some resident memory, not CPU, so "we can scale search separately" is worth roughly the RAM of the code you did not need to load, which is rarely a line item anyone would notice. Illustratively: 40 instances each carrying 400 MB of code and caches that only the search module uses is 16 GB of RAM across the fleet, which is less than a single extracted service costs once it has its own redundant pair, load balancer, three environments and a dashboard. The exception is real but narrow: a module whose working set dictates the instance shape (a 30 GB in-memory index forcing every web node onto a memory-optimised type) is a genuine scaling argument, and its first answer is a separate deployment role (2.6), not a separate service. Fault isolation only pays if the call graph lets a caller degrade when a dependency fails; in a synchronous request path it usually turns into a cascade instead (2.2). Heterogeneity is, for most organisations, a liability dressed as a benefit: every extra runtime is another upgrade treadmill, another hiring constraint and another set of production-debugging skills the on-call engineer has to have at 3 a.m.
The variable that most predicts whether the trade pays off is not traffic. It is how confident you are in the domain boundaries. Boundaries are most wrong at the moment you know least about the domain, which is exactly when greenfield teams draw them. Inside one process, moving a boundary is an IDE refactor and one deploy. Across a network it is an API migration, a data migration and a negotiation between two backlogs. So microservices are cheapest to adopt precisely when you understand the domain well enough to no longer need the flexibility you are giving up.
Stable boundaries · few teams
Modular monolith + 1-2 justified services
Boundaries are known but nobody is blocked on anybody. Extract the thing with a genuinely different runtime, scaling or compliance profile, and stop.
Stable boundaries · many teams
Microservices earn their keep
Deploy contention is real and measured, domains have survived a couple of reorgs, and each team can own a slice end to end. The tax in 2.4 buys throughput.
Unstable boundaries · few teams
Plain, well-factored monolith
You will redraw the domain every quarter. Moving a boundary is an IDE refactor and one deploy. Anything else is paying rent on guesses.
Unstable boundaries · many teams
Modular monolith, enforced ownership
The danger quadrant. Services here freeze the wrong boundaries at the moment you are most likely to redraw them. Enforce module ownership in-process; extract only on the criteria in 2.7.
Architect's note
A distributed monolith is a system that is deployed as many services but has to change, deploy and fail as one. You do not diagnose it from the architecture diagram, because the diagram always looks clean. You diagnose it from four measurements. What fraction of releases in the last quarter required a coordinated deploy order across two or more services? How many tables are written by more than one service? What is the synchronous call depth of your most important user request? And is there a shared library of domain models (the common-models jar, the @company/types package) that every service must bump in lockstep? Above roughly 10% coordinated releases, or any shared-write tables, and you have the costs of distribution without its benefits.
The call-depth measurement matters because composition is multiplicative, and teams consistently underestimate by how much. Ten serial dependencies at 99.9% each give you 99.0% before your own code fails once: about seven hours of unavailability a month budgeted away by arithmetic. Tail latency is worse. If each dependency is independently slow 1% of the time (its p99), a request that touches ten of them hits at least one slow response 9.6% of the time. Dean and Barroso's "The Tail at Scale" makes the same point at the extreme: with a 100-way fan-out, 63% of user requests are slower than a single server's p99. Once your fan-out passes about ten, your user-facing p99 is governed by your dependencies' p99.9, a percentile most teams do not even put on a dashboard.
Left: 0.999ⁿ and 0.9995ⁿ, assuming independent failures (correlated failures make it better on paper and worse in practice, because they arrive together). Right: 1 − 0.99ⁿ. Your user-facing p99 is set by your dependencies' p99.9 once fan-out passes ~10, which means you now need SLOs on percentiles nobody measures.
Now add retries. Consider a composite scenario (anonymised from several similar incidents): a checkout path of four service layers, each wrapping its downstream call in three attempts. When the bottom dependency slows, the top of the stack can generate up to 3⁴ = 81 attempts at the bottom for every user request, arriving exactly when the dependency is least able to absorb them. The dependency tips from slow to down, every layer's thread pool fills with in-flight waits, and a partial degradation of one leaf becomes a site-wide outage. Nothing was misconfigured by the standards of any single team; the failure lives in the composition, which no single team owns. The monolith version of the same code had one retry policy, at the edge, because there was only one edge.
The fix is structural, and it has to be imposed across teams because no team can fix it locally. Retry at one layer (usually the one closest to the user or the one that owns idempotency); every other layer fails fast and returns an explicit "overloaded, do not retry" signal upward. Cap what remains with a retry budget rather than a retry count: the Google SRE book's overload chapter suggests allowing retries only while they are under about 10% of a client's requests, and gRPC's retryThrottling policy implements the same idea as a token bucket. A 10% budget at each of four layers bounds worst-case amplification at 1.1⁴ ≈ 1.46× instead of 81×. Add deadline propagation so a leaf stops working on requests whose caller has already given up; without it, a slow leaf spends its scarce capacity serving answers nobody will read, which is how a brown-out becomes an outage. If your platform cannot enforce these three things uniformly, you do not have the operational maturity that a deep synchronous call graph requires, and that is an argument about the architecture, not about the platform.
3 attempts at every layer
retry only at L1, others fail fast
10% retry budget at every layer
L1 · edge / BFF
L2 · checkout
L3 · pricing
L4 · inventory
leaf · stock DB
■ 81× load on the leaf, arriving as it slows
■ 3× worst case; L2-L4 return 'overloaded'
■ 1.1⁴ ≈ 1.46× worst case
Bars on a log scale. The pink column is what four independently reasonable client libraries produce by default. Budgets also need deadline propagation: without it, a leaf that is 3× over capacity spends most of that capacity on requests whose callers have already timed out.
Note what the fault-isolation argument quietly assumed. Isolation only exists where the caller has something useful to do when the dependency is gone: a cached price, a default recommendation, an order accepted into a queue and confirmed later. In most request paths that were split along entity lines (orders calls customers calls pricing calls inventory), no layer has a meaningful fallback, because each one needs the answer to produce its own. There, splitting has not isolated faults. It has multiplied the number of components whose failure is total, and added a network between them. The design-review question is blunt: for each synchronous edge, what does the caller return when this edge is down? If the answer for most edges is "an error", the boundary bought no isolation, and the availability arithmetic above is the whole story.
Distributed monoliths are rarely designed. They accumulate, and the decay has a recognisable shape. The composite below is assembled from several organisations that split early (anonymised; the months are typical rather than measured). None of the events is dramatic on its own. The shared models library forces the first lockstep release within a quarter. The first retry cascade arrives when traffic first stresses a leaf. The first reorg strands services. By the second year the metric that matters, lead time for a change that crosses a boundary, has roughly doubled, and it is attributed to "complexity" or "tech debt" rather than to the service map. The tell is that the proposed fix at month 30 is another architecture, when the evidence points to redrawing the boundaries of the one you have.
0.999¹⁰
= 99.0% before your own bugs
9.6%
requests hit a p99 at fan-out 10
3⁴ = 81×
worst-case retry amplification
63%
slow at fan-out 100 (Tail at Scale)
Pitfall · the shared database
The central misconception is that service boundaries reduce coupling. Coupling is a property of the domain: if pricing depends on inventory, it depends on inventory whether the call is a function or an HTTP request. What the boundary changes is who enforces the coupling. In-process, the compiler checks the types, the transaction protects the invariant, and one PR changes both sides atomically. Across a network, the same coupling is enforced by contract tests, schema registries, compatibility windows, sagas and on-call engineers, all of which are slower, weaker and more expensive than a compiler.
The ledger below is worth running on any proposed split. The two rows that do the most damage are the invariant row and the semantic row. An invariant that spans two services, such as "never ship more units than were paid for", stops being guaranteed and becomes eventually true through a saga with compensations. Sometimes that is fine, but it is a product decision, and it is almost never presented to product as one. Semantic coupling is subtler: the definition of an "active customer" or a "billable seat" gets copied into four services, each copy evolves under a different backlog, and two years later finance and product are arguing over numbers that are both correctly computed from different definitions.
Coupling
In-process: enforced by
Across a network: enforced by
Failure when enforcement lapses
Schema / type
compiler, one PR
contract tests, versioning, expand/contract across N repos
consumer breaks at runtime on deploy day
Invariant across entities
one ACID transaction
saga + compensation; invariant becomes eventually true
oversell, double-refund, orphaned records
Semantic (what 'active' means)
one function, one definition
N copies of the rule, drifting independently
reports disagree; nobody can say which is right
Temporal (both must be up)
same process, same fate
timeouts, retries, breakers, bulkheads, queues
retry storm; cascading brown-out
Deployment order
atomic: one artifact
feature flags, compat windows, release choreography
lockstep deploys: the distributed monolith
Operational / shared infra
one blast radius, visible
hidden: shared DB, cache, mesh, DNS, auth service
'independent' services fail together
The invariant row can be put in numbers, which is usually what gets it taken seriously. Take the classic split: an orders service checks stock by calling inventory, takes payment, then asks inventory to decrement, or publishes an event that inventory consumes. Between the stock check and the committed, visible decrement there is a window W. Every order that passes the check inside that window, for the last unit, will oversell. By Little's law, the number of orders in flight in the window is about λ·W, where λ is the order rate on that SKU. For a steady catalogue item at 5 orders/s with a 50 ms synchronous window, that is a quarter of an oversell per sell-out, which a support team never notices. For a promoted drop at 500/s where the decrement travels through a queue with two seconds of consumer lag, it is on the order of a thousand orders sold against stock that does not exist, each one becoming a refund, an apology and a support ticket, all on the day the marketing team was proudest of. In one process the same invariant is a conditional update inside the payment transaction, and the number is zero.
orders/s on the SKU ↓ · window W →
50 ms
300 ms
2 s (queue lag)
5/s
steady catalogue item
0.25
oversells / sell-out
2
oversells / sell-out
10
oversells / sell-out
50/s
promoted item
3
oversells / sell-out
15
oversells / sell-out
100
oversells / sell-out
500/s
drop / flash sale
25
oversells / sell-out
150
oversells / sell-out
1000
oversells / sell-out
Little's law applied to the race: the orders that passed the stock check but whose decrement is not yet visible. Illustrative rates. In one process the same invariant is a single conditional UPDATE … SET qty = qty − 1 WHERE qty > 0 and the number is zero in every cell.
The fix is not a better saga. It is to put the invariant inside one owner's transaction: inventory takes a reservation with a conditional decrement as the first step, returns a reservation id with an expiry, and orders confirms or releases it. The invariant is local again, and the saga only has to handle the cheap direction (releasing an unused hold). That is a boundary redrawn around the invariant, which is the general lesson: when a split requires compensating an invariant rather than enforcing it, the split is in the wrong place, or the product has not agreed that the invariant can be eventually true. Either way, it is a decision to put in front of product with the λ·W number attached.
The metric that tells you your boundaries are wrong is services touched per feature. Pull the last 50 merged features from your tracker, count the repositories each one changed, and look at the median. If it is three or more, your service boundaries cut across the axis along which your product actually changes. You are paying a coordination cost on every roadmap item, and it shows up as lead time, not as incidents, which is why it goes unnoticed for so long. Chapter 05, Signals That an Architecture Has Run Its Course, turns this into a standing indicator; here it is enough to know it is cheap to compute and usually damning.
Architect's note
Every service carries a fixed recurring cost that is paid whether or not anybody changes it this year: dependency bumps and CVE patches, runtime upgrades, its pipeline and infrastructure code, dashboards and alerts that need tuning, a slice of the on-call rota, and the coordination overhead of versioned contracts with its neighbours. Itemised honestly, it comes to something like 0.1 to 0.25 of an engineer per service per year. The figures below are illustrative, so calibrate them from your own tickets. The shape is what matters: the cost is linear in service count, while the benefit saturates at the number of teams that genuinely need to ship independently.
hours / service / year
Σ ≈ 260 h ≈ 0.15 FTE (at ~1,700 productive h/yr)
The numbers are illustrative; calibrate them from your own on-call and upgrade tickets. The shape is not illustrative: the cost is linear in service count, while the benefit saturates at the number of teams that genuinely need to ship independently.
Run the numbers on a very common composite shape: 40 engineers, 60 services, the product of an early "one service per noun" design. At the low end of the band, six engineers' worth of capacity goes to keeping services alive; at the high end, fifteen, which is more than a third of the organisation. You will not find that in any budget line. It shows up as roadmaps that slip for no identifiable reason and a platform team that is always behind. The tax is also bursty. In December 2021, Log4Shell turned patching into a same-week emergency across the industry, and the unit of work was the deployable: an organisation with 8 deployables patched 8 things, an organisation with 200 ran a fleet-wide campaign, chased owners of services nobody had touched in a year, and found the ones whose pipelines no longer built.
The public reversals are instructive when they are read narrowly. Segment wrote in 2018 about collapsing well over a hundred per-destination services back into one, because the per-service overhead had eaten the team. In 2023 the Prime Video team described moving an audio/video monitoring pipeline from a distributed Step Functions and Lambda design to a single process, with a reported cost reduction of about 90%. Neither is an argument that monoliths win. Segment's services were near-identical and shared nearly everything; Prime Video's cost came from passing large intermediate data across network and storage boundaries at high frequency. The lesson that transfers is specific: when the components share most of their code, or exchange heavy data at a high rate, the boundary is overhead.
The itemised tax is per service, and it is the part people eventually learn to count. The part they do not count scales with edges, and edges grow faster than nodes. Three costs sit on edges. The first is test environments: a change to one service can only be tested end to end against compatible versions of everything it talks to, so teams either maintain a shared staging environment that is permanently half-broken (every team's in-progress version colliding with every other's) or spin up per-branch environments whose cost rises with the dependency closure of each service. The second is telemetry volume. Illustratively: a request that crosses 12 hops emits at least 12 spans plus log lines at each hop; at 2,000 requests/s and ~1 KB per span that is about 24 MB/s, roughly 2 TB/day of trace data before logs. At typical per-GB ingest pricing that becomes a line item, so it gets sampled, usually head-sampled at 1-5%, which throws away most of exactly the rare slow requests you needed the traces for. Tail-based sampling fixes that but is itself another stateful system to run. The third is contract churn: each edge is a versioned API with a compatibility window, and a breaking change on a service with k consumers costs k negotiated migrations, which is why so many estates end up with v1, v2 and v3 of the same endpoint all live.
Where the conventional advice is wrong
~0.1-0.25
FTE / service / yr (illustrative)
linear
tax vs service count
saturating
benefit vs service count
≤ ~2
services per team, healthy
Conway's law is usually quoted in one direction: systems mirror the communication structure of the organisations that build them. The inverse Conway manoeuvre exploits this deliberately, shaping teams to get the architecture you want. Both are true, and the trap is in the time dimension. A service map is drawn once, against the org chart of that quarter. The org chart then changes every 12 to 18 months through reorgs, mergers of teams, new product lines and attrition. The service boundaries, and above all the data ownership behind them, do not move, because every move is a migration.
The drift appears as two populations. Orphans are services whose owning team was dissolved or repurposed; they run, they page someone, and nobody can safely change them. Co-owned services are split between two teams after a reorg, so every change needs two sign-offs and every incident starts with an argument about whose it is. Then the second direction takes over: once there are enough services, the next reorg gets designed around the service map rather than around the product strategy, because reassigning 60 services is too disruptive. At that point the architecture is constraining the business rather than serving it, which is Conway running backwards.
Two ratios catch this early. Services per team: above about two per team of six to eight, cognitive load (in the Team Topologies sense) rises past the point where the team can hold its services in its head, and on-call quality falls first. And teams per service: any value above one on a service that changes weekly is an ownership bug. Check both after every reorg, not every year. A modular monolith does not escape Conway's law, but it makes the correction cheap. Reassigning a module means changing a CODEOWNERS file; reassigning a service means changing a pager rota, a cloud account, a deploy pipeline and a data owner.
Pitfall · designing for the target org
A modular monolith is one deployable whose internal modules have the boundaries you would otherwise have drawn with a network: a public interface, private internals, owned data, and events for anything asynchronous. The difference from a big ball of mud with tidy folders is enforcement. Boundaries that only exist as conventions erode at the first deadline. Boundaries that fail CI survive. Shopify built Packwerk to enforce exactly this in its Rails monolith, and every major ecosystem has an equivalent (the diagram lists some). The tool matters less than the ratchet: record today's violations in a file that may only shrink, and fail the build on any new one.
Three rules separate modular monoliths that stay modular from ones that decay within a year. First, data ownership: each module owns a schema (a Postgres schema per module in the same database is enough), writes only to its own, and never joins across schemas in application code. Cross-module reads go through the module's API or a published read model. This is the rule that makes extraction cheap later, because the hardest part of extracting a service is always untangling the data. Second, events through an outbox, even in-process, with the same payload contract a future service would publish, so that an extracted module can move to a broker without its consumers noticing. Third, deployment roles: run the same artifact as separate web, worker, batch and heavy-job pools with separate autoscaling and separate failure domains. This is the lever most teams miss. It gives you most of the resource isolation and fault isolation of services (a memory leak in the PDF renderer no longer takes down checkout) without adding a single network contract.
one repo · one build · one artifact
catalog/
public: CatalogApi
internal/ · private
schema catalog.*
orders/
public: OrdersApi
internal/ · private
schema orders.*
billing/
public: BillingApi
internal/ · private
schema billing.*
identity/
public: IdentityApi
internal/ · private
schema identity.*
CI gate
Packwerk · ArchUnit / Spring Modulith · JPMS · Go internal/ · Nx boundary rules · import-linter. Violations fail the build; a ratchet file lists grandfathered ones and may only shrink.
Data rule
A module writes only its own schema. No cross-schema joins in application code; cross-module reads go through the public API or a published read model.
Deploy roles
Same artifact runs as web, worker, batch and heavy-job pools with separate scaling and separate failure domains. Isolation without a network contract.
Be honest about where the monolith does hurt, because these are the real extraction triggers. Build and test time grows with the codebase. The fix is test-impact analysis and per-module test selection, which the module graph makes possible; the fix is not a split. Deploy contention grows with the number of teams, and it grows non-linearly, which is why it seems to arrive suddenly. A serialised deploy pipeline is a single-server queue. With a 30-minute build-test-rollout and roughly deterministic service time, mean wait is about ρS / 2(1 − ρ): at 75% utilisation that is 45 minutes, at 90% it is 135 minutes, and adding one more team moves you along the steep part of that curve (illustrative figures; plug in your own). The two cheap levers both move the knee before any split: shrink S (per-module test selection, faster rollout), or batch changes in a merge queue so k merges become one deploy, dividing the arrival rate by k at the cost of bisecting a failed batch. Extract only once those are exhausted.
Mean wait W = ρS / 2(1 − ρ), ρ = arrival rate × S / batch size. Batching is not free (batch-formation delay, bisecting a red batch), which the model ignores. Halving S or batching moves the knee from ~5 teams to 10-20 before anyone draws a service boundary.
The curve also explains why deploy contention feels sudden. Under the pink assumptions, four teams wait 45 minutes on average and five teams wait over three hours: adding one team does not add 25% to the pain, it multiplies it. Organisations that hit this knee during a hiring push tend to conclude, in the same quarter, that the monolith "does not scale", and then spend the next year on a split that a merge queue and a faster test stage would have deferred by 10 or 15 teams. Shared blast radius for bad deploys is real, and is mitigated by progressive rollout per deployment role. What genuinely cannot be fixed inside one artifact: components that need a different runtime or language, a regulated perimeter you want out of audit scope, and a component with a fundamentally different availability requirement from everything around it, for example an authentication path that must survive when the rest of the product is down.
Architect's note
The decision tree below starts with the veto, then orders the remaining questions by how reliably each one justifies the tax. The veto: if the boundary would sit inside a transaction, or add two or more synchronous calls to a hot path, do not extract there, whatever else is true; a real compliance or runtime need means you redraw the boundary until it passes. After that, compliance scope reduction comes first because it is the most underrated reason to extract: taking cardholder data or PHI out of the main application can shrink the audited perimeter from the entire codebase to one small service. That is an annual saving in audit effort, penetration-test scope and change-control friction, and it compounds. Resource profile comes next, but its first answer is a deployment role rather than a service. Deploy contention is third and must be measured (merge-queue wait time, release-train slips, rollbacks caused by other teams' changes), because it is the most commonly asserted and least commonly measured.
Q1
Would the new boundary sit inside a transaction, or add ≥2 synchronous calls to a hot path?
Do not extract here. Redraw the boundary first; otherwise you convert a transaction into a saga and add hops to p99 for nothing.
no ↓
Q2
Does the capability sit inside a regulatory or security perimeter you want to shrink (card data, PHI, signing keys)?
Extract. Scope reduction is a real, audited saving and it compounds every year.
no ↓
Q3
Is its resource profile an order of magnitude different (GPU, 30 GB heap, bursty CPU, long-running jobs)?
Try a separate deploy role of the same artifact first. Extract only if the runtime itself must differ.
no ↓
Q4
Is a team measurably blocked by shared deploys (merge-queue wait, release-train slips, rollback of others' changes)?
Fix the pipeline first (merge queue, trunk, per-module test selection). Still blocked → extract that team's modules.
no ↓
Q5
Must it be written in a different language or scale to zero independently (ML inference, edge, third-party runtime)?
Extract, with an async or coarse-grained interface.
all no → Keep it a module. Tighten its boundary in CI so the option to extract stays cheap.
Context changes the defaults far more than technology does. A pre-product-market-fit company with twelve engineers should not spend a single sprint on service boundaries; its domain model will be wrong by next quarter. A regulated company of the same size should extract the payment or health-data perimeter on day one, because the compliance calculus outweighs everything else. At a few hundred engineers, services are not optional, but the unit that tends to work is a domain: a cluster of services behind one gateway, owned by one group, with the internal count kept small. Uber's 2020 DOMA write-up describes grouping roughly 2,200 critical microservices into about 70 domains, each behind a gateway, for this reason. It is what a large-scale microservice estate converges on, and it looks a lot like a set of modular monoliths with network edges.
Context
Default
Rule of thumb
Pre product-market fit · ≤15 eng
Monolith. No module ceremony beyond folders.
Boundaries will be wrong; speed of redraw is everything.
Scaling · 15-80 eng · 3-10 teams
Modular monolith with CI-enforced boundaries; 1-3 extracted services on tree criteria.
The first time a team is blocked, fix the pipeline before the architecture.
Multi-product · 80-300 eng
Modular monolith(s) per product line; services at product and compliance seams.
Services per team ≤ ~2; every service has one owning team.
Large org · 300+ eng
Services grouped into domains with a gateway per domain; macro-services over nano.
Budget a platform team; the tax is now a product.
Regulated (PCI, HIPAA, SOX)
Extract the regulated perimeter early, whatever the size.
Scope reduction beats every other argument here.
Acqui-merged / multi-stack
Services along the acquisition seams; do not unify runtimes for tidiness.
Accept heterogeneity at the seam, not inside it.
Finally, reversing. Merging services back is a normal architectural move, not an admission of failure, and it has a reliable sequence. Find merge candidates from data rather than opinion: co-change analysis across repositories, services that are always deployed together, and pairs with chatty synchronous traffic. Move the code into one repository and one build first, keeping the network call, so nothing changes at runtime. Replace the network call with an in-process call behind the same interface, which removes a hop, a retry policy and a failure mode, and verify with the latency and error dashboards you already have. Merge the data stores last, schema by schema, because that step is the slowest to undo. Each step can be shipped and rolled back on its own. The organisational step, one team owning the merged module, should happen before the technical one, not after; otherwise you simply rebuild the co-ownership problem from 2.5 inside a single repository.
The merge-back has an economics worth stating, because it is the argument that gets it funded. Using the illustrative tax band from 2.4, collapsing 60 services into, say, 8 deployables (a modular monolith per product area plus the justified extractions) removes 52 deployables and returns somewhere between 5 and 13 FTE of recurring capacity, against a one-off cost dominated by the data merges. If each of the ~50 merges takes two to four engineer-weeks without a data step, and a third of them need a data merge costing another four to eight, the programme is roughly 170 to 340 engineer-weeks, about 4 to 8 engineer-years, repaid in well under two years of recovered tax (illustrative; the data step is where estimates go wrong, so size it first). Sequence the merges by tax recovered per week of effort: chatty, co-deployed pairs sharing a database first, because they recover the most (hops, retry policies, lockstep releases) for the least data work.
≥10%
coordinated releases → distributed monolith
≥3
median services touched per feature → wrong seams
≤ ~2
services per team
1
owning team per service, always
Architect's note · decision criteria
Design review · questions that find the expensive mistakes
Where next