The Modular Monolith Advantage
A practical guide to scaling teams and backends in one codebase
Monolith or microservices?
Ask any technical leader and you’ll hear passionate arguments for both. After working at three fast-growing startups that became unicorns in no time, I’ve learned this: most successful products begin as monoliths. Starting with a monolith is usually the right call, at least until growth forces your hand.
But once your product takes off, everything shifts. One day you’re building a new API. The next, your API is melting down under a tidal wave of signups you didn’t see coming. Growth brings new problems: performance issues, slow build times, tightly coupled code, and engineers tripping over each other in the same codebase.
The usual advice? Split everything into microservices. But before you hire an army of SREs and watch your Series A or B funding vanish into your AWS bill, consider another option: the modular monolith.
What is a Modular Monolith?
A modular monolith is a single application organized into clear, self-contained modules, each focused on a distinct part of the business. These modules have well-defined boundaries, explicit responsibilities, and communicate through intentional interfaces. Everything runs and is deployed together, but the internal structure is clean and deliberate. This approach helps keep complexity under control as your codebase grows.
The Architectural Spectrum
Think of modular monoliths as the middle ground between the two extremes most teams debate:
Classic Monolith: Everything tightly coupled, all code intertwined. Changes in one area can break another.
Modular Monolith: One deployable app, but with clear module boundaries and minimal dependencies between them.
Microservices: Each business capability in its own independent service, running as a separate process, often with its own database.
A City, Not a Maze
Think of your system as a city. At first, it’s a bustling town square: everyone is packed into the same space, sharing roads and utilities.
As the city grows, you carve out neighborhoods, each with its own vibe and borders. Everyone still lives within the same city limits, but life becomes more organized and manageable.
Eventually, some neighborhoods grow large enough to become their own towns with separate city halls and infrastructure.
That’s how engineering teams evolve: from a simple monolith, to a modular monolith, and finally to a microservices architecture.
Pros and Cons of Modular Monoliths
Building a modular monolith is a popular choice for teams seeking structure and agility without diving headfirst into the complexity of microservices. But like any architectural decision, it comes with its own set of trade-offs. Let’s take a clear-eyed look at both sides.
Pros
Simple Deployment: You ship everything as a single unit. No juggling dozens of deployment pipelines or keeping service versions in sync.
Clearer Code Organization: Modules bring structure and discipline. Teams can work on different business areas without stepping on each other’s toes.
Easier Refactoring: Need to make a sweeping change? Internal module boundaries are easier to shift than a constellation of microservices.
Lower Operational Overhead: One runtime, one set of logs, one monitoring setup. You avoid the overhead of running a distributed system.
Natural Path to Microservices: If you ever need to split out a module, the clear boundaries you set today make tomorrow’s migration less painful.
Cons
Difficult to Define Module Boundaries: Getting boundaries right is hard. It often takes several iterations and real-world use before your modules feel natural. Shifting these lines later can be disruptive.
Performance Overhead: Isolating modules can mean more API calls and less efficient data access. Without careful design, what used to be a fast database query can become a slow, chatty operation.
Limited Scalability and Deployment Flexibility: Since everything is deployed as a single unit, you can’t scale or deploy modules independently. When certain parts of your system outgrow the rest, you may eventually hit a wall.
Modular monoliths offer a practical blend of structure and simplicity, giving you many of the benefits of microservices without the early complexity or cost. As with any software architecture or system design, long-term success depends on discipline, clarity, and knowing when to adapt.
Designing a Modular Monolith
Designing a modular monolith isn’t just about splitting code into folders. It takes intention, discipline, and smart strategies to keep your system manageable and ready for the future. Let’s break it down into three critical areas: boundaries, communication, and data.
Modules Definition: Drawing the Right Boundaries
Getting boundaries right is everything. Start with your business domains. Use Domain-Driven Design (DDD) techniques to map each core business capability to its own module. For example, keep billing, user management, and inventory as separate units, not simply as technical layers.
Avoid the temptation to cut corners. Each module should own its functionality end to end, including data, logic, and interfaces. The closer your modules reflect real-world business boundaries, the healthier your system will be as it grows.
Review boundaries regularly. As your understanding deepens or requirements shift, do not hesitate to refactor and move responsibilities around. It is better to evolve early than to let messy boundaries fester.
Communication Strategies: Letting Modules Talk
How modules communicate matters. There are two main approaches:
Synchronous Communication: Module API Calls
Modules usually call each other’s functions or methods directly within the same application, using in-process APIs or service interfaces instead of network or REST calls. This is simple and gives immediate responses, but it can increase coupling and make error handling harder as dependencies grow.Asynchronous Communication: Internal Event Bus
Modules can also use an internal event bus that works within the same application, often in-memory. Examples include Node.js EventEmitter, .NET’s MediatR, or Phoenix.PubSub in Elixir. This adds flexibility, since modules can publish and listen for events independently, but it can make debugging and ensuring consistency more difficult.
Choose your strategy based on the needs of each interaction. In practice, most modular monoliths use a mix of both.
Data Isolation: Deciding on Database Architecture
Data ownership is a cornerstone of good modular design. Your choices here affect maintainability, scalability, and how easily you can move to microservices in the future.
There are several levels of data isolation, each with increasing complexity and cost:
Separate Tables: Each module owns its own tables in a shared database. This is the simplest and least costly approach, but there’s still a risk of accidental cross-module queries.
Separate Schemas: Each module gets its own database schema. This creates stronger boundaries, helps prevent data leaks, and simplifies migrations, though it adds some overhead compared to shared tables.
Separate Databases: For maximum isolation, give each module a dedicated database. This approach significantly increases operational complexity, but makes it much easier to extract modules to microservices later.
Different Persistence Technologies: Some modules may benefit from specialized data stores such as key-value, time-series, or document databases. Using the best technology for each module is fine, as long as you keep boundaries clear and avoid coupling through the database.
Pick the level of isolation that fits your team’s size, maturity, and future plans. The more isolation you build in now, the easier it will be to extract modules later.
When (and When Not) to Use a Modular Monolith
Modular monoliths aren’t a universal solution, but for many projects they hit the sweet spot between chaos and complexity. The trick is knowing when they’re a good fit, and when it’s time to look elsewhere.
When to Consider a Modular Monolith
Your Domain Is Too Complex for a Classic Monolith: If your business logic sprawls across multiple concerns and teams keep bumping into each other, a simple monolithic codebase won’t scale. Modular monoliths help organize and manage that complexity.
You Have Multiple Teams Working on the Solution: As soon as you have several teams, modularity helps avoid merge chaos, code collisions, and ownership confusion. Teams can move faster and actually take responsibility for their modules.
You Want an Evolutionary Path Toward Microservices: If you think microservices might be in your future but don’t want the pain and overhead today, start modular. Building a well-structured modular monolith gives you the flexibility to evolve and split out services when you really need to.
When to Rethink Using a Modular Monolith
You Need Independent Scaling or Deployments Now: If parts of your system must scale or deploy independently right away, a single deployable unit will get in your way.
You Have a Polyglot Tech Stack or Specialized Persistence Needs: If your domains demand different programming languages or databases, trying to force them all into one app adds unnecessary friction.
You Already Have Mature, Well-Defined Boundaries: If your domains are stable, teams are fully autonomous, and boundaries are set in stone, jumping directly to microservices may make sense.
Modular monoliths offer clarity, structure, and a natural path to growth. You avoid operational overhead until you truly need it.
From Modular Monolith to Microservices
No architecture lasts forever. Even the best modular monolith can outgrow itself. The good news: if you’ve built strong boundaries and discipline into your modules, migrating to microservices is far less daunting.
Signs It’s Time to Split the Monolith
Bottlenecks and Slow Deployments: Teams are waiting on each other to ship changes, and deployments feel risky.
Scaling Challenges: One part of your app is struggling under heavy load, but you can’t scale it independently.
Team Autonomy: Teams want to move at their own pace, deploy on their own schedule, or experiment with new technologies.
Reliability and Isolation: A bug or outage in one domain can bring down the entire system or create undesirable side effects.
How to Extract Modules Into Services
Pick a Candidate Module: Start with a well-defined module that has clear responsibilities and stands out as a good candidate for extraction. This is often the largest or most complex module, like Module C in the first diagram. Prioritizing the most critical module delivers the biggest impact and helps validate your migration process.
Decouple Communication: Change internal module calls to explicit REST APIs or event-driven patterns. After extraction, the new service (Service C) exposes its own API and interacts with the system through REST or a distributed event bus. At this point, the event bus is external and is often referred to as a message broker or streaming platform. Technologies like Kafka, RabbitMQ, or NATS support communication between independently deployed services.
Migrate Data Ownership: Move the module’s data to its own storage. In the diagram, Service C manages its data in a dedicated NoSQL database, such as MongoDB or DynamoDB. The other modules continue using the original SQL database, like PostgreSQL or MySQL, each with its own schema. This separation lets each service choose the database that best fits its needs.
Set Up Separate Deployment: Deploy the new service independently, giving it its own runtime, deployment pipeline, and monitoring. This allows you to update and scale the service separately, while the rest of the monolith continues to run as before.
Iterate Gradually: Migrate modules in small, deliberate steps rather than all at once. Use lessons from your first extraction to improve future migrations. Let actual pain points and business priorities guide which modules become services next. This approach reduces risk and helps your architecture evolve naturally alongside your team.
A modular monolith is not a dead end; it is a launchpad. The better you design today, the less painful migration will be tomorrow. Take it one step at a time, and let business needs, not hype, set the pace.
Best Practices and Common Pitfalls
A modular monolith thrives on discipline and clarity. Done well, it delivers years of maintainability. Take shortcuts, and you’re back to a tangled mess. Here’s how to get it right, and the traps to avoid.
Best Practices
Enforce Real Module Boundaries: Use strong interfaces and keep shared code to a minimum. Regularly review dependencies to spot and eliminate accidental coupling. Tools like Boundary for Elixir and Packwerk for Ruby can help enforce modular boundaries automatically.
Document and Communicate: Every module should have clear documentation and an explicit owner. Make sure all teams understand how modules interact and where responsibilities begin and end. Use CODEOWNERS files to assign explicit owners to modules, directories, or files.
Visualize and Monitor Complexity: Use module dependency graph tools like Madge for JavaScript/TypeScript or SonarQube for multiple languages to see how your modules interact. If you notice too many dependencies pointing to a single module, it’s a sign to revisit your boundaries and keep the architecture healthy.
Common Pitfalls
Making Modules Too Small: Splitting code into tiny pieces adds overhead and confusion. The solution? Use Bounded Contexts. Each module should represent a meaningful business capability.
Teams Not Prioritizing the Work: Modularization or migration only succeeds when every team is actively involved and committed. If teams aren’t engaged or don’t see this work as a priority, the effort will stall.
No Leadership Buy-in: Without real commitment from senior leadership, modularization will eventually halt. This work often means prioritizing technical changes over new features, at least temporarily. If leadership isn’t on board, wait.
A bit of structure, shared understanding, and honest measurement will keep your modular monolith clean, and ready for whatever comes next.
Real-World Modular Monoliths
You don’t have to take this on faith. Some of the world’s most successful tech companies have embraced the modular monolith pattern to balance growth and maintainability. Here are three popular ones.
Shopify
Shopify runs a massive Rails-based monolith with over 2.8 million lines of Ruby and 500,000 commits as of September 2020. The codebase is carved into modular “components” to manage complexity and accelerate delivery. The team enforces clear boundaries, enables selective testing, and streamlines onboarding.
Basecamp
Basecamp champions the “majestic monolith” since 2003, organizing its codebase by feature and responsibility, resisting distributed systems until truly necessary. Their philosophy keeps abstractions minimal and architecture clear, as detailed by DHH in “The Majestic Monolith”.
GitLab
GitLab maintains a modular monolith containing over 2.2 million lines of code. They’re actively modularizing it using tools like Packwerk to define bounded contexts, enforce boundaries, and prepare for future service extraction. This disciplined architecture boosts engineer productivity, enhances code clarity, and enables gradual decoupling.
If modular monoliths can work for companies at this scale, there’s a good chance they can work for you.
Final Thoughts: Build for Today, Plan for Tomorrow
Software architecture decisions are often less certain than we admit. Modular monoliths show that simple, disciplined solutions can be more effective than chasing trends. They help teams manage complexity, organize code, and focus on delivering real value instead of hype.
Every system grows and changes in ways you can’t fully predict. The most resilient architectures leave space for learning and adaptation. Modular monoliths let you build something strong today while keeping options open for tomorrow.
In the end, good architecture is less about grand plans and more about everyday discipline. It comes down to how your team communicates, how often you revisit boundaries, and how you handle the small decisions that add up over time.
If your system feels unwieldy, you may not need to start over. You might just need clearer boundaries, better habits, and the patience to improve things one step at a time.
Build with intention. Refactor with care. Your future self and your team will thank you.
Thanks for reading The Engineering Leader. 🙏
If you enjoyed this issue, tap the ❤️, share it with someone who'd appreciate it, and subscribe to stay in the loop for future editions.
👋 Let’s keep in touch. Connect with me on LinkedIn.
I like to much this articule!
Easy to read and understand
I enjoy this kind of articles because you get into the details without getting lost in minutia.