When to Break Up Your Monolith (And Why You Probably Shouldn't Yet)
The impulse to split your monolith into microservices hits every growing startup. Here's the framework I use to decide whether the split is premature or overdue — and the modular monolith pattern that buys you time.
Every startup I advise has the monolith conversation eventually. The codebase has grown, deployments are getting nervous, and someone on the team — usually the most recently hired senior — says the words every CTO dreads: "we should really break this into microservices." Sometimes they are right. Most of the time they are early by 12–18 months and the split would slow the team down instead of speeding it up.
The mistake is not wanting to split. The mistake is splitting before the pain is real, because the cost of microservices is constant and the benefit only appears once specific scaling and organizational problems exist. This article is the framework I use with clients to make the decision, the modular monolith pattern that buys time, and the signals that tell you the split is actually overdue.
The case for staying monolith
A monolith at a 5–20 person startup has genuine advantages that teams forget once microservices hype takes hold.
One deploy, one rollback. You ship the whole application. If something breaks, you roll back the whole application. There is one CI pipeline, one deploy target, one set of logs to search. The operational overhead is minimal, and at a startup where every engineer wears three hats, minimal operational overhead is a strategic advantage.
Shared types and refactors. When everything is in one codebase, you can rename a function and your IDE finds every reference. You can change a type and the compiler catches every mismatch. Cross-cutting changes that would require coordinated deploys across three services take a single PR in a monolith.
Transaction boundaries. A single database means real transactions. You do not need distributed sagas, eventual consistency patterns, or compensation logic. The billing update and the user state change happen atomically or not at all.
Hiring simplicity. Every engineer on the team can work on every part of the product. There are no service-specific silos, no "that's the payments team's service," no coordination overhead for features that span domains.
These are not theoretical benefits. They are real productivity multipliers at the scale where most startups operate. Throwing them away for the architectural prestige of microservices is a bad trade.
The signals that the split is real
There are specific, measurable signals that the monolith is becoming a bottleneck. If none of these are present, the monolith is probably fine.
Deploy contention. Multiple teams need to ship changes but cannot because their changes conflict in the same deploy pipeline. If you are doing trunk-based development with feature flags, this threshold is high. If you are doing long-lived branches, you hit it sooner.
Scaling mismatch. One part of the application needs dramatically different resources than the rest. The API serving layer is CPU-bound and needs 20 instances, but the background job processor is memory-bound and needs 3. Scaling the monolith means scaling everything together, which wastes money.
Blast radius anxiety. A bug in the billing module takes down the authentication flow because they share a process. Teams are afraid to deploy because the failure domain is the entire application.
Organizational boundaries. You have distinct teams that own distinct domains and they are stepping on each other's code. Conway's Law is working against you — the code does not reflect the team structure.
Regulatory or compliance partitioning. PII handling, PCI scope, or data residency requirements mean certain data flows need to be physically separated from the rest of the application.
If you are experiencing two or more of these, the conversation about splitting is worth having. If you are experiencing none of them, the conversation is premature.
The modular monolith: buying yourself time
The modular monolith is the pattern I install most often because it gives teams the organizational benefits of microservices without the operational cost.
The idea: the codebase remains a single deployable unit, but internally it is organized into modules with explicit boundaries. Each module has a public API (a set of functions or types it exports) and private internals that other modules cannot access directly.
Concretely, in a Next.js or Node.js codebase, this looks like:
Directory-per-domain. src/billing/, src/auth/, src/notifications/, each with an index.ts that exports the public API. Internal files are not imported from outside the directory.
Enforced boundaries. A linting rule or an import restriction that prevents src/auth/ from importing anything from src/billing/internal/. The boundary is a compile-time check, not a gentleman's agreement.
Domain-specific types. Each module owns its types. Shared types live in a common module that both can import, but one domain does not reach into another domain's internal types.
Explicit cross-module calls. When billing needs to check auth status, it calls auth.getUser(id) through the public API, not by querying the auth tables directly. This mimics the interface a future service boundary would require.
The payoff: when the time comes to extract a module into a service, the boundaries are already drawn. The public API becomes the service API. The internal implementation becomes the service internals. The extraction is mechanical rather than archaeological.
How I do the extraction when it is time
When the signals are real and the split is justified, I follow a specific sequence:
Step 1: Identify the extraction candidate. Pick the module with the most independence — the fewest inbound dependencies. This is usually a worker or background process (email sending, report generation, event processing) rather than a core domain.
Step 2: Verify the boundary. Audit the module's actual dependencies. If other modules bypass the public API and access internals directly, fix that first. The extraction will fail if the boundary is not clean.
Step 3: Extract the data. If the module has its own database tables, migrate them to a separate database or schema. If it shares tables with other modules, this is the hard part — you need to decide which module owns which tables and create an API for cross-module data access.
Step 4: Extract the process. Move the module into its own deployable unit. The public API becomes an HTTP or gRPC service. Internal communication that was function calls becomes network calls.
Step 5: Run both in parallel. For a period, run the old in-process code path alongside the new service path. Compare results. Route a percentage of traffic to the new service and increase gradually. This catch is essential — it catches bugs that only appear under real traffic patterns.
Step 6: Remove the old code. Once the service is stable, remove the in-process version and route all traffic to the service.
The whole process takes 2–6 weeks per module for a well-prepared codebase. For a codebase without modular boundaries, add 4–8 weeks of preparation work.
The mistakes I see
Splitting too early. The most common mistake, by far. A 10-person team splits into three services and spends the next six months building the infrastructure (service discovery, distributed tracing, API gateways) that they would not need as a monolith.
Splitting along the wrong boundaries. Services should map to business domains, not technical layers. A "database service" and an "API service" and a "frontend service" is not microservices — it is a distributed monolith with network calls instead of function calls.
Shared databases. Two services sharing a database is not two services. It is one service with extra steps. If you split services, split the data.
No observability before splitting. You need distributed tracing, centralized logging, and health checks before you add network boundaries. Adding these after the split means debugging in the dark during the most fragile period.
Nano-services. Splitting too fine. Every function as a service. I have seen codebases with 40 services for a product that could run as 3. The coordination overhead dwarfs any benefit.
The counterpoint: when to split early
There are legitimate cases for early splitting. If your application processes sensitive health or financial data that must be isolated by regulation, the split is compliance-driven and the timing is now regardless of team size. If you have a genuinely CPU-intensive workload (ML inference, video processing) that would starve the rest of the application, an early extraction makes sense. And if you are acqui-hiring a team that brings their own service, integrating at the service boundary is cleaner than merging codebases.
But these are the exceptions. The default should be monolith until the pain is measurable.
Your next step
This week, answer two questions about your codebase. First: do you have any of the five split signals (deploy contention, scaling mismatch, blast radius, org boundaries, compliance partitioning)? If no, put the microservices conversation on hold. Second: does your monolith have clear module boundaries with enforced imports? If no, start drawing them. The modular monolith is the move that gives you options without commitments.
Where I come in
Architecture assessments — monolith, modular monolith, or microservices — are a standard engagement for me, usually as part of a broader technical due diligence or scaling readiness review. Book a call if your team is debating the split and wants an outside perspective grounded in what actually works at your stage.
Related reading: The Seed-Stage Stack · Your Cloud Bill Is a Strategy Document · Postgres Is All You Need
Debating your architecture? Book a call.
Get in touch →