The Modular Monolith

This page explains the core architectural philosophy behind this template.

What is a Modular Monolith?

A modular monolith is an architectural approach that combines the deployment simplicity of a monolith with the organizational benefits of well-defined modules. ThoughtWorks describes it as:

“A set of modules with specific functionality, which can be independently developed and tested, while the entire application is deployed as a single unit.”

Definition and Key Characteristics

A modular monolith is:

  • A single deployable unit that can be built, tested, and deployed as one artifact

  • Organized into domain modules with clear boundaries and responsibilities

  • Built with explicit interfaces between modules rather than implicit dependencies

  • Designed so modules can be independently developed and tested even though they deploy together

Single Deployable Unit, Multiple Domain Modules

You ship one application, but internally it’s organized by business domain:

  • Users and authentication

  • Billing and subscriptions

  • Core product features

  • Notifications and messaging

Each domain is a module with its own models, services, and APIs.

Clear Boundaries, Shared Infrastructure

Modules share infrastructure (database connections, caching, web server) but maintain clear boundaries in business logic. Cross-cutting concerns like authentication, logging, and error handling are handled at the infrastructure level.

Why Not Microservices (Yet)?

Microservices are powerful for large organizations with mature platform teams. For most teams starting out, they introduce complexity that outweighs their benefits.

Distributed Computing Complexity

Distribution should be avoided unless the benefits clearly outweigh the costs. If you can keep it all in one app, you have a much better chance of keeping it all in your head too.

Microservices introduce distributed computing challenges:

  • Network failures between services

  • Data consistency across service boundaries

  • Complex deployment orchestration

  • Service discovery and load balancing

These aren’t insurmountable, but they require investment in tooling and expertise.

Operational Overhead

Running microservices means:

  • Multiple deployment pipelines

  • Distributed logging and tracing

  • Cross-service debugging

  • Version compatibility management

A small team managing a dozen services often spends more time on operations than features.

When Microservices Make Sense

Microservices become valuable when:

  • Different parts of your system need to scale independently

  • Teams are large enough to own separate services

  • You need different technology stacks for different problems

  • Your domain boundaries are well understood and stable

The modular monolith lets you discover these boundaries before committing to distribution.

Why Not a Traditional Monolith?

A traditional monolith without internal structure creates its own problems.

Spaghetti Dependencies

Without clear boundaries, code depends on code arbitrarily. A change in the billing module breaks authentication because someone took a shortcut. The dependency graph becomes a tangled mess—what some call ending up with a “Distributed Monolith” even with code in a single repository.

Difficulty Onboarding New Developers

New team members can’t understand a slice of the system—they have to understand everything to change anything. Onboarding takes weeks, and developers are afraid to make changes.

Scaling Team Becomes Scaling Problems

Adding developers doesn’t increase velocity because everyone is stepping on each other. Merge conflicts are constant. Coordination overhead dominates.

The Best of Both Worlds

The modular monolith gives you the best of both approaches.

Modularity Without Distribution

You get clear boundaries and separation of concerns without the operational complexity of distributed systems. Modules communicate through explicit interfaces, but those calls are in-process, not over the network.

In practice, this means using an in-memory event bus: modules publish domain events when significant things happen, and other modules subscribe to react. The event bus is a simple pub-sub mechanism that routes events to registered handlers, all within the same process.

This is significantly easier to design, deploy, and manage because modules ship together with optimized inter-module communication.

The same event contracts that work in-memory can later be routed through RabbitMQ, AWS SNS, or other message brokers when you need independent scaling or service extraction. Your handlers stay the same. Only the transport changes.

Strong Boundaries Enable Future Extraction

When you do need to extract a service (because it needs independent scaling, or a separate team will own it), you have a clean seam. The module already has a defined interface. Extraction is straightforward, not a multi-month rewrite.

The goal is to identify good architectural boundaries before extracting code into independent services. This sets you up to migrate to microservices in the future if needed.

Grow Your Architecture With Your Team

Start simple. Add modules as your domain expands. Extract services when the organizational or technical need is clear. Your architecture evolves with your business rather than constraining it.

The Modular Monolith is simple in its concepts, but powerful in enabling teams to scale their software.

Supporting Research

This approach draws from several practitioners who’ve written about their experiences:

  • DHH (Basecamp): “If you can keep it all in one app, you have a much better chance of keeping it all in your head too.”

  • Dan Manges (Root Insurance): “We’ve heard of teams ending up with a Distributed Monolith: code in independent services that is as difficult to work with as a Monolith. One underlying cause of that is poor architecture.”

  • ThoughtWorks: The modular monolith is “significantly easier to design, deploy and manage” because modules ship together with optimized inter-module communication.

Further Reading