Skip to content
SaaS Development2 July 2026 · 7 min readUpdated 30 September 2026

Refactoring a Monolithic SaaS Into Modular Boundaries in 2026

Most SaaS teams that reach for microservices need clearer module boundaries first. How to find bounded contexts, extract a module behind events, and decide whether Nx or Turborepo is worth adding.

Refactoring a Monolithic SaaS Into Modular Boundaries in 2026

If every change to your SaaS codebase takes longer than the last one, and there is one file you avoid opening, you probably need a modular monolith architecture rather than microservices. Your first action takes ten minutes: pick the module you are most afraid to touch and list every other part of the code that imports it. That list is the boundary you don't have yet. The rest of this post is about drawing it inside the one deployable you already run.

A modular monolith keeps one build and one deploy, but enforces real boundaries between modules. Martin Fowler's MonolithFirst note from 2015 argues that you shouldn't start a new project with microservices, and that any refactoring of functionality between services is much harder than in a monolith. Fowler also says he didn't have enough anecdotes at the time to be firm about the rule, so treat it as a strong heuristic, not a law. The SaaS MVP stack I recommend for a greenfield build starts as one deployable for the same reason.

When does a SaaS codebase actually need module boundaries?

Two vines from different plants tightly entwined around a single strained, cracked wooden trellis stake, illustrated in a botanical-tech style against a dark circuit-board backdrop

A SaaS codebase needs module boundaries once two unrelated features can no longer change independently without both breaking. Team size, line count and deploy frequency are proxies for that one symptom.

Milan Jovanović lists warning signs in his guide to overgrown bounded contexts from April 2025: fear of modifying code because everything is interconnected, single entities that serve several unrelated purposes, classes over 1,000 lines, and business logic from different subdomains bleeding into each other. The 1,000-line figure is his rule of thumb, not a measured threshold. If three of those four describe your code, boundaries are the next piece of work.

Callidus, the multi-tenant clinic SaaS for UK aesthetic clinics that I built on React, Firebase and Stripe Connect, shows the smallest version of a boundary. The platform has six roles: super admin, owner, admin, manager, practitioner and receptionist, each with different access to clinical data, financial data, settings and billing. The case study explains that inline role checks rot the moment the matrix changes, so the codebase is built around role helpers (hasClinicalAccess, hasManagerAccess, hasAdminAccess) and every access gate stays consistent. That is a module boundary in the sense that matters here: one place owns the rule, and the rest of the code asks it. It has nothing to do with how many servers the app runs on.

How do you find the bounded contexts hiding in a monolith?

A glowing seed pod isolated inside a clear glass capsule at the center of a dense wall of tangled, chaotic ivy, botanical-tech illustration

Find bounded contexts by grouping the code that changes together, then test each group with vocabulary: if different teams use different words for what looks like the same entity, you have found a seam.

Jovanović's vocabulary test asks whether teams use different vocabulary for each area and whether you would give each domain to a different team. If billing calls something a subscription and support calls it an account, treat that as a boundary and not as a naming inconsistency. The bounded-context idea comes from domain-driven design, and the search term most people use for it is bounded context in microservices, but the same test works before any service exists. When the boundary is unclear, keep the module coarser and split later. A wrong split is expensive to undo.

Which module do you extract first?

Start with modules that are pure side effects. Jovanović's example is notifications: sending one is a pure side effect, it doesn't change business state, and so it is easier to decouple safely. Replace each direct call into the module with a domain event, so the calling code only announces that something happened. Core business logic, which every other module depends on, comes last, after you have practised extraction on the cheap cases. His steps, in order, are to identify subdomains by what changes together, extract one context at a time starting with low risk, replace direct calls with events, migrate data gradually, and repeat. His summary line is that boundaries should be enforced by code, not just folder structure.

Why must data boundaries follow code boundaries?

A module that owns its code but shares tables with everyone is only half separated. In his data boundaries article from August 2025, Jovanović recommends a schema per module and a dedicated database role whose privileges cover only that schema. Cross-module reads go through read-only views or read models built from events, not through joins across schemas. His code samples use EF Core, but the schema and role mechanism is plain PostgreSQL, so the idea carries to other stacks.

Three separate potted plants on a table with roots diverging above ground but all three connected underground through one shared, cracked clay pipe glowing at the crack, botanical-tech illustration

This is also why the modular monolith vs microservices debate often misses the point. If you split code into services that still read and write the same tables, you pay for network calls and partial failures but still can't deploy one piece alone, because a schema change touches all of them. That is my reasoning from how shared data works, consistent with Fowler's point that refactoring across services is harder than inside a monolith. Fix the data boundary first and the deployment question gets smaller.

What should the public surface of a module look like?

Jovanović's definition of a modular monolith from March 2024 is a single application organized into independent modules, each grouping related functionality behind a clear boundary, loosely coupled and communicating through public APIs, and still deployed as a single unit. In practice that means each module exposes a small entry point and keeps everything else private. In a TypeScript codebase, the entry point can be one index file per module, and the lint rule from the next section can forbid imports that reach past it:

// modules/notifications/index.ts  (the only file other modules may import)
export { sendNotification } from "./public/sendNotification";
export type { NotificationRequested } from "./public/events";

Keep that surface small on purpose, and review it like any public API, because a change to it affects every caller in the repository. Every function you export is a promise to the rest of the codebase, and a short list of promises is easier to keep.

Do you need Nx or Turborepo for a modular monolith?

No. Plain workspaces plus an import rule in CI are enough to start, and a build tool becomes worth adding when build times or dependency-graph complexity actually hurt.

The tools do different jobs, and one claim in older advice no longer holds. Turborepo's caching docs describe caching so you never do the same work twice, and remote caching that shares results across machines and CI. Its boundaries feature checks two violations: importing a file outside a package's directory and importing a package that isn't declared as a dependency. It also supports tags, and the docs mark it experimental. Nx's module boundaries feature works through project tags and declared constraints, enforced by the @nx/enforce-module-boundaries ESLint rule or by a conformance plugin.

OptionWhat it gives youBoundary enforcementWatch out for
npm or pnpm workspaces plus a lint ruleNo extra tooling, one installWhatever rule you write, for example a cross-module import check in CINo task caching
TurborepoTask caching, remote cachingExperimental boundaries check, with tagsFeature status: confirm it is still experimental before relying on it
NxProject graph, generators, cachingTag constraints through @nx/enforce-module-boundariesMore concepts to learn than a small team may need

Buying the tool before the discipline gets the order wrong. Boundaries come from deciding what each module owns and then failing the build when someone breaks that. The tool only makes the check cheaper to run.

How do you extract a module without a rewrite?

Extract one module at a time behind events, and let each step ship on its own. No step needs a feature freeze.

  1. Pick the lowest-risk module, usually a pure side effect such as notifications or audit logging.
  2. Give it its own schema or table prefix, with a database role that only it can write through.
  3. Replace every direct call into it with a domain event.
  4. Add a CI check, either a lint rule or an architecture test, that fails the build on a cross-module import. A boundary without a failing check is a convention.
  5. Repeat with the next-lowest-risk module. Leave the modules everything else depends on, often billing or the core entity model, for last.
  6. Consider a build tool or a service split only when build time or ownership friction is the bottleneck, not code tidiness.

Consider a team three sprints into this work that removes a direct import between two modules and watches an unrelated build break. That is a hypothetical, but a plausible shape: a third file depended on the internals of both. Such breaks are the boundary work surfacing coupling nobody had written down, and they are the reason to go module by module. Order matters too: pick modules that can fail loudly during the transition without touching a paying customer's data. A failed notification is an annoyance. A failed multi-tenant billing module mid-extraction is a support queue.

If you are still choosing between a no-code stack and writing the code yourself, or you are signing your first B2B SaaS customers, this work is premature. You earn the problem by having enough customers that the code tangled.

Open your repository now, run a search for imports of the module you fear most, and write down how many distinct callers it has. Which one of them could switch to an event first?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What is a modular monolith in SaaS architecture?

A modular monolith is one deployable application whose code is split into modules with clear boundaries and small public entry points. It ships as a single unit, with one build and one deploy, but modules talk to each other through explicit interfaces or events instead of reaching into each other's internals. Milan Jovanović's definition describes modules that are loosely coupled and communicate through public APIs while the application deploys as one unit. The difference from a plain monolith is enforcement: a lint rule or an architecture test fails the build when a module imports another module's internals.

How do you identify bounded contexts in an existing SaaS codebase?

Group the code that changes together, then check whether different teams use different vocabulary for what looks like the same entity. Jovanović's guide asks whether teams use different words for each area and whether you would give each domain to a different team. If billing says subscription and support says account, treat it as a boundary. Start from how the code actually changes, not from the folder structure, since folders usually record history. When a boundary is unclear, keep the module coarser and split later, because a wrong split is expensive to undo.

When should you refactor a SaaS codebase instead of rewriting it?

Refactor when the business logic still works and only the coupling between features has become the problem. A rewrite throws away edge cases nobody documented, and it freezes feature work while you catch up. Incremental extraction, one module at a time, lets each step ship on its own. Martin Fowler's MonolithFirst note makes a related point: refactoring functionality between services is much harder than inside a monolith. Consider a rewrite only when the underlying data model is wrong, not just tangled, which is a narrower case than most teams assume.

Do you need Nx or Turborepo to run a modular monolith?

No, a modular monolith can start with plain workspaces and an import rule in CI. Nx and Turborepo add build orchestration and caching, and both now offer some boundary checks. Nx enforces tag-based constraints through its module boundaries rule, and Turborepo has an experimental boundaries feature that flags imports outside a package and undeclared dependencies. Add a tool when build times or dependency-graph complexity hurt, and confirm the feature status in the current docs before relying on it.

How is a modular monolith different from microservices?

A modular monolith enforces boundaries in code inside one deployable unit, while microservices enforce them by deploying each piece separately over a network. The modular version gives up independent scaling and independent deploys per module, and it avoids network calls, service discovery and distributed tracing. A risky middle ground is splitting code into services that still share one database, because you take on network costs while a schema change still touches every service. That is my reasoning from how shared data behaves, so judge it against your own system before acting on it.