Microservices & Distributed Systems

Microservices vs Monolith: When to Split

MatterAI
MatterAI
13 min read·

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:

  1. Independent scaling: one workload needs 100 replicas while another needs 2. Separate deploys let each scale independently.
  2. Independent deploy cadence: a team ships 10 times a day while another ships weekly. Separate deploys remove the coupling.
  3. Isolation of failure: a crash in one service must not take down the whole product.
  4. Team autonomy at scale: 50+ engineers on one codebase create merge conflicts and release trains. Service ownership restores autonomy.
  5. Different tech stacks: genuinely justified rarely, but real (e.g., a Go service for high-throughput ingestion next to a Node API).
  6. Compliance or security boundaries: PCI data isolated in its own service with its own access controls.

The Decision Framework

Ask these questions in order:

QuestionIf 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

Get started free - https://app.matterai.so


Follow us on X · LinkedIn · GitHub

Share this Guide:

Ship Faster. Ship Safer.

Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.

No credit card requiredSOC 2 Type IISetup in 2 min