read.mir0n.pro

Decomposition is not microservices

Two decisions got welded together some time around 2015, and most architecture arguments since have been about the weld rather than about either decision.

The first decision is decomposition. Where the seams in your system are. What talks to what, through which contract, and what each part is allowed to know about the others. It is a design question, it is answered when the system is designed, and it is expensive to change afterwards.

The second decision is packaging. How many programs those parts are shipped as. On how many machines, behind how many network hops, with how many deployment pipelines. It is an operational question, it is answered when you deploy, and it should be cheap to change.

Microservices bind the second to the first. One module, one process, one repository, one pipeline — and then, quietly, the binding starts being treated as though it were the modularity. You hear it in the way people talk: "we broke the monolith into services" is said as if breaking it into processes is what created the boundaries. It is not. The boundaries are either in the design or they are not, and a network hop does not put them there.

Which is why "monolith or microservices" is the wrong question. The right one is:

Is there a seam?

If there is, the number of processes is a knob you turn per environment. If there is not, no number of processes will save you — the field already has a name for that outcome, and it is not a compliment.

I have been able to test this more literally than most, because the system I build runs as eight processes, or five, or four — from the same code, with the same settings, chosen at deployment. Not a migration. Not a branch. A composition choice made in a deployment file.

Here is what turning that knob actually cost.


What the compositions are

Three shapes, all shipping the same modules:

classic compact super-compact
processes854
the entity, identity and sign-in servicesthree programsone programone program
the gateway and the tree cachetwo programsone programone program
the audit writerits own programits own programnone — database triggers
messaging buses defined772
broker destinations421

Nothing was rewritten to get from the first column to the third. The services were already talking through a defined seam — a messaging bus with typed events, and interfaces rather than reach-in calls — so composing them is a matter of which beans start in which process, and which transport a message takes when both ends happen to be in the same JVM.

That last point is the whole mechanism. A seam does not stop being a seam when both sides are in one process. It just stops costing a network hop.


What it cost: 0.3%

The measurement rig rebuilds from nothing per cell — stack down, volumes dropped, database re-seeded, identity realm re-imported — then drives a 200-user load through the gateway until throughput stops climbing. Same box, same load profile, observability off:

plateau throughput
classic, eight processes1,228 requests/sec
compact, four processes1,232 requests/sec

A difference of 0.3%, which is inside the noise of the rig. Half the programs, the same work done.

Two honest notes about that number, because it is the one everything else leans on. The two figures come from two measurement runs five weeks apart on the same machine and the same protocol, not from one alternating A/B. And it is one box under one load profile — it says what composition costs there, not what it costs at a scale neither shape was measured at.

What the split was costing, meanwhile, is easy to count: twelve service pods against six, at the same replica count, on the same cloud cluster.

The second replica goes further, not less far

The reflex objection is that fewer, larger processes must scale worse. On this rig they scaled better:

one replica two replicas gain
classic1,2281,883+53.3%
compact1,2322,054+66.7%

Fewer, larger processes leave more headroom per pod before the machine saturates, so the second copy has more of the machine left to use. Eight processes on one box spend a share of it on being eight processes.

And some costs disappear rather than move

The one that surprised me. With logging on, the compact shape does not merely relocate the logging cost — it removes part of it. Four processes log a request once where eight logged it at every hop. A request that crossed three services left three sets of entry and exit lines; merged, it leaves one.

Tracing and metrics do not shrink the same way — they are per request and per span, and the request count did not change. But the log volume is a function of how many programs the request walked through, which is a packaging property, not a design one.

For context on why that matters at all: on the cloud cluster, logging is 88% of the observability bill — and 79% in the classic shape measured earlier. The pillar that dominates the cost is the one the composition shrinks.


Where this does not generalise, and I would rather say it than have it found

My services fold because they were never separate contexts. They speak one language about one data model and share a kernel of common code. They are a functional partition of one domain — which is exactly why they can be merged, and exactly why a team whose services are genuinely different domains, with different vocabularies and different owners, cannot do what this article describes. For them the process boundary is carrying an organisational boundary as well, and that is a real job the network hop is doing.

So this is not "microservices were a mistake". It is narrower: if your services are one system split by function, the split into processes is a deployment decision and should be treated as one. A great many systems that call themselves microservices are exactly that, which is why the migration stories keep being written.

What you give up when you fold, stated plainly:

Those are real, and for some systems they are decisive. The argument is not that they never matter. It is that they are deployment trade-offs, and you should get to make them per environment rather than have them fixed in the architecture — which is precisely what happens when the two decisions are one decision.


What it looks like when they are separated

The four-process shape runs the small cloud demo on a single modest instance, because that is what fits there. The eight-process shape runs where the traffic pays for it. The code is identical. Nobody migrated anything, and no branch exists for "the small version".

That is the actual claim, and it is smaller and more useful than "monoliths are back":

Decompose because your design needs seams. Choose the process count because your deployment needs a shape. They are two decisions, and they should stay two.

Design your seams as carefully as microservice advocates say you should. Then package them as loosely as a monolith advocate would. The reason those two positions have been fighting for a decade is that everybody kept assuming you could only have one.


The system is Esquire, a framework for backoffice systems — Java and Spring Boot, an Angular front end, Postgres or Oracle, identity synchronised to Keycloak. The measurements above come from its own load harness, and the matrix documents with every cell in them are in the repository: github.com/mir0n-pro. A deployment is running at esquire.mir0n.pro — sign in with mainadmin / q, it is a demonstration tree and it is seeded fresh with each release.