33%

Lesson 1 of 3

What Makes a Monolith Modular

One deployable, several modules, and boundaries that something in the build actually refuses to let you cross. Without the third part it is just a large application.

11 min read

A modular monolith is a single deployable unit divided into modules with explicit boundaries, where each module owns a business capability and exposes a deliberate public surface. It is not a compromise or a stepping stone — for a large share of systems it is the correct end state, because it gives most of the organisational benefits of services without the operational cost of distribution.

Three arrangements of the same code

The difference is not the number of deployables. It is whether a boundary exists and what it costs to cross.

A big ball of mud where every module imports every other module freely. A modular monolith where four modules communicate only through published surfaces inside one deployable. A microservice system where the same four modules are separate deployables communicating over a network.

Big ball of mud

One deployable

any file may import any file

No enforced edges

coupling grows silently

Modular monolith

One deployable

one process, one release

Enforced module surfaces

build fails on a breach

In-process calls

no network, no partial failure

Microservices

Four deployables

independent releases

Network boundaries

partial failure is now normal

Notice what the middle column keeps and what it avoids. It keeps a single transaction across modules, one deployment, one place to look during an incident, one set of credentials and one runtime to upgrade. It avoids the properties that a network forces on you: partial failure, retries, idempotency, distributed tracing as a prerequisite rather than a nicety, and the coordination cost of releasing several units.

A module surface and the rule that makes it real. Everything not exported here is private to the module.typescript
// features/assessment/index.ts — the entire public surface of the module.
export { quizService } from './application/quiz-service';
export type { GradedAttempt, QuizSummary } from './domain/quiz';

// Allowed from another module:
import { quizService } from '@/features/assessment';

// Refused by the build, even though the file exists and the import compiles
// in an editor:
//   import { PrismaQuizRepository } from '@/features/assessment/infrastructure/prisma-quiz-repository';
//
// The rule in .dependency-cruiser.cjs is what converts a naming convention
// into a boundary. Without it, the first deadline puts that import in place
// and nobody notices for a year.

How modules talk

StyleWhat it looks likeUse it when
Direct call through the public surfaceModule A calls `billingService().charge(...)` synchronouslyA needs the result now, and a failure should fail the whole operation
In-process domain eventA publishes `OrderPlaced`; B and C subscribe, in the same transaction or just afterSeveral modules react to something, and A should not know who they are
Shared read model owned by one moduleB maintains a projection it owns, updated from A's eventsB queries A's data constantly and the query shapes differ from A's model
Three communication styles inside one deployable, and when each is appropriate.

In practice

Choosing a modular monolith with services on the table

A health-tech company of eighteen engineers was rebuilding a patient-scheduling platform. Leadership expected microservices, having been told that was what scaling required. The team had four capability areas: scheduling, clinical records, billing and notifications.

Constraints

  • Eighteen engineers in three teams
  • Peak load of 300 requests per second, growing 40% per year
  • Two engineers with production Kubernetes experience
  • Regulatory requirement for a complete audit trail across all four areas

Decision

One deployable with four enforced modules, each owning its own database schema, communicating through published surfaces and in-process events. Notifications was designed with an outbox so that it could be extracted later without changing its callers.

Why

None of the usual justifications for separate services applied: no module needed independent scaling at 300 requests per second, no team was blocked by another's release cadence at three teams, and there was no differing availability requirement. Meanwhile the audit requirement was much easier to satisfy with one transaction spanning all four modules, and the team had very little operational capacity for a distributed system.

What it cost

The whole application must be redeployed for any change, so a risky change to billing carries a small risk to scheduling — mitigated by trunk-based development, feature flags and a fast pipeline. They also accepted that if the company reaches a size where three teams become eight, the extraction work will be real, and they reduced its cost in advance by enforcing module surfaces and separate schemas from day one.

A stakeholder asks why the system is "still a monolith"

As a developer

Explains that microservices would be over-engineering at this size and that the team lacks the operational experience to run them. True, and it sounds like a limitation being defended.

As an architect

Reframes the question around what the business is buying. Independent deployment is worth paying for when teams are blocked by each other's release cadence; independent scaling is worth paying for when one component's load differs by an order of magnitude. Neither is true today, and here is the specific measurement — release blocking incidents per quarter, and the load profile — that will tell us when it becomes true. The point is not that services are premature; it is that the trigger is written down and being watched.

A team says they have a modular monolith. Each capability has its own directory, code review checks that modules only use each other's public interfaces, and the team is disciplined. What is the risk?Reveal

The boundary depends entirely on human attention, which is exactly the resource that disappears near a deadline, during an incident, and whenever a new joiner does not yet know the rule. The characteristic pattern is that nothing breaks for a year and then someone attempts to extract a module and finds forty imports reaching into its internals, each added by a reasonable person under pressure. The fix is inexpensive: one dependency rule in the build that fails on a forbidden import. It costs an afternoon and converts a convention into a property of the system. Note also the secondary risk — even with import rules, two modules may still be reading each other's tables, which the import graph cannot see.

Key takeaways

  • A modular monolith is one deployable with modules that map to capabilities and have enforced public surfaces.
  • The enforcement is the architecture; the folder structure without it is a big ball of mud with better naming.
  • It keeps single transactions, one deployment and one place to debug, avoiding the costs a network imposes.
  • Modules communicate by direct calls through the surface, by in-process events, or through owned read models.
  • Shared tables defeat modularity even when the import graph looks clean.
What Makes a Monolith Modular · Architecture Atlas