System Design

You Extracted Microservices Before You Understood the Boundaries

Table of Contents

Key takeaway: A network boundary makes a module boundary expensive to change. Find the boundaries inside a monolith first, where moving them is a refactor rather than a migration, and split only where independence is genuinely needed.


The Distributed Monolith

An organisation splits its application into fifteen services. Eighteen months later:

Deploying a feature requires coordinating four services. Local development needs eight services running. A single user request traverses six services, so debugging requires correlating logs across all of them. Every service reads from the same database, so no schema change is safe. Latency increased because in-process calls became network calls. And an outage in one service takes down features across the product.

This is a distributed monolith — the coupling of a monolith with the operational complexity of distributed systems. It is strictly worse than either alternative.

The cause is not that microservices are wrong. It is that the boundaries were drawn incorrectly, and a network boundary makes an incorrect boundary very expensive to correct.

Inside a monolith, moving a boundary is a refactor: the compiler finds the callers, tests verify behaviour, and one deployment ships it. Across services, moving a boundary means changing two codebases, migrating data, coordinating deployments, maintaining compatibility during the transition, and doing all of it without downtime.

That asymmetry is the entire argument for finding boundaries before distributing them.


Boundaries Are the Only Thing That Matters

A correct service boundary has properties that are worth checking explicitly, because a boundary lacking them will generate coordination overhead indefinitely.

It owns its data exclusively. No other service reads or writes its tables. Shared databases are the single strongest indicator of a boundary that does not exist — if two services must coordinate schema changes, they are one service with extra network hops.

It can be deployed independently. If shipping a change requires deploying another service simultaneously, the boundary is not real.

It can fail without taking others down. Callers degrade rather than fail. If a dependency being unavailable means the caller is unavailable, you have added a failure mode without adding independence.

It maps to a business capability. Boundaries follow domain concepts, not technical layers. A service per layer — one for the API, one for business logic, one for data — guarantees that every feature touches all of them.

It has a stable interface. If the contract changes with every feature, the boundary is in the wrong place.

One team can own it. Boundaries that span teams produce coordination overhead permanently.

The data ownership criterion is the one to check first. It is objective, it is easy to verify, and shared database access is the most common reason microservice architectures fail to deliver independence.


What Splitting Actually Costs

The costs are mostly operational and mostly permanent.

Network calls replace function calls. Latency, serialisation, and a new failure mode where previously there was none. A function call cannot time out.

Distributed debugging. A request spanning six services requires correlated tracing to follow. Without it, debugging means reading six log streams and inferring the sequence.

Local development complexity. Running the system locally requires running its parts, or building sophisticated stubbing infrastructure. Both are ongoing costs paid by every engineer daily.

Data consistency becomes manual. Covered in the next section, and this is the largest hidden cost.

Deployment coordination. Interface changes require compatible deployment ordering, which reintroduces coordination the split was meant to remove.

Duplicated infrastructure. Each service needs its own deployment pipeline, monitoring, alerting, secrets, and on-call coverage.

Testing complexity. Integration testing across services requires either running everything or maintaining contract tests, and contract tests need discipline to stay accurate.

The operational costs scale with service count, which means they are borne by an organisation whose size may not have grown proportionally. Fifteen services requires meaningfully more platform investment than three, and that investment is frequently not budgeted when the split is proposed.


Transactions Do Not Survive the Split

The most underestimated consequence, because in a monolith this is free.

Inside one database, a transaction spanning several operations either fully succeeds or fully rolls back. Order created, inventory decremented, payment recorded, all atomic.

Across services, each operation is a separate call to a separate database. There is no shared transaction. If the third step fails, the first two have already committed.

The available approaches all involve accepting complexity that did not previously exist:

Saga pattern. A sequence of local transactions, each with a compensating action to undo it. If a later step fails, the compensations run in reverse. This works and it means writing and testing a compensation for every step, and compensations are not always possible — an email cannot be unsent.

Eventual consistency. Accept that the system is temporarily inconsistent and converges. Frequently acceptable for the business and it must be a deliberate decision with the interface designed for it, because users will observe intermediate states.

Transactional outbox. Write the event to the same database as the state change, in one transaction, and publish from there. This solves the specific problem of state and event diverging, and it is a genuinely necessary pattern rather than optional.

Restructure to avoid the need. Frequently the best answer. If two operations must be atomic, that is evidence they belong in one service.

That last option deserves emphasis. Discovering that two operations require a distributed transaction is information about the boundary rather than a problem to engineer around. The presence of a required saga is frequently a signal that the split was drawn in the wrong place.


Signals That a Split Is Justified

The legitimate reasons, which are more specific than general architectural preference.

Independent scaling with genuinely different profiles. One component needs twenty times the capacity of the rest, or needs specialised hardware. Extracting it means scaling it separately rather than scaling everything.

Different availability requirements. A component that must remain available when the rest can degrade, or vice versa.

Team autonomy at scale. Past a certain organisational size, teams sharing a codebase and deployment pipeline coordinate constantly. Service boundaries aligned with team boundaries reduce that. This becomes compelling somewhere past several dozen engineers, not at ten.

Genuinely different technology needs. A component requiring a different language or runtime for real reasons — specific libraries, performance characteristics — rather than preference.

Regulatory isolation. Data that must be processed in a specific jurisdiction or under specific controls.

Independent deployment cadence. A component that must ship several times daily alongside one that ships monthly.

Blast radius containment. Isolating a component whose failure must not affect the rest.

Notably absent: architectural fashion, a belief that microservices are inherently more scalable, and the assumption that they will make development faster. That last one is usually wrong in the short and medium term — the operational overhead slows delivery until team size makes the coordination savings dominate.


The Modular Monolith Alternative

Most of the organisational benefits of services are achievable with modules inside one deployable unit.

The structure: clear module boundaries with explicit public interfaces, each module owning its data — separate schemas or at minimum enforced table ownership — communication through interfaces rather than direct table access, no circular dependencies between modules, and separate test suites per module.

What this provides: boundaries that are discoverable and enforceable, independent development within modules, clear ownership, and the ability to reason about a module in isolation.

What it retains from the monolith: real transactions, function calls rather than network calls, one deployment, one repository, straightforward local development, and one thing to monitor.

And critically: boundaries that can be moved. If a module boundary turns out to be wrong, correcting it is a refactor. That optionality is enormously valuable while you are still learning the domain, which is most of the time.

Enforcement is what distinguishes a modular monolith from a monolith with folders. Static analysis that fails the build when a module reaches into another’s internals, or language-level module systems where the compiler enforces it, is the mechanism. Without enforcement, boundaries erode — and the erosion is invisible until you attempt to extract something.

The pragmatic path for most organisations: build a modular monolith, enforce the boundaries mechanically, and extract a service when a specific, articulable reason appears.


Extracting a Service Safely

When extraction is justified, the sequence that minimises risk:

Establish the module boundary first. Inside the monolith, with enforced separation and its own data. If this is difficult, extraction will be considerably harder.

Define the interface explicitly. The contract the service will expose, exercised through the module boundary before any network is involved.

Separate the data. Give the module its own schema, remove cross-schema queries, and replace them with interface calls. This is usually the largest piece of work and it can be completed before extraction.

Introduce the network boundary behind the existing interface. Callers continue calling the same interface; its implementation becomes a remote call. This isolates the change to one layer.

Run both paths in parallel. Route a fraction of traffic to the new service while the in-process path handles the rest, comparing results. This validates the extraction under real conditions.

Shift traffic gradually with a way back. Increase the proportion, retaining the ability to revert.

Remove the in-process path last. Only after the service has operated reliably at full traffic.

The data separation step is where extractions fail. A service extracted while still reading another service’s tables has moved code without moving the boundary, and the resulting coupling is worse than before because it is now a coupling across a network.


What Must Exist Before the First Split

Prerequisites that are frequently discovered too late:

Distributed tracing. Without it, debugging a multi-service request is guesswork. This is not optional infrastructure.

Centralised structured logging with correlation identifiers. Reading logs per service and correlating manually does not scale past a few services.

Automated deployment per service. Manual deployment of fifteen services is not viable.

Service discovery and configuration management. Hardcoded endpoints become unmanageable quickly.

Contract testing. Some mechanism preventing a service from breaking its consumers.

A local development story. How an engineer runs enough of the system to work. Answering this after the split has already happened produces daily friction.

Clear ownership. Every service has an accountable team, on-call coverage, and documentation.

An organisation lacking most of these is not ready to split, and splitting first means building this infrastructure under pressure while operating a system it was needed for. The infrastructure investment is the real precondition, and it is what makes the difference between microservices that work and a distributed monolith.


Common Pitfalls

Splitting before understanding the domain. Boundaries become expensive to move exactly when you most need to move them.

A shared database across services. The clearest sign the boundary does not exist.

Services per technical layer. Guarantees every feature touches every service.

Distributed transactions as a routine pattern. Their necessity indicates a boundary drawn wrongly.

No tracing. Makes multi-service debugging impractical.

Splitting for scalability without a scaling problem. Adds cost with no benefit.

Boundaries that span teams. Permanent coordination overhead.


Conclusion

The decision that determines whether a service architecture succeeds is where the boundaries go, and boundaries are learned rather than designed. Inside a monolith, learning that a boundary is wrong costs a refactor. Across services, it costs a migration.

That asymmetry argues for finding the boundaries first, in a modular monolith with mechanically enforced separation and per-module data ownership. That structure delivers most of the organisational benefit — clear ownership, independent development, reasoning in isolation — while retaining transactions, simple local development, and the ability to move a boundary that turns out to be wrong.

Then extract a service when there is a specific reason: genuinely different scaling needs, different availability requirements, team autonomy at a size where coordination is actually the bottleneck, or regulatory isolation. Not because the architecture is fashionable, and not on an expectation that it will make development faster, which in the short term it will not.

And build the tracing, logging, deployment automation, and local development story before the first extraction rather than after. Those are the precondition, and the difference between microservices that work and a distributed monolith is largely whether they exist.


Frequently Asked Questions

At what organisational size do microservices make sense? Usually past several dozen engineers, where coordination on a shared codebase becomes the binding constraint. Below that, the operational overhead typically exceeds the coordination savings.

Is a monolith inherently unscalable? No. A well-built monolith scales horizontally by running more instances. The scaling limits people attribute to monoliths are usually database limits, which persist after splitting.

What is the strongest indicator of a bad boundary? Shared database access. If two services read or write the same tables, they cannot deploy or evolve independently, which means the boundary provides nothing.

How can service size be judged? By whether it owns a coherent business capability with its own data. Line counts are not meaningful — some correct services are small, others are substantial.

Should distributed transactions be implemented? Prefer restructuring so they are unnecessary. Where genuinely required, sagas with compensating actions work, and their necessity is usually a signal that the boundary is wrong.

Can a distributed monolith be recovered? Yes, by consolidating services that cannot deploy independently back together, then re-splitting along boundaries that hold. Uncomfortable and frequently the correct move.

What is the most common reason splits fail? Data not being separated. Extracting code while leaving shared tables moves the code without moving the boundary, and the coupling is now across a network.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button