Architecture style

Modular monolith

One deployable, but inside it clear modules with their own data and a public API. You get the order of microservices without their network cost.

Modular monolith: one process, modules with their own data schemaspublic APIClientsHTTP layerCourses moduleBilling moduleUsers modulecourses schemabilling schemausers schema
Modular monolith: one process, modules with their own data schemas

The idea

A modular monolith splits code into modules by business domain: courses, billing, users, notifications. Each module has internal code nobody touches and a small public API that others use to talk to it.

Everything still deploys together, like a plain monolith. The difference is the rules: the billing module does not SELECT from courses tables, it calls courses.getPrice(courseId).

How to keep the boundaries

Boundaries must be checked automatically, or they will be gone in six months. Ruby has Packwerk from Shopify, Java has ArchUnit or Spring Modulith, TypeScript has the ESLint no-restricted-imports rule or dependency-cruiser.

Data is split too: a separate PostgreSQL schema per module (courses.*, billing.*) and no cross-schema joins. Reports that need data from several modules are built from a separate read model or through module APIs.

Modules can talk through in-process events: billing publishes PaymentSucceeded and courses unlocks the course. Later this bus is easy to swap for Kafka or RabbitMQ if a module moves out into its own service.

The path to services

Extracting a well separated module into a service is cheap: its API already exists, its data is already apart, it has few dependencies. That is why the modular monolith is often called the best start for future microservices.

Many teams never take that step, because a modular monolith solves the main problem (teams not blocking each other) without new infrastructure. Shopify chose exactly this path for its Rails codebase with millions of lines.

Pros

  • The deploy simplicity of a monolith plus clear boundaries between domains.
  • Teams own modules and conflict less.
  • Calls between modules without a network, transactions where they really matter.
  • A module can become a service later, once there is a real reason.

Cons

  • Needs discipline and tooling that checks boundaries.
  • Deploy and runtime are still shared: one module can take everyone down.
  • Drawing the right boundaries early is hard, you will have to move them.

When to choose it

  • The product is growing, the team is 10 to 40 people, but microservices feel premature.
  • The domain has obvious subdomains (catalog, payments, learning).
  • You want to keep a cheap path to services open for later.

Where it is used

  • Shopify: modularizing a large Rails monolith into components with boundaries enforced by Packwerk.

Tools

Sources

Logos belong to their owners and are used for identification only. Icons: Simple Icons (CC0), Devicon (MIT).