Microservices vs Monolith: When to Split
Microservices vs Monolith: When to Split
The microservices vs monolith debate is usually settled by fashion, not engineering. The honest answer: most teams should start with a monolith, and most teams that split should have waited. This guide gives you the decision framework, the real costs of distribution, and the safe path when a split is genuinely justified.
The Real Costs of Microservices
Microservices trade a simple problem (one codebase, one deploy) for a hard one (distributed systems). The costs are not theoretical:
- Network failures: every service call can fail, time out, or return garbage. You now need retries, timeouts, circuit breakers, and idempotency.
- Data consistency: no more transactions across tables. You need sagas, outbox patterns, and eventual consistency.
- Operational complexity: N services means N deploys, N log streams, N dashboards, N alerting rules.
- Debugging: a request now spans multiple services, each with its own logs and traces.
- Team coordination: API contracts become political artifacts.
The rule of thumb: a microservice should be justified by a concrete benefit, not by architecture aesthetics.
When a Monolith Is the Right Answer
A monolith is the right answer for:
- Startups and small teams: speed of iteration beats everything else.
- Early-stage products: the domain boundaries are not yet known; a monolith lets you discover them.
- Teams under 10-15 engineers: the coordination overhead of microservices exceeds the benefit.
- Simple domains: CRUD apps do not need distributed systems.
The modular monolith is the modern compromise: one deployable, but with hard module boundaries enforced by the compiler or linter.
// A modular monolith — boundaries enforced in code, not in deploys
// modules/orders/index.ts
export { OrdersService } from "./orders.service";
export type { Order, OrderStatus } from "./order.model";
// modules/orders/orders.service.ts — internal, not exported
# Enforce boundaries with a lint rule
npx eslint --rule 'no-restricted-imports: ["error", { "patterns": ["modules/orders/*"] }]' src/modules/payments
When Microservices Are Justified
Microservices earn their cost in specific situations:
- Independent scaling: one workload needs 100 replicas while another needs 2. Separate deploys let each scale independently.
- Independent deploy cadence: a team ships 10 times a day while another ships weekly. Separate deploys remove the coupling.
- Isolation of failure: a crash in one service must not take down the whole product.
- Team autonomy at scale: 50+ engineers on one codebase create merge conflicts and release trains. Service ownership restores autonomy.
- Different tech stacks: genuinely justified rarely, but real (e.g., a Go service for high-throughput ingestion next to a Node API).
- Compliance or security boundaries: PCI data isolated in its own service with its own access controls.
The Decision Framework
Ask these questions in order:
| Question | If yes, lean toward... |
|---|---|
| Team smaller than ~15 engineers? | Monolith |
| Domain boundaries still unclear? | Monolith |
| One workload needs independent scaling? | Microservices |
| Teams need independent deploy cadence? | Microservices |
| Failure isolation is a hard requirement? | Microservices |
| Compliance requires hard data boundaries? | Microservices |
| None of the above? | Modular monolith |
How to Split Safely
If you split, do it incrementally. The strangler fig pattern: extract one service at a time, keeping the monolith healthy throughout.
Step 1: Find the seam. Look for modules with stable boundaries, few cross-dependencies, and clear ownership:
# Find the module with the fewest cross-module imports
grep -r "from '../" src/modules/ | awk -F: '{print $1}' | sort | uniq -c | sort -n
Step 2: Extract the data first. The database is the hardest coupling. Move the service's tables behind an API before moving the code:
Before: monolith ──► orders table (shared DB)
After: monolith ──► orders-service API ──► orders DB (owned)
Step 3: Extract the code. Move the module to a new service, expose it over HTTP or gRPC, and point the monolith at the new endpoint.
Step 4: Handle consistency. Replace the shared transaction with a saga or outbox pattern:
// Outbox pattern — write the event in the same transaction as the state change
await db.transaction(async (tx) => {
await tx.order.update({ id, status: "paid" });
await tx.outbox.create({ type: "order.paid", payload: { id } });
});
// A relay worker publishes outbox rows to the message broker
Step 5: Add the distributed systems toolkit. Retries, timeouts, circuit breakers, and tracing are not optional:
// Circuit breaker around a service call
const breaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeoutMs: 30_000,
});
async function callOrdersService(payload: unknown) {
return breaker.execute(() =>
fetch("http://orders/orders", {
method: "POST",
body: JSON.stringify(payload),
signal: AbortSignal.timeout(2_000),
}),
);
}
The Anti-Patterns
- Splitting for "scalability" you do not have: if you are not at the scale where one service needs independent scaling, you are paying for nothing.
- Splitting by technical layer (frontend, backend, DB): this creates distributed monoliths with all the cost and none of the benefit.
- Splitting without data ownership: if services share a database, you have not split anything.
- Splitting to use a new tech stack: the stack is rarely the bottleneck.
Implementation Checklist
- Confirm a concrete benefit justifies the split
- Start with a modular monolith and enforced boundaries
- Extract data ownership before code
- Split one service at a time (strangler fig)
- Add outbox/saga for consistency
- Deploy retries, timeouts, circuit breakers, tracing
- Keep the monolith healthy during extraction
- Revisit the decision: is the split paying for itself?
MatterAI builds frontier AI infrastructure for engineering teams — from inference-optimized models to autonomous coding agents and agentic code reviews.
Explore what we're building:
- Orbital IDE — Autonomous AI coding agent with background agents and deep codebase memory
- AI Code Reviews — Agentic pre-commit reviews across GitHub, GitLab, and Bitbucket
- Axon Models — Frontier-grade reasoning models at 70% lower inference cost
Share this Guide:
More Guides
Model Context Protocol (MCP): Building MCP Servers from Scratch
Build production-grade MCP servers with the TypeScript and Python SDKs. Covers the MCP architecture, stdio and HTTP transports, tools, resources, prompts, and the security model every AI application needs.
16 min readRAG vs Fine-Tuning: When to Use Each for Your LLM Application
Decide between retrieval-augmented generation and fine-tuning with a practical decision framework. Compare cost, latency, freshness, and accuracy, and see working implementations of both approaches.
13 min readGrafana Stack vs SigNoz: Choosing an OpenTelemetry-Native Observability Platform
Compare the Grafana LGTM stack (Loki, Tempo, Mimir) with SigNoz for OpenTelemetry observability: architecture, storage engines, operational overhead, cost at scale, and migration paths.
10 min readLLM Fine-Tuning: LoRA vs QLoRA vs Full Fine-Tuning
Compare full fine-tuning, LoRA, and QLoRA for LLMs with memory requirements, training recipes, and code. Learn which method fits your GPU budget, dataset size, and quality requirements.
14 min readPrompt Injection Defense: Securing LLM Apps Against Jailbreaks, RAG Poisoning, and Tool-Use Exploits
Defend production LLM applications against direct jailbreaks, indirect injection via RAG pipelines, and tool-use exploits with layered defenses, input filtering, and least-privilege tool design.
10 min readContinue Reading
Model Context Protocol (MCP): Building MCP Servers from Scratch
Build production-grade MCP servers with the TypeScript and Python SDKs. Covers the MCP architecture, stdio and HTTP transports, tools, resources, prompts, and the security model every AI application needs.
16 min readRAG vs Fine-Tuning: When to Use Each for Your LLM Application
Decide between retrieval-augmented generation and fine-tuning with a practical decision framework. Compare cost, latency, freshness, and accuracy, and see working implementations of both approaches.
13 min readGrafana Stack vs SigNoz: Choosing an OpenTelemetry-Native Observability Platform
Compare the Grafana LGTM stack (Loki, Tempo, Mimir) with SigNoz for OpenTelemetry observability: architecture, storage engines, operational overhead, cost at scale, and migration paths.
10 min readShip Faster. Ship Safer.
Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.
