All posts
ArchitectureMay 26, 2026·7 min read

Monolith vs Microservices for Node.js Apps

The microservices hype has cooled. When does a Node.js monolith make more sense, when should you decompose, and what does a modular monolith look like in practice?

The default should be a monolith — here's why

The software industry spent a decade hyping microservices, and the pendulum is swinging back. Amazon, Netflix, and Google run microservices because they have thousands of engineers, complex organizational structures, and traffic patterns that require independent scaling. Most applications — including many that serve millions of users — don't have these constraints.

A monolith is simpler to develop (no network calls between services), simpler to test (run the whole app locally), simpler to debug (one log stream, one trace), simpler to deploy (one artifact), and simpler to operate (one monitoring target). Start there and migrate when you have evidence that a monolith is causing a specific, measurable problem.

The real cost of microservices

Every microservice boundary you introduce adds:

  • Network latency — A function call takes nanoseconds. A network call takes milliseconds. At 10 services in a chain, you've added 10–100ms of minimum latency.
  • Partial failure modes — What happens when service B returns a 503? You need circuit breakers, retry logic, fallback behavior. In a monolith, a function can't "be down".
  • Distributed transactions — ACID transactions across service boundaries require sagas or two-phase commit. Neither is simple.
  • Distributed tracing — A request that crosses 5 services requires trace context propagation and a distributed tracing system to understand.
  • Per-service everything — Deployment pipeline, monitoring dashboard, alert rules, oncall runbook, scaling policy. Multiply by the number of services.
  • Data consistency — Each service owns its database. Joining data across services requires either API calls, event-driven eventual consistency, or a reporting database.

These costs are real and ongoing. The benefits — independent deployability, independent scalability, team autonomy — are also real, but they only pay off at a certain organizational and traffic scale. That scale is much higher than most teams think.

When microservices make sense

There are legitimate reasons to decompose. The key is having a specific, measurable problem:

  • Team coordination friction — 20+ engineers constantly stepping on each other in the same codebase, with deployment conflicts blocking releases multiple times per week
  • Wildly different scaling requirements — A video transcoding component needs to scale 100x during evenings while your API needs 2x. In a monolith you'd be scaling the entire app for one use case.
  • Different reliability requirements — Your payment processing cannot be affected by your analytics service going down. Hard process isolation is the right tool.
  • Different technology requirements — A computer vision model that must run Python can reasonably be a separate service from your Node.js API.
  • Compliance boundaries — PCI DSS requires that cardholder data be isolated. This sometimes genuinely requires service separation.

The modular monolith: the best starting point

The modular monolith gives you clean architecture and the option to extract services later, without paying the operational cost upfront. The key rule: modules communicate through explicit interfaces, never through direct database access or function calls that bypass the module boundary.

src/
  modules/
    users/
      index.ts          // Public interface — what other modules can use
      users.service.ts  // Business logic — private
      users.repo.ts     // Database access — private
      users.router.ts   // HTTP routes — private
      users.events.ts   // Events this module emits
      users.types.ts    // Types exported in the public interface
    billing/
      index.ts
      billing.service.ts
      billing.repo.ts
      billing.router.ts
    notifications/
      index.ts
      notifications.service.ts
  lib/
    event-bus.ts        // In-process event emitter for inter-module communication
    db.ts
  app.ts
// modules/users/index.ts — public interface only
export { UserService } from './users.service';
export type { User, CreateUserInput } from './users.types';
// users.repo.ts is NOT exported — it's an internal implementation detail

// modules/billing/billing.service.ts — communicates through the event bus
import { eventBus } from '@/lib/event-bus';

eventBus.on('user.created', async ({ userId }) => {
  await billingService.createBillingAccount(userId);
});

// ✗ Don't do this — direct database access across module boundaries
import { prisma } from '@/lib/db';
const user = await prisma.user.findUnique(...); // billing module shouldn't query users table directly

When you do extract a service: the strangler fig pattern

Don't attempt a big-bang rewrite. Use the strangler fig pattern: run the new service alongside the monolith, route traffic gradually, and remove the monolith code once the service is proven stable.

// Step 1: Add a feature flag to route to new service
async function getUser(userId: string): Promise<User> {
  if (featureFlags.useUserService) {
    return userServiceClient.getUser(userId);
  }
  return userRepository.findById(userId); // Monolith code path
}

// Step 2: Gradually enable the flag (10% → 50% → 100%)
// Step 3: Remove monolith code path once stable at 100%

The Netflix/Amazon fallacy

The most common microservices antipattern is copying architecture from companies that operate at 1000x your scale. Netflix has hundreds of dedicated platform engineers maintaining their service mesh, observability stack, and deployment infrastructure. That's the cost of their architecture at scale. At your scale, those same engineers would be building features.

Copy principles, not architectures. The principles microservices encode — loose coupling, high cohesion, independent deployability — can all be applied in a well-structured monolith. The architecture follows naturally when your organization genuinely needs it.

Ready to put this into practice?

Deploy your Node.js app to production in minutes — zero YAML, automatic CI/CD, and HTTPS included.