Engineering

Modular Monolith First: The Architecture Decision Most Teams Get Backwards

Most teams reach for microservices to solve team and scaling problems. They end up with distributed systems problems instead. Here's why modular monolith is the correct default — and how to extract services when they actually earn it.

16 July 2026

Two-phase architecture evolution: a modular monolith with four bounded modules, then a core monolith with one extracted microservice triggered by a real scaling signal

Most teams treating microservices as the default starting architecture are solving a problem they don't have yet — and creating several they didn't anticipate.

The conversation usually starts with good intentions: "We want to scale independently." "We need team autonomy." "We're building for growth." By the time the first real scaling pressure arrives, the team is spending more engineering time on service orchestration, distributed tracing, and cross-service deployments than on the actual product.

This post is going to argue one point directly: the modular monolith is not a stepping stone for teams that haven't graduated to microservices. It is the correct default architecture — and defaulting to microservices before the system earns them is one of the most expensive early decisions a team can make.


The Microservices Default Has a Marketing Problem

Microservices became the consensus architecture around 2015, riding the wave of Netflix, Amazon, and Uber engineering blog posts. The problem: those posts described how billion-scale companies solved billion-scale problems.

Most teams aren't Netflix. They're building products with 5–50 engineers, unknown usage patterns, and the primary challenge of shipping fast enough to find product-market fit before the runway runs out.

Applying Netflix's architecture at series A is the engineering equivalent of buying Formula 1 suspension for your daily commute. The technology is real. The fit isn't.

The costs that rarely appear in the "microservices benefits" list:

  • Distributed systems complexity: Every network call between services is a failure point. Timeouts, retries, circuit breakers, and partial failures become your problem, not the framework's.
  • Operational overhead: Each service needs its own CI/CD pipeline, monitoring, alerting, secrets management, and scaling configuration. A 10-service system is 10× the infrastructure to maintain — and 10× the surface area when something breaks at 2 a.m.
  • Local development pain: Running 8 services to test a feature means a full-day setup job, Dockerfiles, service registries, and environment variable sprawl. npm start turns into docker-compose up plus manual seeding across 4 databases.
  • Cross-service transactions: What used to be a database transaction becomes a distributed saga with compensating logic, event sourcing, or eventual consistency — all of which are genuinely hard to get right and even harder to debug when they go wrong.
  • Debugging across service boundaries: Tracing a bug through 4 services with async messaging between them is orders of magnitude harder than reading a stack trace in a single process.

None of these are unsolvable. But they arrive before the benefits of microservices become real — and most teams underestimate the timing badly.


A Real Example: The Wrong Reason to Decompose

A company we worked with runs an e-learning platform. When we joined, they had already made the switch: they'd taken their original monolith and decomposed it into microservices roughly 18 months earlier.

The reasons were familiar. The engineering team was growing. Different squads were stepping on each other's code. Leadership had read about how microservices enable team autonomy and wanted to adopt the pattern. The decomposition happened quickly — not because the system demanded it, but because the org chart suggested it.

A year and a half in, they were dealing with:

Deployment coupling the monolith never had. Breaking changes in one service's API would silently affect downstream services because contract testing wasn't in place. Deployments had to be carefully sequenced — a "small fix" to the course service required coordinating releases across three teams.

Data consistency failures under normal load. Eventual consistency between the user service and the content service meant that newly registered users would sometimes land on a course page that didn't yet know their subscription status existed. Race conditions that were impossible inside a single database transaction became production incidents. The fix — adding retries and polling — papered over a design problem.

An observability gap that made debugging slow. Pinpointing why a course completion didn't trigger a certificate issuance required tracing through four services. They had no distributed tracing set up, so diagnosing the issue meant adding logs to three services, deploying all three, reproducing the bug, and reading logs across four different dashboards.

Feature velocity that slowed as the team grew. A feature touching user state, course progress, and billing now required coordinating across three teams, three repositories, and three deployment schedules. Microservices were supposed to make teams faster. Instead, the team structure had been encoded into the architecture, and every cross-cutting feature now required cross-team alignment.

The irony: they decomposed to enable team autonomy. They got team interdependence instead.

The problem wasn't that microservices are inherently bad. The problem is that they decomposed before the module boundaries were well understood, and they decomposed everything at once. The modules that became separate services still had logical coupling — they just hid it inside network calls instead of function calls, making it invisible and far more expensive to untangle.


What "Modular" Actually Means

Here's where most of the confusion lives: teams hear "modular monolith" and assume it means a monolith with better folder structure. It isn't.

A modular monolith enforces strict module boundaries inside a single deployable unit:

  • No cross-module database access: The Billing module does not query the users table directly. It calls the Users module's public API and gets back a UserSummary — nothing else.
  • No shared domain objects across boundaries: Each module owns its own models. Notifications does not import User from the Auth module. It receives a NotificationRecipient DTO with exactly the fields it needs.
  • Explicit public interfaces per module: Each module exposes a defined set of use cases. Everything else is internal and invisible to the rest of the system.
  • Independent test suites per module: Each module can be exercised in isolation. If a module test imports from another module's internals, it fails the boundary check.

If you enforce these rules — ideally with architecture tests that fail the build when violated — you get the same logical isolation that microservices give you. Without the network boundary between them.

The key insight is this: the architectural discipline lives at the module boundary, not at the deployment boundary. Microservices enforce the boundary with a network hop. A modular monolith enforces it with an access rule. Both boundaries are real. Only one of them introduces a distributed systems failure surface.

Converting a well-structured module to a microservice later is straightforward: the interface already exists. You're moving a function call across a network, not refactoring tangled code under production load.

Converting a poorly bounded module is painful whether you do it at month three or month twenty. Microservices don't solve the boundary problem. They make it a distributed systems problem.


The Extraction Path

This is what makes the modular monolith a genuinely strategic choice rather than a phase to outgrow.

You don't extract a module to a microservice because it came up in an architecture meeting. You extract it when a specific, real signal says the complexity is earned:

Signal 1 — Independent scaling requirement. One module needs to scale at a fundamentally different rate than the rest of the system. A video transcoding module on an e-learning platform is the obvious example: CPU-intensive, triggered by uploads, completely decoupled from session volume. That's a real case for extraction.

Signal 2 — Independent deployment velocity. One module needs to ship ten times more frequently than the rest. If your recommendation engine is pushing model updates daily and the rest of the system ships weekly, the coupling becomes friction with a measurable cost.

Signal 3 — Team ownership with genuine autonomy requirements. Not "we have multiple teams" — that's solved with clear module ownership inside the monolith. The signal is that a team needs to choose its own runtime, its own tech stack, and deploy without coordinating with anyone. That's a qualitatively different requirement from team coordination.

Signal 4 — Compliance or security isolation. Some modules need infrastructure-level boundary enforcement that can't be achieved inside a shared process — PCI-DSS requirements for payment processing, or HIPAA data residency requirements for health information.

Notice what isn't on this list: "we're growing," "we want to be like Shopify," or "we anticipate needing this later." Growth doesn't automatically require microservices. Shopify ran a monolith for years as a multi-billion-dollar business and has been explicit that modular architecture inside the monolith was central to making that scale.

When a signal is real, the extraction process should be deliberate:

  1. One module at a time. Never decompose in bulk. Each extraction is its own project, its own risk surface, and its own opportunity to get it wrong.
  2. The interface already exists. If you enforced boundaries, the service contract is your existing module public API. You're not designing the interface under pressure — you're promoting one that's already in production.
  3. Run them side by side briefly. Route traffic to the new service while the module still exists in the monolith. Validate behavior under production load, then deprecate the in-process version.
  4. Set up observability before extraction, not after. Distributed tracing, structured logs with correlation IDs, and health endpoints need to exist before the first extraction. Adding them retroactively to a running distributed system is significantly harder.

What You Get With This Approach

For a tech lead making the call on a new system, the benefits that actually matter:

Faster development in the early stages. A single codebase is dramatically faster to work in than a multi-service system. Cross-cutting concerns — auth middleware, error handling, structured logging — are implemented once. Refactoring is local. The feedback loop between change and test is tight.

Dramatically easier onboarding. A new engineer can run the full system with one command. They can understand the system by reading the module structure, not by reverse-engineering a service topology diagram and figuring out which services each local docker-compose file is supposed to represent.

Module isolation you can actually verify. You can write architecture tests that fail if a module bypasses its declared interface. ArchUnit (Java), dependency-cruiser (Node.js), and similar tools let you encode the boundary rules as build checks. The boundaries are enforceable, not just conventional.

A migration path that doesn't hurt. When a module genuinely needs to become a service, the work is scoped and the interface is defined. You're promoting a known contract, not reverse-engineering implicit dependencies from a tangled codebase under time pressure.

Lower operational cost where it counts most. One service, one CI/CD pipeline, one monitoring setup, one set of secrets. The engineer-hours you save on infrastructure go into the product.


Recommendations

If you're starting a new system:

  1. Default to modular monolith. The burden of proof is on microservices, not the monolith. "We might need to scale this independently someday" is not sufficient justification.
  2. Enforce module boundaries from day one using automated checks. Boundaries enforced by convention drift. Boundaries enforced by the build do not.
  3. Write down your extraction criteria before you build anything. What specific, measurable signal would trigger a module becoming a service? Revisit in architecture reviews. If no signal has fired after a year, you've validated the approach.
  4. Don't confuse team structure with deployment structure. A team can own a module inside a monolith with full autonomy over its design, its tests, and its roadmap. You don't need separate deployments for team ownership — you need clear boundaries and a defined interface.

If you're already running microservices and feeling the coordination tax:

Look honestly at which services deploy together, share databases, or require synchronous calls to complete most user-facing operations. Those are candidates for re-consolidation — what the industry has started calling "macro-services" or just honest acknowledgment that the decomposition happened at the wrong granularity. Merging two tightly coupled services is not failure. Keeping them separate because of sunk cost is.


The Honest Tradeoff

Modular monolith isn't the right answer forever for every system. Large-scale products with genuinely independent load profiles, teams that need to choose different tech stacks, or strict regulatory isolation will eventually extract services. That's not a failure of the approach — that's the approach working as designed.

What the modular monolith gives you is the right to make that decision with evidence. You ship fast, accumulate real usage data, understand your actual bottlenecks, and extract exactly what earns the added complexity — one module at a time, with a clean interface already in place.

The teams that start with microservices are betting on their architectural intuitions at the moment of least information. The teams that start with a well-structured modular monolith are making extraction decisions with production data, real usage patterns, and a system that's already running.

Most of the time, the data eventually supports extracting one or two services — not the twelve the initial architecture assumed.

Start modular. Extract deliberately. Scale on signal.