Every SaaS engineer hits the same wall eventually: the codebase runs, ships, and makes money, and every change to it takes longer than the last one. Refactoring a monolithic SaaS into modular boundaries is the fix nobody wants to schedule, because it produces zero visible feature for weeks. I've done this exact rebuild-in-place twice now, once on a codebase nobody else could safely touch anymore, and the lesson both times was the same: you don't need microservices, you need honest boundaries inside the one deployable you already have.
This isn't a pitch for a rewrite. Most teams that reach for microservices when their monolith gets unwieldy actually need something smaller and much less risky: a modular monolith, where the code is organized into real, enforced boundaries but still ships as one thing. No new servers to provision. No deployment pipeline to redesign. Just boundaries, enforced in code, inside the deployable you already have. The SaaS MVP stack I recommend for a greenfield build is deliberately monolithic on day one, because a modular monolith is where almost every growing SaaS should land before it earns the operational cost of splitting into services.
Key Takeaways
- A monolith doesn't need microservices to fix its coupling problem. It needs module boundaries enforced in code, not folders.
- Organizations consolidating microservices back into fewer deployable units rose from 23% to 42% between 2024 and 2026, according to a 2026 analysis by developer Daniel Jeong on modular monolith adoption. The pendulum swung, and most teams never needed to swing it in the first place.
- Bounded contexts come from asking which parts of the codebase change together and which teams use different vocabulary for the same nouns, not from drawing boxes on a whiteboard.
- Extract in this order: pure side-effect modules first (notifications, audit logging), core business logic last. Data isolation follows the same order.
- A shared database across "separated" services is the distributed monolith trap. You inherit the network calls without gaining independent deploys.
- Nx and Turborepo solve a build-orchestration problem, not a boundary problem. A modular monolith works in a single package with disciplined imports before it ever needs either tool.
When Does a SaaS Codebase Actually Need Module Boundaries?

A SaaS codebase needs module boundaries once two unrelated features can no longer change independently without both breaking. That's the whole test. Everything else, team size, line count, deploy frequency, is a proxy for that one symptom.
On Callidus, the multi-tenant clinic SaaS platform I built for UK aesthetic clinics, the trigger was six roles: super admin, owner, admin, manager, practitioner, receptionist, each with different access to clinical data, financial data, settings, and billing. Inline role checks scattered through components rot the moment the access matrix changes even slightly. So the fix wasn't a services split. It was a set of access helpers, hasClinicalAccess, hasManagerAccess, hasAdminAccess, that every screen and every Firestore rule calls through instead of inlining its own logic. That's a module boundary. It has nothing to do with how many servers the app runs on.
Have you ever opened a file to fix one small thing and found six unrelated features tangled through it? Closed the laptop instead of touching it? That reflex, avoidance dressed up as prioritization, is usually the first honest signal that boundaries are missing.
How Do You Find the Bounded Contexts Hiding in Your Monolith?

You find bounded contexts by asking which parts of the code change together, not by drawing an architecture diagram first. Milan Jovanović's refactoring approach, laid out in his write-up on overgrown bounded contexts, starts with exactly this question, then checks whether different parts of the team use different vocabulary for what looks like the same entity. If billing calls it a "subscription" and support calls it an "account," that's a seam, not a synonym.
His extraction order matters more than the mapping exercise. Start with the modules that are pure side effects, notification sending is the classic first cut, because nothing else depends on their internals, only on the event that triggers them. Billing shouldn't even need to know notifications exist; it just needs to emit an event and walk away. Core business logic, the stuff every other module actually depends on, comes last, once you've built the muscle of extracting things safely.
Data isolation follows the same order. Give each module its own schema and its own dedicated database role, one write path, and replace any cross-module join with a read-only view or a subscriber-owned read model, per Jovanović's data-boundary guidance. Resist the urge to query across schemas just because the tables sit in the same physical database. That urge is exactly what turns modules back into one tangled thing wearing separate folder names.
The Distributed Monolith Trap

The fastest way to make a monolith worse is to split its code into "services" while every service still reads and writes the same shared database. You get every cost of a distributed system, network hops, new failure points, cascading redeploys, and none of the benefit, because nothing actually deploys independently. One analysis calls the shared database "the anchor chaining you to your monolith," which is the most accurate one-line description of the failure mode I've read.
Do You Need Nx or Turborepo for a Modular Monolith?
You don't need Nx or Turborepo to start a modular monolith at all, because plain workspace boundaries and disciplined imports handle the job first. Reach for either tool once build times or dependency-graph complexity actually hurt, which for most SaaS teams is well after the boundary work is done.
Monorepo tooling is now genuinely mainstream. Sixty-three percent of companies with 50 or more developers run one, per daily.dev's 2026 monorepo tooling survey, and the two tools solve different-sized problems.
| Tool | Best fit | What it actually buys you | Where it falls short | |---|---|---|---| | Plain npm/pnpm workspaces | Solo builder, one team, under ~10 packages | Zero config, fast enough | No task caching, no dependency-graph visualization | | Turborepo | 5-50 packages, JS/TS-only, Vercel-deployed | Remote caching, simple pipeline config | Less suited to multi-language or enterprise-scale graphs | | Nx | Large orgs, complex dependency graphs, code generation needs | Architectural rules, generators, multi-language support | Steeper setup, more to learn for a small team |
Mercari's Web Platform Team rolled out a self-hosted Turborepo remote cache in February 2026 and cut PR build durations by 30% overall, with individual task durations down 50%, according to the same daily.dev report. That's a real number, but it's a caching win on top of boundaries that already existed. Buy the tool after the discipline, not instead of it.
Shipping the Boundary Extraction Without a Rewrite
- Pick the lowest-risk module first. Notifications, audit logging, and anything that's a pure side effect of another action are the safest starting points because nothing else needs their internals.
- Give that module its own schema or table prefix, plus a dedicated database role that only it can write through.
- Replace every direct call into the module with a domain event. The calling code shouldn't know the module exists, only that an event fired.
- Add a boundary check to your CI pipeline. A lint rule or an architecture test that fails the build on a cross-module import turns the boundary from a convention into a fact.
- Repeat on the next-lowest-risk module. Save the module everything else depends on, usually billing or the core entity model, for last.
- Only reach for Nx, Turborepo, or a service split once build time or team-ownership friction, not code tidiness, is the actual bottleneck.
None of this requires stopping feature work for a quarter. Each step ships on its own, and the B2B SaaS codebase gets safer to change after every single one, not just at the end.
Picture the moment it usually goes sideways. Wednesday afternoon, three sprints into a boundary extraction, you rip out a direct import between two modules and the build breaks somewhere that has nothing to do with either one. Trace it far enough and there's almost always a third file quietly relying on both internals, written by someone who left the team, referencing a shortcut nobody documented. That's usually the actual reason teams stall halfway through this kind of work: the boundary you're drawing keeps surfacing debt nobody remembers writing, not the boundary itself.
Let me back up on one thing. Extraction order isn't really about risk tolerance. It's about which modules can fail loudly during the transition without taking a paying customer's data with them. Notifications failing is an annoyance. A multi-tenant billing module failing mid-extraction is a support queue and a refund.
If you're still deciding between shipping fast on a no-code stack versus writing the code yourself, this whole exercise is premature. Module boundaries are a problem you earn by having enough paying customers that a monolith actually got tangled. Most teams reach for the architecture book before they've reached the problem the book is solving.
Pick one module in your codebase today, the one you're most afraid to touch, and ask what it would take to make it depend on an event instead of a direct call. That's the whole first step. Everything else in this post is just what to do after.
