When to Actually Split the Monolith
Quick answer: Split when you can name and measure the pain. The real signals are concrete: teams blocking each other on releases, a component whose scaling profile genuinely diverges, a fragile component that can take the payment core down with it, regulated data needing a smaller blast radius, an organisation that has outgrown one coherent domain. The false ones are fashion, a conference talk, a new CTO's preference and a CV. Before splitting, exhaust module boundaries, indexes, caching, replicas and queues — and write down the metric the split must move.
Somewhere between the first ten customers and the first real growth curve, someone in the room says it out loud: maybe it's time to break this up.
Sometimes they're right. We are not here to defend monoliths past their usefulness — starting with one is a claim about early-stage risk, not a permanent position. Refusing to split when the pain is real is its own kind of expensive.
The problem is that most splits begin without anyone stating what the split is for. The test: can you name the pain, and can you measure it? If you can't, the moment hasn't arrived. Splitting on fashion is how a team acquires all the costs of a distributed system and none of the benefits.
The running example, as through the rest of this series, is a cross-border money transfer product: GBP leaves London, EUR arrives in Lisbon.
What counts as a real signal to split?
There are five, and each is a sentence you could put a number next to.
Teams are blocking each other on releases. Not "coordination is annoying" — measurable waiting. A finished KYC fix sits for six days because the compliance work in the same release train isn't ready, and nobody ships a half-tested branch into a payments system. Independent deployment is the most reliable reason to extract a service, and it needs teams, plural: two contending for one release train is a signal, one team that occasionally rebases is not.
One component's scaling profile genuinely diverges. FX quoting is read-heavy and chatty: every screen refresh, every rate poll, every calculator keystroke. The payout engine handles far fewer instructions a day, each long-running and waiting on a bank rail. Scaling one app for quote traffic means provisioning screening and payout capacity you will never use. The word doing the work is genuinely: a different order of magnitude and a different shape — CPU-bound versus IO-bound, spiky versus steady — not "this endpoint is our busiest".
A fragile component threatens the payment core. This is the argument internal modularity cannot answer: a module boundary protects you from bad coupling, not from a process-level failure. A partner SDK that leaks memory, a document generator that allocates its way into an OOM kill, a native library that segfaults — when it shares a process with transfer execution, its worst day becomes your worst day. Extraction here is containment, not tidiness.
Regulated data needs a smaller blast radius. Raw KYC documents, identity images, card data. Isolating them shrinks what audits must cover and shortens the list of engineers who can reach them. What a licence or regulator requires by way of segregation varies by jurisdiction and licence type — confirm it with a lawyer qualified where you operate, not from an architecture diagram.
The organisation has outgrown one coherent domain. When "the payments team" quietly covers onboarding, compliance operations, treasury and three partner integrations, and nobody holds the whole thing in their head, the boundary already exists socially. That makes it a hiring and ownership decision wearing an architecture costume, which is why it belongs with how you build the team.
| Real signal | What it looks like in a transfer product | The number that proves it |
|---|---|---|
| Release contention | KYC fix waits on the compliance team's branch | Days a ready change waits before it ships |
| Divergent scaling | Quote traffic dwarfs payout volume, and behaves nothing like it | Requests per second and resource shape per component |
| Fragile neighbour | Document generation OOMs the process that executes transfers | Incidents where component A took down component B |
| Regulated data | Raw KYC images and identity documents in the main app | Systems and people inside audit scope |
| Org outgrew the domain | Three teams, one shared domain nobody fully holds | Teams per bounded context; onboarding time for a new engineer |
Which signals are false, and why do they feel so convincing?
Because they arrive dressed as strategy, usually from someone senior who is right about most things.
| What gets said | What it usually means | What to do instead |
|---|---|---|
| "Everyone does it at scale" | We are copying the endpoint of a decade-long journey, not the path | Ask which of their problems you currently have |
| "There was a great talk about this" | A company with 400 engineers solved a 400-engineer problem | Read the talk again, looking for their team count |
| "The new CTO prefers services" | A preference formed at a previous company, at a previous size | Ask what pain it removes here, in a sentence |
| "It'll be good for the team's skills" | Resume-driven development, with production as the training set | Fund the learning some other way; don't pay for it in incidents |
| "The monolith is technical debt" | Something is unpleasant to work in, and the topology got blamed | Diagnose the actual debt before changing the topology |
The last row deserves saying flatly, because it is the most common: a monolith is not technical debt. An unstructured monolith is. Tangled dependencies, a shared database everything reaches into, boundaries nobody enforces — that is real debt, and distributing it does not repay it. It turns a tangle you can refactor with a compiler into one you refactor with versioned APIs and data migrations. The cure for a big ball of mud is boundaries, and boundaries fit inside one deployable: the whole argument for a modular monolith.
Does "we can't scale" mean the architecture, or one slow query?
In our experience it means one slow query far more often — and the two are easy to tell apart if you look before committing a quarter.
The usual culprits are boring and specific: an N+1 query on the transaction list endpoint firing one statement per row; a missing composite index on (account_id, created_at) that turns a customer's history into a table scan as the table grows; a synchronous call to a KYC provider inside a request the user is waiting on; an FX rate table read on every page load with no cache. Each produces exactly the symptom — slow now, worse with growth — that gets narrated as "we've outgrown the monolith".
The diagnostic takes days. Look at p99 latency by endpoint, not averages across the app; if two or three endpoints dominate, you have a query problem, and the next question is where their time goes — database, external call, or your own CPU.
And a monolith scales horizontally — stateless instances behind a load balancer is the same scaling story services get. What does not scale by adding instances is the database, and splitting services does not fix a database unless you also split the data, the genuinely hard part. Teams routinely spend a quarter on extraction and arrive at the same overloaded database, now reached through five codebases.
What should you try before splitting?
Most scaling problems die here, cheaply, in the time it would take to write the migration plan.
- Module boundaries first. If changes ripple unpredictably, that's a boundary problem, and boundaries inside one deployable are enforceable with import rules and architecture tests in CI. Do this regardless — you need it before any extraction.
- Indexing and query work. The highest-return day of engineering most growing products have available: composite indexes matching real access patterns, N+1s removed, query plans checked at production data volumes, not a dev seed of 500 rows.
- Read replicas. Reporting, admin tooling, compliance exports and transaction history are reads. Move them off the primary and the write path stops competing with them.
- Caching. FX rates, fee schedules, corridor configuration, partner metadata — read constantly, changed rarely. A cache here often removes more load than an extraction would.
- Background work into queues. Notifications, document generation, webhook fan-out, reconciliation jobs and provider retries do not belong in a request a user is waiting on. Moving them out shortens p99 and is most of the work of a future extraction.
- Partitioning. When a ledger or events table becomes the problem by sheer size, partitioning by time or account is a well-understood answer needing no distributed system.
None of this is glamorous, which is rather the point — nobody gives a conference talk called "We Added The Index". But if the product is fast, deploys are unblocked and incidents are rare after two weeks of it, you have saved a quarter and a permanent operational bill.
What metric is the split supposed to move?
The discipline that separates good splits from bad ones costs one paragraph in a document: write down the metric the split is supposed to move, before you start.
Pick one, with today's number and a target:
- Deploy frequency, or its sharper cousin: days a finished change waits before it ships.
- p99 latency on a named endpoint, under a named load.
- Incident blast radius — how many customer-facing capabilities go down when component X fails.
- Onboarding time — days until a new engineer ships something in this area alone.
Then set a date to check. Not because the number will be perfectly attributable, but because writing it down moves the argument from taste to evidence and tells you whether the first extraction worked before you commit to the next five. If nobody will commit to a number, what you have is a preference.
No metric, no split. That rule has never cost us anything, and it has stopped several expensive quarters.
What does splitting actually cost?
Fair is fair, so here is the bill.
During: you live in two worlds. Some functionality is extracted, some isn't, and every engineer has to know which is which. There is a data migration with a dual-write period and a cutover, running while real transfers move through the code you are changing. The roadmap slows, whatever the plan said. And the most common failure mode is not a bad split — it's a split abandoned at 60%, leaving a distributed monolith on a shared database.
After, permanently: a pipeline, secrets and configuration per service, a wider on-call surface, and one stack trace becoming several correlated logs. Debugging changes character — you stop asking "what threw?" and start asking "where did the request stop, and what state were the other four in?" And the database transaction is gone, replaced by idempotency keys, an outbox and reconciliation you now own.
That machinery is the price of the architecture, not an optional extra. At the scale we saw on Wise's core payment platform the distributed design is correct — and it is carried by dedicated reconciliation systems, mature idempotency and an operations function. Running a market surveillance platform across three production regions taught the same lesson from the operations side: every additional deployable is a permanent commitment.
A checklist you can actually apply
Run these in order. Stop at the first "no".
- Can you name the pain in one sentence, without the word "microservices" in it? If not, stop.
- Can you measure it today? Write the number down. No number, no split.
- Have you tried the cheap fixes — indexes, caching, replicas, queues, real module boundaries? If not, do those and re-measure.
- Would extraction actually fix this pain? If the bottleneck is one shared database, a new service will not help unless you split the data too.
- Is the boundary already clean in code? A module that owns its tables and is reached only through its interface can be extracted. One that shares tables is a data migration wearing an architecture costume.
- Can you extract exactly one thing — the smallest, least-coupled candidate — and verify the metric moved before doing another? If five extractions are needed before any benefit appears, that is a rewrite.
- Can you afford the permanent overhead? Pipeline, on-call, tracing, reconciliation. If the team is already stretched thin, that is your answer.
The right first extraction is almost never the payment core. It is notifications, document generation, a provider integration, reporting — loosely coupled work where a mistake is survivable and the team learns the mechanics. The core goes last.
FAQ
How do I know if we've genuinely outgrown our monolith?
Point at a pain and a number: days a ready change waits, a component with a load profile an order of magnitude apart from the rest, a fragile component that has already taken production down with it, regulated data in audit scope, or more teams than the domain can hold. Anything else is a preference.
Is a monolith technical debt?
No. An unstructured monolith is — tangled dependencies and no enforced boundaries. That debt is repaid with module boundaries, not by distributing the tangle across a network, which turns a refactor the compiler can check into versioned contracts and data migrations.
We have performance problems. Won't microservices fix them?
Usually not. Performance problems in growing products are overwhelmingly missing indexes, N+1 queries, uncached hot reads and synchronous external calls in the request path. And if the bottleneck is one shared database, services do not help until you split the data.
How long does a split take?
Longer than the plan, because the code is the easy half and the data is the hard half — schema separation, dual writes, cutover, all while production traffic keeps moving. Which is why each extraction should be independently valuable rather than staged as one migration project.
Can we split later if we don't do it now?
Yes, if modules own their tables and talk through interfaces. That is the whole argument for designing a monolith you can split later: it keeps the option open at almost no cost, to be exercised when a real signal appears rather than guessed at now.
Trying to decide whether it's time?
The useful version of this conversation lasts an hour and produces one page: the pain in a sentence, the number that proves it, the cheap fixes you haven't tried, and the smallest extraction worth doing first.
GPO-Tech builds commercial software and connected products from Tallinn, Estonia, inside the EU and in European working hours. Our payments background is first-hand: two and a half years on Wise's core payment platform team, plus cross-border transfer products and a real-time market surveillance platform for regulated markets running across three production regions. We have done both the extraction that was overdue and the one that shouldn't have happened, and we'll tell you which you're looking at.
Send us your architecture and the pain you're feeling, and we'll tell you whether it's a split or an index. Ask a question → or book a call with our engineers →.
GPO-Tech designs and builds connected products and commercial software end to end — from one team in Tallinn, Estonia.