ZycoSoft
Architecture & Engineering

The Modular Monolith in 2026: How to Structure It So It Stays Clean, Scales Without a Rewrite, and Doesn't Become the Big Ball of Mud You Were Trying to Avoid

The modular monolith is no longer a compromise. Here is how to structure one so it stays clean under pressure, scales without a rewrite, and gives you a clear, evidence-based signal when a module is genuinely ready to leave.

Architecture & Engineering

Share

The Modular Monolith in 2026: How to Structure It So It Stays Clean, Scales Without a Rewrite, and Doesn't Become the Big Ball of Mud You Were Trying to Avoid

The Modular Monolith in 2026: How to Structure It So It Stays Clean, Scales Without a Rewrite, and Doesn't Become the Big Ball of Mud You Were Trying to Avoid . 

Most SaaS architecture debates in 2026 are still framed as a binary: monolith or microservices. That framing is the problem. A poorly structured monolith becomes unmaintainable at 50,000 lines. A prematurely distributed microservices architecture becomes unmaintainable at 15 services. The question engineering leads should actually be asking is how to build a modular monolith architecture for SaaS that enforces the discipline of distributed systems without paying their operational cost, and that gives you a clear, evidence-based exit ramp when a specific module genuinely earns the right to leave.

The industry data in 2026 supports this more than it ever has. Shopify's engineering team published internal results from adopting Packwerk-enforced, pack-based modularisation across their Rails monolith: 55% faster developer onboarding and 68% fewer cross-module regressions. Sam Newman's updated second edition of Monolith to Microservices explicitly repositions the modular monolith as a first-class production architecture rather than a transitional step. And the microservices regression trend is now well-documented, with engineering teams at companies of all sizes consolidating distributed services back into unified applications after the operational overhead proved disproportionate to the scale problems it was solving.

This post goes inside the modular monolith itself. Not the decision of whether to use one (see our existing piece on monolithic vs microservices architecture for SaaS), but how to structure it correctly from day one so it stays clean under pressure.

Why Modular Monoliths Fail: The Coupling Problem

A modular monolith fails for one reason: coupling accumulates quietly until the boundaries that were supposed to exist have been bypassed so many times that the module structure is fiction. The code is still in folders labelled by domain, but every module is importing directly from every other module's internals, and a change to the billing logic breaks the notification service three layers away.

This happens because most teams treat modularisation as a folder convention rather than an enforced contract. Folders are bypassed under deadline pressure. Enforced interfaces are not. The distinction between a well-structured modular monolith and a big ball of mud is not intent, it is enforcement.

The second failure mode is boundary design based on technical layers rather than business capabilities. A structure organised as controllers, services, and repositories gives you nothing. Every feature touches all three layers, so every change is a cross-cutting concern. A structure organised around Billing, Tenancy, Notifications, Reporting, and Integrations gives you boundaries that reflect how the business actually changes over time.

How to Design Internal Module Boundaries That Hold : 

The right boundary unit is a business capability: a cohesive set of behaviour, data, and rules that changes for the same business reasons and is owned by the same team or the same area of product thinking. This maps directly to Domain-Driven Design's bounded context concept, applied at the in-process level rather than the service level.

For a typical SaaS product, a defensible initial module decomposition might look like this:

  1. Identity and Access: authentication, authorisation, roles, permissions.
  2. Tenancy: tenant provisioning, plan assignment, feature entitlement.
  3. Billing: subscription lifecycle, payment processing, invoicing, webhooks from Stripe or Mollie.
  4. Core Domain: the primary product behaviour (one or more modules depending on complexity).
  5. Notifications: email, in-app, webhook delivery, templating.
  6. Reporting and Analytics: read-side aggregations, exports, dashboards.
  7. Integrations: third-party API adapters, import/export pipelines.

Each of these modules owns its own data models. Within a single shared database, each module accesses only tables that belong to it, using a named schema prefix or a documented ownership convention. Cross-module data access is mediated through the module's public API, never via a raw query that joins across module boundaries. This is the seam that makes future extraction tractable, because if you ever need to move Billing into its own service, you already know exactly which data it owns and which contracts other modules depend on.

If your SaaS product handles multi-tenant data isolation, the Tenancy module boundary is particularly load-bearing. Getting this wrong creates security and compliance risk that propagates everywhere. Our guide on multi-tenant SaaS architecture and tenant isolation covers the data model decisions in detail.

Enforcing Interface Contracts Without Network Overhead : 

The architectural insight that makes the modular monolith genuinely powerful is this: you can enforce the discipline of service-to-service communication at the code level, in process, without paying the latency, serialisation, or operational cost of a network hop.

The mechanism is straightforward. Each module exposes exactly one public entry point: a facade class, an interface file, or an explicitly exported module object. Everything else inside the module is private by convention and, critically, by static analysis enforcement. No other module is permitted to import directly from a non-public path inside another module.

In practice, the enforcement tooling depends on your stack:

  1. Ruby on Rails: Packwerk (the same tool Shopify uses) defines package boundaries and fails CI on any violation of the dependency graph.
  2. Java or Kotlin: ArchUnit allows you to write architectural tests that assert no class in the Billing package imports directly from a class in the Tenancy package's internal namespace.
  3. TypeScript/Node.js: Custom ESLint rules or the dependency-cruiser tool can enforce that imports never cross module boundaries except through the designated index file.
  4. Python: import-linter provides explicit contract definitions between modules and fails on violations.

These rules run in CI. A pull request that bypasses a module boundary fails the build. This is non-negotiable. Without automated enforcement, the boundaries erode within two to three months of team growth or deadline pressure. The tooling is the policy.

Evolvability Patterns: Keeping the Architecture Honest at Scale

A modular monolith is not a static structure. It needs patterns that allow it to evolve without requiring you to rewrite the internal contracts every time the product changes.

Three patterns are worth building in from the start :

  1. Internal event bus: Rather than having Module A call Module B directly when something happens, Module A publishes an internal domain event (for example, SubscriptionActivated or TenantProvisioned) and Module B subscribes to it. This decouples the modules temporally without requiring a message broker. When you eventually extract a module to a service, you swap the in-process event bus for a real queue with minimal changes to the business logic.
  2. Versioned module APIs: When a module's public interface needs to change, introduce the new version alongside the old one and deprecate the old one explicitly. This prevents the silent contract breaking that turns a refactor into a multi-team incident.
  3. Read-side separation: Reporting and analytics queries often have access patterns that are fundamentally different from the write-side domain logic. Giving Reporting its own read models (populated by events from the domain modules) prevents it from coupling to the internal schema of every other module and removes a common source of performance-driven coupling, where a reporting query starts joining across module tables because it is the easiest path.

These patterns also make the feature flag and entitlement architecture that sits in your Tenancy module far cleaner to evolve, because the entitlement check is a well-defined call through a public interface rather than a condition scattered across module internals.

The Four Signals That Tell You a Module Is Ready to Extract : 

Extraction to a microservice is justified when a specific, measurable pressure makes the cost of extraction worth paying. "We might need to scale this someday" is not that pressure. Here are the four signals that are:

  1. Independent deployment pressure: The module needs to be deployed significantly more or less frequently than the rest of the application, and the coupling of those release cycles is creating concrete release bottlenecks or regression risk.
  2. Team ownership conflict: A separate team with a separate roadmap owns the module, and coordinating releases across teams is consuming more engineering time than the cost of service extraction would.
  3. Distinct scaling profile: The module has a computational or infrastructure requirement that is materially different from the rest of the application. A machine-learning inference module that needs GPU allocation is the canonical example. Scaling the entire monolith to get GPU access for one module is wasteful enough to justify extraction.
  4. Data model divergence: The module's data access patterns are so different that they are causing schema migration conflicts, locking contention, or forcing index strategies that hurt the rest of the application. This is the signal that the shared database has become a constraint rather than a simplification.

If none of these four signals are present, keep the module where it is. The absence of these signals is the evidence that extraction is premature, not a failure of ambition.

How ZycoSoft Makes This Architecture Decision in Practice : 

Every SaaS product we build at ZycoSoft starts from a deliberate architecture decision rather than a default. For most early and growth-stage products, a well-structured modular monolith is the right call. It is not a compromise or a stepping stone. It is the architecture that delivers the fastest time to production, the lowest operational overhead, and the clearest path to extraction when a genuine signal arrives.

In our Custom SaaS Development engagements, we enforce module boundaries from sprint one using the tooling described above, design the internal event bus before the first cross-module integration is needed, and document the public API surface of every module as a first-class artefact alongside the code. When we take on MVP Development work, we scope the initial module decomposition explicitly so that the boundary decisions made at MVP stage do not become the technical debt that forces a rewrite at Series A.

We have seen both failure modes up close: the big ball of mud that accumulated because nobody enforced the boundaries, and the prematurely distributed system that required a team of five to operate infrastructure that should have needed one. The modular monolith done correctly sits between them, and getting it right from the start is what separates a SaaS codebase that scales gracefully from one that requires a painful restructure at the worst possible moment, right when growth demands your engineering capacity for product, not for archaeology.

Our embedded engineering teams bring this architectural discipline as standard, not as a later-stage conversation. If you are building a new SaaS product or inheriting one that is approaching the big ball of mud territory, the time to get the structure right is before the next major feature cycle, not after it.

Talk to us about your SaaS architecture before the next sprint starts. Get in touch with ZycoSoft here.

Share

Frequently Asked Questions

A modular monolith is a single deployable unit whose internal code is divided into explicit, bounded modules with enforced interface contracts. A standard monolith has no enforced boundaries, so any part of the code can call any other part, which leads to the big ball of mud over time. The modular version keeps deployment simple while preserving the architectural discipline that prevents coupling from accumulating.

It is a valid production architecture in its own right. Sam Newman's updated second edition of Monolith to Microservices explicitly repositions the modular monolith as a first-class production choice, not a transitional state. Shopify runs its core commerce platform as a modular monolith using Packwerk-enforced boundaries and published internal data showing 55% faster developer onboarding and 68% fewer cross-module regressions after adopting this approach.

Enforce boundaries at the code level rather than at the network level. Each module exposes a public API surface (a single facade class or interface file) and all cross-module calls must go through it. Use static analysis tools such as Packwerk for Ruby, ArchUnit for Java, or custom ESLint rules for TypeScript to fail CI builds on any direct cross-module import. This gives you the discipline of microservices communication without the latency or operational cost.

There are four signals worth acting on: first, the module has a deployment cadence that is meaningfully faster or slower than the rest of the application; second, a separate team owns it and cross-team coordination is creating release bottlenecks; third, its scaling profile differs materially (for example, it needs GPU access or horizontal scaling the rest of the application does not); fourth, its data model has diverged enough that sharing a database is causing schema migration conflicts or performance contention.

The most defensible pattern is logical schema separation within a single database. Each module owns a named schema or table prefix and accesses only its own tables directly. Cross-module data access is mediated through the module's public API, not by joining across schemas at the query layer. This preserves the operational simplicity of a single database while building the seam that makes future extraction to a separate data store straightforward if the signal arises.

The microservices regression trend refers to the documented wave of engineering teams consolidating distributed services back into unified applications after discovering that the operational overhead (service mesh complexity, distributed tracing, network latency, and polyglot data consistency) outweighed the benefits at their scale. Teams that distributed prematurely are now rebuilding coherence. The practical response is to start with a well-structured modular monolith and extract services only when a specific, measurable pressure makes extraction worth the cost.

Planning a software project? Let us discuss how ZycoSoft can help.

Tell us what you are building and we will help you scope the right solution, team, and timeline.