Microservices vs monolith: the trade is organisational
Services are not faster, simpler, or more scalable by default. They trade local complexity for distributed complexity, and the question is whether your team structure needs that trade.
On this page
- What we'll cover
- What you are actually buying
- What it costs
- The modular monolith, which is usually the answer
- Where boundaries should go
- Data, which is the hard part
- The distributed monolith
- Extracting a service when you need to
- Testing across a boundary
- Connecting services with CodeWords
- What to get right first
- Frequently asked questions
- Related reading
The choice between a monolith and microservices is usually presented as a technical one about scalability. It is mostly not. A single well-structured application scales further than almost anybody needs, and the reason organisations split into services is that independent teams want to deploy independently.
That reframes the question usefully. The cost of services is distributed complexity — network calls that fail, data spread across stores, debugging across boundaries — and the benefit is organisational autonomy. If you do not need the autonomy, you are paying the cost for nothing.
What we'll cover
- What you are actually buying
- What it costs
- The modular monolith, which is usually the answer
- Where boundaries should go
- Data, which is the hard part
- The distributed monolith
- Extracting a service when you need to
- Testing across a boundary
- Connecting services with CodeWords
- What to get right first
- Frequently asked questions
What you are actually buying
Four things, and it is worth checking which you need.
Independent deployment. A team ships without coordinating with other teams, which is the main benefit and the one that genuinely requires separate services.
Independent scaling. One component can be scaled without scaling everything, which matters when a component's resource profile is genuinely different.
Technology independence. Different services can use different languages and stores, which is occasionally valuable and frequently a source of fragmentation.
Fault isolation. One service failing does not necessarily take everything down — which is true only if the callers handle its absence, and often they do not.
Notice that three of the four are organisational or conditional. If one team owns everything and deploys together, you are buying almost nothing.
What it costs
The honest ledger, because these costs are real and paid continuously.
Every call can fail. An in-process function call either works or throws; a network call can time out, partially succeed, or succeed with the response lost — and every caller must handle that.
Transactions no longer span the operation. Consistency across services means sagas, compensation, and eventual consistency, all of which are significantly harder than a database transaction.
Debugging crosses boundaries. A single user action touches five services, and without distributed tracing built in from the start, reconstructing what happened is archaeology.
Local development gets harder. Running the system on a laptop means running several services, or mocking them, and both degrade over time.
Versioning becomes an interface problem. Changing a shared contract requires coordinating deployments, which is precisely the thing services were meant to avoid.
Operational surface multiplies. Twelve services means twelve deployments, twelve sets of alerts, twelve dependency upgrade paths.
The modular monolith, which is usually the answer
One deployable unit with strict internal boundaries — modules that communicate through defined interfaces and do not reach into each other's data.
It gets most of the structural benefit. Clear ownership, enforceable boundaries, and code that can be reasoned about in isolation.
Without the distributed costs. Calls are function calls, transactions are transactions, and debugging is a single stack trace.
It is the right starting point for almost every system, including ones that will eventually need services, because a well-modularised monolith is straightforward to extract from and a tangled one is not.
The discipline is enforcing the boundaries. Without a mechanism — module systems, linting rules, separate packages, architecture tests — modules quietly become coupled and the eventual extraction is as hard as it would have been anyway.
Where boundaries should go
Around business capabilities, not technical layers. A "user service", an "order service" — not a "database service" and an "API layer".
Where the data has a natural owner. If two services both need to write the same records, the boundary is in the wrong place.
Along team lines, since that is the benefit you are buying. A service owned by two teams has the coordination cost of a monolith and the complexity of a service.
Where change happens together. Things that always change at the same time belong in the same unit, and splitting them means every change becomes a coordinated release.
Not by size. "Micro" is not a target, and a service small enough to be trivial is usually small enough that its network boundary costs more than it provides.
Data, which is the hard part
Shared databases defeat the purpose. If two services read and write the same tables, they cannot be deployed independently, because a schema change affects both.
A database per service is the usual guidance and the consequence is that queries spanning services become an application concern.
Joins across services do not exist. What replaces them is duplication, an aggregating service, or a read model built from events — all of which are more work than a join.
Eventual consistency becomes a product decision. How stale can this view be is a question somebody has to answer, and it usually reaches the interface.
Reporting needs its own answer. Analytics across a system split into services means a warehouse fed by each of them, which is an additional system to build and operate.
The distributed monolith
The failure state worth naming, because it is common and it has the worst of both.
Services that must be deployed together because their interfaces change in step.
Shared database tables between supposedly independent services.
Synchronous call chains where one request traverses five services and fails if any is unavailable, so availability multiplies downwards.
Shared libraries containing business logic that must be upgraded everywhere at once.
The symptom is a release train: services that cannot ship independently. At that point you have paid all the distributed costs and received none of the autonomy, and the honest response is either to fix the boundaries or to merge services back together.
Extracting a service when you need to
Extract when you have a reason, not on a schedule. A component with a genuinely different scaling profile, a team that needs to ship independently, or a piece of the system with a different compliance posture.
Extract the boundary first, inside the monolith. Make the module talk through an interface, remove direct data access, and let it run that way for a while — most of the work is here and it is reversible.
Move the data with the service, which is the part that makes it real and the part people defer.
Keep the old path working during the transition, and cut over deliberately rather than in a single release.
Measure whether it helped. Deployment frequency and lead time for the owning team are the numbers to look at, since those are the benefits you were buying.
Testing across a boundary
The practice that has to change when a call becomes a network call, and the one most often left until it hurts.
Unit tests get easier and prove less. A service tested in isolation against mocks passes while being incompatible with the thing it calls.
Contract tests are the answer to that. Both sides test against an agreed contract, so a breaking change is caught by the provider's own suite rather than in an environment.
Mocks drift. A hand-written mock of another service reflects what that service did when somebody wrote the mock, and nothing updates it when the real one changes.
End-to-end tests across many services are slow and flaky in proportion to the number of services involved, which is why they should be few and cover only critical paths.
Test the failure modes deliberately — the timeout, the partial response, the service being down — because those are the cases distribution introduced and they are exactly what nobody simulates.
Keep a runnable local setup, and treat its decay as a real problem. When people cannot run the system, they stop testing before pushing.
Connecting services with CodeWords
Systems split into services accumulate integration work between them and with third parties. CodeWords connects to more than 3,000 integrations, which covers the connective work without adding services to operate.
- Open CodeWords and start a new automation.
- Describe what should happen in plain language to Cody, the automation builder: which systems, what triggers it, and what should result.
- Describe the exceptions: a service that is unavailable, a duplicate event, a response that arrives late.
- Connect each system once and reuse the connections.
You describe the outcome; Cody builds it, connects it, and deploys it. The free plan covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.
What to get right first
Module boundaries inside whatever you have now. They are the precondition for extraction, they deliver most of the structural benefit on their own, and unlike a service split they are cheap to change when you get them wrong — which you will, at least once.
Two habits make the difference. Enforce boundaries mechanically rather than by convention, since a rule that only exists in a document is one that erodes within a quarter. And put the boundary where the data has a single owner, because two components writing the same records can never be deployed independently regardless of how they communicate.
Frequently asked questions
Are microservices more scalable?
Independently scalable, which is different. A well-built single application scales further than most systems need, and the usual reason to split is that teams want to deploy independently rather than that one component is hitting a ceiling.
Should a new system start with microservices?
Almost never. Start with a modular monolith: you get clear boundaries and ownership without distributed complexity, and extracting later from a well-modularised system is straightforward.
What is a distributed monolith?
Services that must be deployed together, usually because they share database tables or their interfaces change in step. It has all the costs of distribution and none of the autonomy, and the symptom is a release train.
Can services share a database?
They can, and then they are not independent — a schema change affects both, so they cannot deploy separately. If two components must write the same records, the boundary is in the wrong place.
How do I replace a join across services?
Duplicate the data, build an aggregating read model, or query a warehouse fed by both. All three are more work than a join, which is one of the real costs of splitting and is worth acknowledging before rather than after.
How small should a service be?
Not a useful question. Size follows from the boundary, and a service small enough to be trivial usually costs more in network and operational overhead than it provides. Draw boundaries around capabilities and ownership.
How do I know the split worked?
Deployment frequency and lead time for the team that owns the new service, since independent deployment is the benefit you were buying. If those did not improve, the boundary is probably in the wrong place.
How do I test a system split into services?
Contract tests between each pair, so a breaking change is caught in the provider's own suite. Keep end-to-end tests few and limited to critical paths, since their flakiness grows with the number of services involved.
Why do mocked tests pass while the system is broken?
Because a hand-written mock reflects what the other service did when the mock was written, and nothing updates it when the real one changes. That drift is precisely what contract tests exist to catch.