The question rarely arrives as a question. It arrives as a project: we want to break the monolith into microservices. And it arrives with a number nobody ever justifies, usually between eight and twenty.
This article answers it differently from most texts, and the answer is uncomfortable for both camps. The target state that holds up in grown systems is neither the monolith nor a landscape of twenty services. It is a monolith with internal boundaries and two or three real services alongside it, in the places where a service can do something a module cannot.
Why it is nearly always wanted
The wish rarely comes from architecture and almost always from four observations, all of them true.
A release takes hours and everybody waits. One team blocks the other because both touch the same file. A fault in one place takes the whole application down. And one part of the system needs ten times the compute of the rest but gets the same.
What stands out: three of the four are problems of delivery and organization, not of decomposition. A slow release does not get faster through splitting, it gets multiplied. Two teams blocking each other block each other through a shared database just as well. Only the fourth observation, the part with a different load profile, is a real argument for a service of its own.
So what comes first is not the cut but an honest attribution: which of the four do we have, and does a service actually solve it?
What a service costs before it delivers anything
A service is not a module with a network in between. It carries a fixed base price, and that price applies regardless of how well it is cut.
It needs a deployment path of its own, monitoring of its own with alerts of its own, a place for its secrets, error handling for the case where the caller cannot reach it, and an answer to how a request gets traced across its boundary. On top of that comes the part nobody plans for: a business change touching two services needs coordination, and coordination is the one effort in a project that does not parallelize.
So twelve services do not mean twelve times a module, they mean twelve deployment paths, twelve alert chains and a test environment that only works in full. The price is bearable when it spreads across many teams. With one team of six, nobody carries it.
What a module already does
The skipped intermediate step is a boundary inside the application. It costs almost nothing and solves two of the four observations above.
A module with an interface of its own, tables of its own and a rule that nobody reaches across is possible inside the same application. What is missing is only enforcement, and that is a check in the build, not an architecture:
// A test, not a comment. The boundary exists only when
// something checks it; an agreement in the team holds a year.
public function testOrdersDoNotReachIntoStock(): void
{
$violations = [];
foreach ($this->filesIn('src/Order') as $file) {
foreach ($this->importsIn($file) as $import) {
// Allowed is exactly the neighbour's public
// interface, not its models and tables.
if (str_starts_with($import, 'App\\Stock\\')
&& $import !== 'App\\Stock\\StockApi') {
$violations[] = "$file -> $import";
}
}
}
self::assertSame([], $violations, implode("\n", $violations));
}The gain is larger than it looks. Two teams work in separate directories without a network boundary in between. And if it does become a service later, the work is already done: a module with a clean boundary can be pulled out, an entangled one cannot.
That is the real reason not to skip this step. It is either the destination or the groundwork, and in both cases it is the same effort.
When a service really is one
Which leaves the cases where a service of its own delivers more than a module. There are four, and what they share is that a module cannot express them.
A different load profile. One part needs ten times the instances of the rest, or needs them at different times. Scaling separately only works separately.
A different language. If a part runs an order of magnitude better in Go than in PHP, that is a reason. If it merely looks nicer in Go, it is not.
A different release rhythm. A part that has to ship several times a day while the rest ships monthly, or the reverse, a part that may rarely be changed for regulatory reasons.
A different availability requirement. The part that has to answer even when the rest is down.
What is not on that list: team size, cleanliness, modernity and the number of lines. If you cannot name one of the four reasons, build a module.
The number in the project brief
Which leaves the number from the first paragraph. It appears in almost every project like this, and it is never derived.
Here is how to derive it: count the reasons, not the areas. Three parts with a different load profile and one with a different release rhythm make four services. Everything else stays in the core. If that produces a number below three, it is not a lack of ambition, it is the usual outcome.
A second quantity caps the first, and it regularly gets overlooked: the number of teams is an upper bound, not a target. A service nobody feels responsible for does not get updated, does not get watched, and gets forgotten at the next schema change. Two teams cannot operate twelve services; they can build twelve services and then carry twelve services around with them.
So anyone who has to name a number at this point names not the target architecture but the next step: one service, with one of the four reasons, and then the same question again.
How the incremental rebuild runs technically is covered in The strangler fig pattern in practice.
The route once the decision is made
With one of the four reasons in place, the rebuild follows an order that cannot be rearranged.
First the area gets a boundary inside the application, and an enforced one. Then it gets its own data, which is the actual effort and counts in weeks, not days. Then it gets built a second time in the same place, behind a routing layer, with a comparison run against the old one. And only then does the traffic switch.
Reverse that order and start with the new service, and you build it against a model you do not know yet, and find out halfway through that the data does not come along.
What comes out of cuts in the wrong place is covered in The distributed monolith.
The target state nobody shows off
In the end, the systems I have seen all arrive at the same picture: one application with five to ten cleanly separated modules, two or three services alongside with one of the four reasons, and a shared release for the core.
That is not a compromise out of exhaustion. It is the state with the best ratio of agility to operational cost, and it has an advantage a landscape of twenty services does not have: you can leave it in either direction. A module becomes a service when a reason appears. A service becomes a module again when the reason goes away.
The term for it is in the glossary, and the question of when a service gets built alongside an existing system is a service of its own.
This article continues a series about systems that already exist. The retrospective on the first three months orders its articles by situation.

