Modular Monolith Architecture

The $50,000 Mistake: Microservices Too Soon

Let me tell you about an expensive lesson. When I started building the POS system, I was fresh off reading about microservices at tech giants. Netflix! Uber! Amazon! They all used microservices, so obviously, my POS system for small restaurants needed them too, right?

Wrong. So incredibly wrong.

For the first three months, my two-person team spent more time:

  • Debugging network calls between services

  • Managing six different deployment pipelines

  • Troubleshooting distributed tracing

  • Coordinating database schemas across services

...than actually building features customers wanted.

One particularly painful day, I spent 6 hours debugging why orders weren't being created. The issue? The Inventory Service was returning a 200 OK status with an error message in the response body, and the POS Core Service wasn't checking the body. In a monolith, this would have been a function call with a clear exception. In microservices, it was a day of my life I'll never get back.

That's when I discovered the modular monolith—the architecture that could have saved me months of frustration.

What Is a Modular Monolith?

A modular monolith is a single deployable application with well-defined internal module boundaries. Think of it as microservices discipline without microservices complexity.

spinner

Key difference from microservices: Everything runs in the same process, uses the same database (or carefully separated schemas), and deploys as one unit. But modules communicate through well-defined interfaces, just like they would in microservices.

The POS System as a Modular Monolith

Here's how I refactored the POS system into a modular monolith:

Project Structure

Core Application Setup

Shared Configuration

Module Example: Auth Module

Module Example: POS Core Module

Internal Event Bus

Tenant Isolation Middleware

Benefits of Modular Monolith

1. Simpler Deployment

2. Easier Debugging

3. Lower Operational Cost

Modular Monolith:

  • 1 server (can scale vertically)

  • 1 database

  • 1 deployment pipeline

  • 1 set of logs to monitor

Microservices:

  • 6+ servers (need orchestration)

  • 6 databases (or complex shared DB)

  • 6 deployment pipelines

  • 6 sets of logs (need aggregation)

  • Service mesh for communication

  • Distributed tracing infrastructure

4. ACID Transactions

When to Split Into Microservices

Here are the signals that told me it was time to split:

Signal 1: Team Scaling

Signal 2: Different Scaling Requirements

Signal 3: Different Technology Needs

Migration Pattern: Strangler Fig

When we eventually split into microservices, we used the Strangler Fig pattern:

spinner

Key Learnings

  1. Start with modular monolith, not microservices

    • Easier to refactor modules than distributed services

    • Can split later when you have real data about bottlenecks

  2. Well-defined module boundaries are critical

    • Each module should have clear responsibilities

    • Communication through interfaces (service layer)

    • No direct database access across modules

  3. Internal API discipline prepares for microservices

    • Design modules like they're separate services

    • Makes actual split much easier later

  4. Events enable loose coupling within monolith

    • Modules react to events instead of direct calls

    • Easier to extract into message-based microservices

  5. Operational simplicity has real value

    • Less infrastructure = less to break

    • Faster development = more features shipped

Common Mistakes

  1. Tight coupling between modules

  2. Shared database without boundaries

  3. Skipping the modular monolith phase

    • Jumping straight to microservices is rarely justified

    • Build modular monolith first, prove the need for split

  4. Not planning for eventual extraction

    • Design like each module could become a service

    • Avoid shared state that would be hard to split

When to Use Modular Monolith

Use Modular Monolith When:

  • Team size < 10-15 developers

  • Traffic can be handled by vertical scaling

  • Rapid iteration is priority

  • Operational simplicity matters

  • You're not sure about service boundaries yet

Consider Microservices When:

  • Multiple independent teams (15+ developers)

  • Clear scaling bottlenecks in specific modules

  • Need technology diversity (different DBs, languages)

  • Independent deployment is critical

  • Can handle operational complexity

Next Steps

Now that you understand how to build a well-structured modular monolith, the next article explores multi-tenant architecture patterns—how to securely isolate data for different customers within the same system.

We'll cover:

  • Tenant isolation strategies (separate DB vs shared DB)

  • How the x-tenant-id flows through all modules

  • PostgreSQL row-level security

  • Real bug: the cross-tenant data leak I caused

Next Article: 03-multi-tenant-architecture-patterns.md - Learn how to build secure multi-tenant systems that prevent data leaks and maintain performance at scale.


Remember: The modular monolith is not a compromise or a stepping stone. For many systems, it's the optimal architecture. Don't let FOMO drive you to microservices before you've exhausted simpler options.

Last updated