Home JavaScript Microservices Architecture in Node.js
Advanced 8 min · 2026-07-12

Microservices Architecture in Node.js

Microservices in Node.js: architecture patterns, inter-service communication, API gateways, service discovery, database per service, and production deployment considerations..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

Microservices architecture decomposes an application into independently deployable services, each with its own database and API. In Node.js, microservices communicate via HTTP/REST, gRPC, or message q

✦ Definition~90s read
What is Microservices Architecture in Node.js?

Microservices architecture decomposes an application into independently deployable services, each with its own database and API. In Node.js, microservices communicate via HTTP/REST, gRPC, or message queues. Key patterns include the API Gateway (single entry point routing requests to services), service discovery (consul, etcd, or DNS-based), database per service (avoiding shared databases), and circuit breakers (preventing cascading failures).

Think of a monolithic app as a single food truck that serves everything: burgers, sushi, tacos, and ice cream.

Cross-cutting concerns include distributed tracing (OpenTelemetry), centralized logging, and health check endpoints. Production patterns include using Docker Compose or Kubernetes for orchestration, implementing retries with exponential backoff for inter-service calls, and maintaining backward compatibility in service APIs.

Plain-English First

Think of a monolithic app as a single food truck that serves everything: burgers, sushi, tacos, and ice cream. If the ice cream machine breaks, the whole truck shuts down. Microservices are like a food court: each stall has its own kitchen, menu, and staff. If the sushi stall runs out of rice, the burger joint next door keeps serving. You can upgrade the taco stand without closing the whole court. Each service is independent, communicates via simple orders (APIs), and can be scaled or fixed without affecting the others.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Your Node.js monolith handles authentication, payments, email, search, and user management. A bug in the email module crashes the entire application, taking payments and search offline with it. Microservices isolate failures, enabling independent deployment, scaling, and technology choices for each service. But microservices also introduce complexity: network latency, data consistency, service discovery, and distributed debugging. This article covers when microservices make sense, how to split a Node.js monolith, inter-service communication patterns, and the production infrastructure you need before migrating.

Why Microservices in Node.js?

Monoliths work until they don't. When your Node.js application grows beyond a few dozen endpoints, deployment coupling, scaling inefficiencies, and team coordination bottlenecks emerge. Microservices decompose the monolith into independently deployable services, each owning a bounded context. Node.js is a strong fit due to its non-blocking I/O, lightweight process model, and rich ecosystem for HTTP and message-based communication. However, microservices introduce distributed system complexity: network latency, partial failures, data consistency, and observability overhead. The decision to adopt microservices should be driven by organizational scalability and deployment autonomy, not by hype. Start with a modular monolith and extract services only when the pain justifies the cost.

monolith-vs-microservice.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Monolith: single Express app
const express = require('express');
const app = express();
app.use('/users', require('./routes/users'));
app.use('/orders', require('./routes/orders'));
app.listen(3000);

// Microservice: separate process for users
const express = require('express');
const app = express();
app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id);
  res.json(user);
});
app.listen(3001);
Output
Monolith runs on port 3000; user service runs on 3001.
Try it live
🔥When to Extract
Extract a service when a domain module has high change velocity, requires independent scaling, or needs a different data store. Premature decomposition adds accidental complexity.
📊 Production Insight
In production, a premature microservice split caused a 3x increase in request latency due to added network hops. We re-merged two services until the team size justified the split.
🎯 Key Takeaway
Microservices solve organizational scaling and deployment coupling, not technical problems.
microservices-nodejs THECODEFORGE.IO Node.js Microservices Architecture Layers Tiered components from client to data storage Client Layer Web App | Mobile App | External API API Gateway Express Gateway | Kong | Nginx Service Layer User Service | Order Service | Payment Service Communication Layer REST/HTTP | gRPC | Message Broker (RabbitMQ) Data Layer PostgreSQL | MongoDB | Redis Cache Observability Layer ELK Stack | Prometheus | Jaeger THECODEFORGE.IO
thecodeforge.io
Microservices Nodejs

Service Boundaries and Domain-Driven Design

The hardest part of microservices is defining service boundaries. Domain-Driven Design (DDD) provides tactical patterns: bounded contexts, aggregates, and domain events. Each microservice should own a bounded context — a cohesive subdomain with its own data model and logic. For example, an e-commerce system might have separate services for Catalog, Cart, Orders, and Payments. The key is to minimize cross-service communication: if two contexts frequently need synchronous calls, they might belong together. Use event-driven communication for eventual consistency across contexts. Define aggregates as consistency boundaries within a service; never span aggregates across services. A common mistake is splitting by technical layers (e.g., a 'data service' or 'logic service') — this creates chatty, fragile systems.

bounded-context.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Order service - owns order data and logic
class OrderService {
  async createOrder(userId, items) {
    const order = new Order({ userId, items, status: 'pending' });
    await this.orderRepo.save(order);
    await this.eventBus.publish('order.created', { orderId: order.id });
    return order;
  }
}

// Payment service - subscribes to order.created
class PaymentService {
  constructor() {
    this.eventBus.subscribe('order.created', async (event) => {
      await this.processPayment(event.orderId);
    });
  }
}
Output
Order service publishes event; Payment service reacts asynchronously.
Try it live
⚠ Anemic Domain Model
Avoid services that are just CRUD wrappers around a database. Each service should encapsulate business logic and enforce invariants.
📊 Production Insight
We once split by 'data service' and 'business logic service' — the result was a distributed monolith with 10x the network calls. Redesigning around bounded contexts cut latency by 80%.
🎯 Key Takeaway
Bound services by business capability, not technical layers.

Inter-Service Communication: REST, gRPC, and Message Queues

Services need to talk. The three main patterns are synchronous (REST, gRPC) and asynchronous (message queues, event streams). REST is simple and ubiquitous but suffers from chatty interfaces and tight coupling. gRPC offers typed contracts, streaming, and better performance via HTTP/2 and protobuf — ideal for internal high-throughput calls. Message queues (RabbitMQ, Kafka) decouple services and enable event-driven architectures. Choose synchronous calls for queries that need immediate consistency (e.g., 'get user profile'). Use async for commands that can tolerate eventual consistency (e.g., 'place order'). Always implement circuit breakers and retries with exponential backoff for synchronous calls. Prefer idempotent consumers for async to handle duplicate messages.

grpc-service.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// gRPC service definition (user.proto)
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest { string id = 1; }
message User { string id = 1; string name = 2; }

// Node.js server
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDef = protoLoader.loadSync('user.proto');
const grpcObj = grpc.loadPackageDefinition(packageDef);
const server = new grpc.Server();
server.addService(grpcObj.UserService.service, {
  GetUser: (call, callback) => {
    const user = { id: call.request.id, name: 'Alice' };
    callback(null, user);
  }
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  server.start();
});
Output
gRPC server listens on port 50051, returns user by ID.
Try it live
💡Prefer gRPC for Internal Calls
gRPC's strong typing and streaming capabilities reduce bugs and improve performance over REST for service-to-service communication.
📊 Production Insight
A cascading failure occurred when a REST call to the user service timed out, causing the order service to exhaust its connection pool. We added circuit breakers and a bulkhead pattern to isolate failures.
🎯 Key Takeaway
Use synchronous calls for queries, async for commands; always handle failures gracefully.
microservices-nodejs THECODEFORGE.IO Node.js Microservices Layered Architecture Tiered design with API gateway, services, and data stores Client Layer Web App | Mobile App | External API API Gateway Authentication | Rate Limiting | Request Routing Service Layer User Service | Order Service | Payment Service Inter-Service Communication REST/HTTP | gRPC | Message Broker (RabbitMQ) Data Layer PostgreSQL | MongoDB | Redis Cache Observability & Infrastructure ELK Stack | Prometheus | Jaeger Tracing THECODEFORGE.IO
thecodeforge.io
Microservices Nodejs

API Gateway Pattern

An API gateway acts as a single entry point for clients, routing requests to appropriate microservices. It handles cross-cutting concerns: authentication, rate limiting, logging, and response aggregation. In Node.js, popular gateways include Express-based custom gateways, Kong, or Envoy. The gateway should not contain business logic — it's a proxy. For mobile clients, the gateway can compose responses from multiple services (e.g., fetch user + orders in one call). However, avoid turning the gateway into a monolithic orchestrator; use backend-for-frontend (BFF) patterns if needed. Always implement health checks and graceful degradation. A common pitfall is making the gateway too smart, leading to tight coupling and a single point of failure.

api-gateway.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

app.use('/users', createProxyMiddleware({ target: 'http://user-service:3001', changeOrigin: true }));
app.use('/orders', createProxyMiddleware({ target: 'http://order-service:3002', changeOrigin: true }));

app.get('/health', (req, res) => res.json({ status: 'UP' }));

app.listen(3000);
Output
Gateway routes /users to user-service:3001 and /orders to order-service:3002.
Try it live
⚠ Don't Overload the Gateway
Avoid putting business logic or complex aggregation in the gateway. It should remain a thin routing layer.
📊 Production Insight
We saw the gateway become a bottleneck when it started doing data transformation and caching. Moving those responsibilities to BFF services improved scalability and team ownership.
🎯 Key Takeaway
API gateway simplifies client communication but must stay stateless and thin.

Service Discovery and Load Balancing

In a dynamic microservices environment, service instances come and go. Service discovery allows services to find each other without hardcoded addresses. Two patterns: client-side discovery (service registry like Consul, etcd) and server-side discovery (load balancer like Nginx, HAProxy). For Node.js, client-side discovery with Consul is common: each service registers itself on startup and deregisters on shutdown. The client queries the registry to get healthy instances and load balances (e.g., round-robin). Kubernetes provides built-in service discovery via DNS and endpoints. In production, always use health checks to remove unhealthy instances. A common failure is stale registry entries causing requests to dead instances — implement TTL and heartbeats.

consul-register.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const consul = require('consul')();
const express = require('express');
const app = express();

const serviceId = 'user-service-1';
app.get('/health', (req, res) => res.json({ status: 'pass' }));

app.listen(3001, () => {
  consul.agent.service.register({
    id: serviceId,
    name: 'user-service',
    address: 'localhost',
    port: 3001,
    check: { http: 'http://localhost:3001/health', interval: '10s' }
  }, (err) => {
    if (err) console.error('Registration failed', err);
  });
});

process.on('SIGINT', () => {
  consul.agent.service.deregister(serviceId, () => process.exit());
});
Output
Service registers with Consul on startup, deregisters on shutdown.
Try it live
🔥Kubernetes Built-in Discovery
If you're on Kubernetes, use its DNS-based service discovery (e.g., 'user-service.namespace.svc.cluster.local') instead of an external registry.
📊 Production Insight
A misconfigured TTL caused Consul to keep dead instances for 5 minutes, leading to 50% request failures. We reduced TTL to 30s and added a deregister hook on shutdown.
🎯 Key Takeaway
Service discovery decouples service locations; always use health checks and TTLs.

Data Management: Database per Service and Eventual Consistency

Each microservice should own its database — no shared databases. This ensures loose coupling and independent deployability. However, it introduces data consistency challenges. For transactions spanning services, use the Saga pattern: a sequence of local transactions with compensating actions on failure. Sagas can be choreographed (each service publishes events) or orchestrated (a coordinator service). For queries that need data from multiple services, use Command Query Responsibility Segregation (CQRS) with materialized views. In Node.js, implement sagas using message queues and idempotent handlers. Avoid distributed transactions (2PC) — they are slow and fragile. Accept eventual consistency as a trade-off for scalability.

saga-choreography.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Order service
class OrderSaga {
  async createOrder(userId, items) {
    const order = await this.orderRepo.save({ userId, items, status: 'pending' });
    await this.eventBus.publish('order.created', { orderId: order.id, userId });
  }
}

// Inventory service
class InventorySaga {
  constructor() {
    this.eventBus.subscribe('order.created', async (event) => {
      try {
        await this.reserveInventory(event.orderId);
        await this.eventBus.publish('inventory.reserved', event);
      } catch (err) {
        await this.eventBus.publish('inventory.failed', event);
      }
    });
    this.eventBus.subscribe('payment.failed', async (event) => {
      await this.releaseInventory(event.orderId);
    });
  }
}
Output
Order created event triggers inventory reservation; failure triggers compensation.
Try it live
⚠ Avoid Distributed Transactions
Two-phase commit (2PC) is rarely worth the complexity. Sagas with compensating actions are more resilient.
📊 Production Insight
We initially used a shared database for 'simplicity' — a schema change in one service broke another. Moving to database-per-service eliminated coupling but required careful saga design for order fulfillment.
🎯 Key Takeaway
Own your data; use sagas for consistency across services.

Observability: Logging, Metrics, and Tracing

Microservices are distributed, making debugging hard. Observability is the ability to understand system state from external outputs. Three pillars: logging (structured, centralized), metrics (latency, error rates, throughput), and distributed tracing (trace requests across services). Use structured logging (JSON) with correlation IDs to trace requests. For metrics, use Prometheus with Node.js client libraries. For tracing, implement OpenTelemetry to propagate trace context via HTTP headers. In production, aggregate logs in Elasticsearch, metrics in Grafana, and traces in Jaeger. A common failure is inconsistent correlation IDs — always pass them via middleware. Without tracing, debugging a slow request across 10 services is nearly impossible.

tracing-middleware.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const { trace, context } = require('@opentelemetry/api');
const express = require('express');
const app = express();

app.use((req, res, next) => {
  const tracer = trace.getTracer('my-service');
  const span = tracer.startSpan('http.request');
  context.with(trace.setSpan(context.active(), span), () => {
    res.on('finish', () => {
      span.setAttribute('http.status_code', res.statusCode);
      span.end();
    });
    next();
  });
});

app.get('/api', (req, res) => {
  res.json({ message: 'Hello' });
});

app.listen(3000);
Output
Every request creates a span; trace context propagates via headers.
Try it live
💡Always Include Correlation IDs
Generate a unique ID per request at the gateway and pass it via headers. Include it in all logs and traces.
📊 Production Insight
A production incident where a single slow database query caused a cascading timeout across 5 services. Without distributed tracing, root cause analysis took 4 hours. After implementing OpenTelemetry, we identified it in 5 minutes.
🎯 Key Takeaway
Observability is non-negotiable; invest in tracing early.

Testing Microservices: Integration and Contract Tests

Testing microservices requires a shift from monolithic integration tests. Unit tests remain important, but the critical tests are integration tests (service-level) and contract tests (between services). Integration tests should test each service in isolation with its real database (use testcontainers for Postgres, etc.). Contract tests (e.g., Pact) verify that service A's expectations of service B's API are met — they catch breaking changes early. Avoid end-to-end tests that spin up all services — they are slow and flaky. Instead, use consumer-driven contracts to ensure compatibility. In Node.js, use supertest for HTTP integration tests and @pact-foundation/pact for contract tests. Always run contract tests in CI to prevent incompatible deployments.

contract-test.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { Pact } = require('@pact-foundation/pact');
const { API } = require('./api');

const provider = new Pact({
  consumer: 'OrderService',
  provider: 'UserService',
  port: 1234,
});

beforeAll(() => provider.setup());
afterAll(() => provider.finalize());

describe('User Service contract', () => {
  test('should return user by ID', async () => {
    await provider.addInteraction({
      state: 'user exists',
      uponReceiving: 'a request for user',
      withRequest: { method: 'GET', path: '/users/1' },
      willRespondWith: { status: 200, body: { id: '1', name: 'Alice' } },
    });
    const api = new API(provider.mockService.baseUrl);
    const user = await api.getUser('1');
    expect(user).toEqual({ id: '1', name: 'Alice' });
  });
});
Output
Pact test verifies OrderService can consume UserService API.
Try it live
🔥Testcontainers for Integration Tests
Use testcontainers to spin up real databases in Docker for integration tests. Avoid in-memory mocks that hide real behavior.
📊 Production Insight
A missing field in a user API response broke the order service in production. Contract tests would have caught it. Now we enforce contract tests in CI and block deployment if they fail.
🎯 Key Takeaway
Contract tests prevent breaking changes; integration tests verify service behavior.

Deployment and CI/CD for Microservices

Each microservice should have its own CI/CD pipeline, build artifact, and deployment strategy. Use containerization (Docker) for consistent environments. Orchestrate with Kubernetes or a simpler platform like Nomad. Implement blue-green or canary deployments to reduce risk. In CI, run unit tests, contract tests, and security scans. Build a Docker image and push to a registry. In CD, deploy to a staging environment, run smoke tests, then promote to production. Use feature flags to decouple deployment from release. A common pitfall is a monolithic CI pipeline that builds all services together — this defeats the purpose of microservices. Each service should be deployable independently.

.github/workflows/deploy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: Deploy User Service
on:
  push:
    branches: [main]
    paths: ['services/user/**']
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build Docker image
        run: docker build -t user-service:latest ./services/user
      - name: Push to registry
        run: docker push registry.example.com/user-service:latest
      - name: Deploy to Kubernetes
        run: kubectl set image deployment/user-service user-service=registry.example.com/user-service:latest
Output
CI/CD pipeline triggers only on changes to user service directory.
💡Independent Pipelines
Each service should have its own CI/CD pipeline triggered by changes to its code. Avoid a monorepo with a single pipeline that builds everything.
📊 Production Insight
We had a single pipeline for all services — a failing test in one service blocked deployments for all others. Splitting into per-service pipelines reduced deployment time from 2 hours to 10 minutes.
🎯 Key Takeaway
Independent deployment pipelines enable fast, safe releases.

Common Pitfalls and Anti-Patterns

Microservices introduce complexity. Common anti-patterns include: distributed monolith (services tightly coupled via synchronous calls), shared database, anemic services (CRUD without logic), chatty communication (too many small requests), and ignoring data consistency. Another pitfall is over-engineering: starting with microservices for a small team is a mistake. Also, avoid rolling your own service mesh or API gateway — use battle-tested solutions like Envoy or Kong. In Node.js, watch out for memory leaks in long-running processes and ensure graceful shutdown. Finally, don't neglect observability until after an incident — invest early.

anti-pattern-chatty.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
// Anti-pattern: chatty synchronous calls
async function getOrderDetails(orderId) {
  const order = await http.get(`http://order-service/orders/${orderId}`);
  const user = await http.get(`http://user-service/users/${order.userId}`);
  const product = await http.get(`http://catalog-service/products/${order.productId}`);
  return { ...order, user, product };
}

// Better: aggregate in a BFF or use GraphQL federation
async function getOrderDetails(orderId) {
  const response = await http.get(`http://bff/orders/${orderId}?include=user,product`);
  return response.data;
}
Output
Chatty pattern makes 3 sequential calls; BFF aggregates in one call.
Try it live
⚠ Distributed Monolith
If you need to deploy multiple services together to make a feature work, you have a distributed monolith. Refactor to reduce coupling.
📊 Production Insight
We fell into the distributed monolith trap: deploying a new feature required coordinated releases of 4 services. We merged two services and used async events to decouple the rest.
🎯 Key Takeaway
Avoid distributed monolith, shared databases, and over-engineering.

When to Avoid Microservices

Microservices are not a silver bullet. Avoid them if your team is small (<10 developers), your domain is simple, or you have no deployment automation. Start with a modular monolith — structure your code into well-defined modules with clear interfaces. You can extract services later when the need arises. Also, avoid microservices if your organization cannot support the operational overhead: multiple databases, CI/CD pipelines, monitoring, and incident response. For many startups, a monolith with a clear internal architecture is faster to build and iterate. Remember: the goal is to deliver value, not to use a trendy architecture.

modular-monolith.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Modular monolith: separate modules with clear boundaries
const express = require('express');
const app = express();

// User module
const userModule = require('./modules/user');
app.use('/users', userModule.router);

// Order module
const orderModule = require('./modules/order');
app.use('/orders', orderModule.router);

// Each module has its own database connection and models
app.listen(3000);
Output
Modular monolith with separate route modules, deployable as one unit.
Try it live
🔥Start Modular
A modular monolith with well-defined interfaces is easier to split later than a messy monolith. Don't jump to microservices prematurely.
📊 Production Insight
A startup I advised spent 6 months building a microservices platform for a simple CRUD app. They could have shipped the MVP in 2 weeks with a monolith. They pivoted back and never looked back.
🎯 Key Takeaway
Microservices add complexity; use them only when the benefits outweigh the costs.

Conclusion: Build for the Right Reasons

Microservices in Node.js can enable independent deployment, team autonomy, and scalability — but at the cost of operational complexity. The key is to start with a modular monolith, extract services when the pain of coupling exceeds the pain of distribution, and invest heavily in observability, CI/CD, and testing. Remember that the architecture should serve the business, not the other way around. As a senior developer, your job is to make pragmatic trade-offs. Use the patterns in this article to build resilient, maintainable systems — but always question whether the complexity is justified.

decision-tree.txtTEXT
1
2
3
Is your team > 10 developers? -> Yes -> Do you have independent deployability needs? -> Yes -> Consider microservices
Is your domain complex with clear bounded contexts? -> Yes -> Consider microservices
Otherwise -> Start with modular monolith
Output
Decision tree for microservices adoption.
💡Pragmatism Over Purity
Don't let architectural purity delay delivery. A well-structured monolith beats a distributed mess every time.
📊 Production Insight
The most successful microservices migrations I've seen started with a monolith that was already modular. The teams extracted services one by one, each time measuring the impact on velocity and reliability.
🎯 Key Takeaway
Microservices are a means to an end, not an end in themselves.

Saga Pattern Implementation

Distributed transactions across microservices require careful handling. The Saga pattern manages failures by breaking a transaction into a sequence of local transactions, each with a compensating action. Two approaches exist: choreography (each service publishes events) and orchestration (a coordinator directs steps). For Node.js, orchestration with a dedicated saga orchestrator service is recommended for complex workflows. Use a message broker like RabbitMQ or Kafka to emit events. Implement idempotency keys and retry logic to handle duplicates. Example: an order saga might reserve inventory, charge payment, and confirm shipment. If payment fails, compensate by releasing inventory. Use libraries like 'sagas' or build your own with state machines. Always log saga state transitions for debugging.

saga-orchestrator.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const { v4: uuidv4 } = require('uuid');
const amqp = require('amqplib');

class SagaOrchestrator {
  constructor() {
    this.sagas = new Map();
  }

  async startSaga(orderId) {
    const sagaId = uuidv4();
    const saga = { id: sagaId, orderId, step: 0, state: 'PENDING' };
    this.sagas.set(sagaId, saga);
    await this.executeStep(saga);
    return sagaId;
  }

  async executeStep(saga) {
    const steps = [
      { service: 'inventory', action: 'reserve', compensate: 'release' },
      { service: 'payment', action: 'charge', compensate: 'refund' },
      { service: 'shipping', action: 'ship', compensate: 'cancelShipment' }
    ];
    const step = steps[saga.step];
    try {
      await this.callService(step.service, step.action, saga.orderId);
      saga.step++;
      if (saga.step >= steps.length) {
        saga.state = 'COMPLETED';
        console.log(`Saga ${saga.id} completed`);
      } else {
        await this.executeStep(saga);
      }
    } catch (err) {
      saga.state = 'FAILED';
      console.error(`Saga ${saga.id} failed at step ${saga.step}: ${err.message}`);
      await this.compensate(saga, steps);
    }
  }

  async compensate(saga, steps) {
    for (let i = saga.step - 1; i >= 0; i--) {
      const step = steps[i];
      await this.callService(step.service, step.compensate, saga.orderId);
    }
    saga.state = 'COMPENSATED';
  }

  async callService(service, action, orderId) {
    // Publish message to service queue
    const conn = await amqp.connect('amqp://localhost');
    const ch = await conn.createChannel();
    await ch.assertQueue(service);
    ch.sendToQueue(service, Buffer.from(JSON.stringify({ action, orderId })));
    // In production, wait for reply with timeout
  }
}

module.exports = SagaOrchestrator;
Output
Saga orchestrator manages distributed transaction with compensating actions.
Try it live
⚠ Idempotency is Critical
Ensure each step is idempotent to handle retries without side effects. Use unique request IDs and store processed IDs in a database.
📊 Production Insight
In production, use a persistent saga log (e.g., in PostgreSQL) to recover from orchestrator crashes. Set timeouts for each step to avoid indefinite hangs.
🎯 Key Takeaway
Saga pattern with orchestration provides clear failure handling and compensations for distributed transactions.

CQRS and Event Sourcing

CQRS (Command Query Responsibility Segregation) separates write and read models, optimizing each for its workload. Event sourcing stores state changes as an append-only event log. Combined, they enable powerful audit trails and temporal queries. In Node.js, implement commands that validate and emit events, and projections that build read models from events. Use a message broker to distribute events. Example: an order service writes events (OrderCreated, OrderShipped) to an event store (e.g., EventStoreDB or PostgreSQL). A separate read service subscribes to events and updates a denormalized view for fast queries. This pattern adds complexity but excels in high-write or audit-heavy systems. Avoid CQRS/ES for simple CRUD; it's overkill.

event-sourcing.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const { Client } = require('pg');
const amqp = require('amqplib');

class EventStore {
  constructor() {
    this.client = new Client({ connectionString: process.env.DATABASE_URL });
  }

  async appendEvent(aggregateId, event) {
    await this.client.query(
      'INSERT INTO events (aggregate_id, event_type, data, version) VALUES ($1, $2, $3, $4)',
      [aggregateId, event.type, JSON.stringify(event.data), event.version]
    );
    // Publish to message broker
    const conn = await amqp.connect('amqp://localhost');
    const ch = await conn.createChannel();
    await ch.assertExchange('events', 'topic', { durable: true });
    ch.publish('events', event.type, Buffer.from(JSON.stringify(event)));
  }

  async getEvents(aggregateId) {
    const res = await this.client.query(
      'SELECT * FROM events WHERE aggregate_id = $1 ORDER BY version',
      [aggregateId]
    );
    return res.rows;
  }
}

// Projection example
class OrderProjection {
  constructor() {
    this.orders = new Map();
  }

  applyEvent(event) {
    if (event.type === 'OrderCreated') {
      this.orders.set(event.data.orderId, { status: 'created', items: event.data.items });
    } else if (event.type === 'OrderShipped') {
      const order = this.orders.get(event.data.orderId);
      if (order) order.status = 'shipped';
    }
  }
}

module.exports = { EventStore, OrderProjection };
Output
Event store appends events and publishes them; projection builds read model.
Try it live
💡Start with CQRS without Event Sourcing
If you need separate read/write models but not full audit, implement CQRS with traditional databases first. Add event sourcing only when you need temporal queries or event-driven integrations.
📊 Production Insight
Use a dedicated event store like EventStoreDB for high performance. Ensure projections are idempotent and can rebuild from scratch. Monitor event processing lag.
🎯 Key Takeaway
CQRS/Event Sourcing decouples reads from writes and provides an immutable event log, but adds significant complexity.

Circuit Breaker Pattern

Circuit breakers prevent cascading failures by stopping calls to a failing service. When failures exceed a threshold, the circuit opens and subsequent calls fail fast. After a timeout, a half-open state allows a probe request; if it succeeds, the circuit closes. In Node.js, use libraries like 'opossum' or 'cockatiel'. Configure thresholds: failure count, timeout, and reset timeout. Example: an API gateway calls a payment service with a circuit breaker. If payment service returns 5xx errors 3 times in 10 seconds, the circuit opens for 30 seconds. During open, return a cached response or fallback. Always log circuit state changes. Combine with retry/backoff for transient failures.

circuit-breaker.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const CircuitBreaker = require('opossum');
const axios = require('axios');

async function callPaymentService(orderId) {
  const response = await axios.post('http://payment-service/charge', { orderId });
  return response.data;
}

const options = {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000,
  name: 'payment-service'
};

const breaker = new CircuitBreaker(callPaymentService, options);

breaker.fallback(() => ({ status: 'fallback', message: 'Payment service unavailable, order queued' }));

breaker.on('open', () => console.log('Circuit opened for payment-service'));
breaker.on('halfOpen', () => console.log('Circuit half-open for payment-service'));
breaker.on('close', () => console.log('Circuit closed for payment-service'));

// Usage
async function processOrder(orderId) {
  try {
    const result = await breaker.fire(orderId);
    console.log('Payment result:', result);
  } catch (err) {
    console.error('Payment failed:', err.message);
  }
}

module.exports = { processOrder, breaker };
Output
Circuit breaker opens after 50% errors in 10 seconds, falls back to queued response.
Try it live
🔥Monitor Circuit State
Expose circuit breaker metrics (state, failure count, latency) via Prometheus. Alert on open circuits to trigger incident response.
📊 Production Insight
Tune thresholds based on historical latency and error rates. Use separate circuit breakers per endpoint, not per service, to isolate failures.
🎯 Key Takeaway
Circuit breakers protect downstream services and provide graceful degradation with fallbacks.

Message Broker Deep Dive: Kafka vs RabbitMQ

Choosing the right message broker is critical for inter-service communication. RabbitMQ is a general-purpose broker with AMQP, supporting complex routing and direct messaging. Kafka is a distributed streaming platform optimized for high throughput, replayability, and event sourcing. Use RabbitMQ for task queues, RPC, and low-latency messaging. Use Kafka for event streaming, log aggregation, and large-scale data pipelines. In Node.js, 'amqplib' for RabbitMQ and 'kafkajs' for Kafka. Consider: RabbitMQ has built-in retry/dead-letter queues; Kafka requires consumer-side handling. Kafka persists messages longer, enabling replay. Both support clustering. For microservices, start with RabbitMQ; move to Kafka when you need replay or high throughput.

broker-comparison.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// RabbitMQ producer
const amqp = require('amqplib');
async function rabbitSend(queue, msg) {
  const conn = await amqp.connect('amqp://localhost');
  const ch = await conn.createChannel();
  await ch.assertQueue(queue, { durable: true });
  ch.sendToQueue(queue, Buffer.from(msg), { persistent: true });
  setTimeout(() => conn.close(), 500);
}

// Kafka producer
const { Kafka } = require('kafkajs');
const kafka = new Kafka({ clientId: 'my-app', brokers: ['localhost:9092'] });
const producer = kafka.producer();
async function kafkaSend(topic, msg) {
  await producer.connect();
  await producer.send({ topic, messages: [{ value: msg }] });
  await producer.disconnect();
}

// RabbitMQ consumer
async function rabbitConsume(queue, handler) {
  const conn = await amqp.connect('amqp://localhost');
  const ch = await conn.createChannel();
  await ch.assertQueue(queue, { durable: true });
  ch.consume(queue, msg => {
    handler(msg.content.toString());
    ch.ack(msg);
  });
}

// Kafka consumer
const consumer = kafka.consumer({ groupId: 'test-group' });
async function kafkaConsume(topic, handler) {
  await consumer.connect();
  await consumer.subscribe({ topic, fromBeginning: true });
  await consumer.run({ eachMessage: async ({ message }) => handler(message.value.toString()) });
}
Output
RabbitMQ uses queues; Kafka uses topics with partitions. Both support pub/sub.
Try it live
💡Start Simple, Evolve
Begin with RabbitMQ for most microservices. Migrate to Kafka only when you need replay, high throughput (>10k msg/s), or event sourcing.
📊 Production Insight
Monitor consumer lag in both brokers. For Kafka, tune partition count and replication factor. For RabbitMQ, set max queue length and TTL to avoid unbounded growth.
🎯 Key Takeaway
RabbitMQ excels at task distribution and RPC; Kafka excels at event streaming and replayability.

Canary and Blue-Green Deployments

Deploying microservices with zero downtime requires advanced strategies. Blue-green deployments run two identical environments (blue and green). Traffic is switched all at once. Canary deployments gradually shift a percentage of traffic to the new version, monitoring for errors. Use a service mesh (e.g., Istio) or API gateway (e.g., Kong) to control traffic splitting. In Node.js, implement canary via Kubernetes with multiple deployments and a service selector. Example: deploy v2 with 10% traffic, monitor for 5 minutes, then increase to 50%, then 100%. Automate rollback if error rate spikes. Blue-green is simpler; canary is safer for high-risk changes.

canary-deployment.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-service
      version: canary
  template:
    metadata:
      labels:
        app: my-service
        version: canary
    spec:
      containers:
      - name: my-service
        image: my-service:2.0.0
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-service
  ports:
  - port: 80
    targetPort: 3000
Output
Canary deployment with 1 replica of v2.0.0 alongside stable version. Adjust replicas to control traffic weight.
⚠ Monitor Metrics During Canary
Track error rates, latency, and business metrics (e.g., conversion). Set up automated rollback if any metric exceeds threshold.
📊 Production Insight
Use feature flags alongside canary to decouple deployment from release. For stateful services, blue-green requires careful database migration handling.
🎯 Key Takeaway
Blue-green provides instant switch; canary reduces risk by gradual rollout with monitoring.

OpenTelemetry Distributed Tracing

Distributed tracing correlates requests across microservices. OpenTelemetry is the standard for collecting traces, metrics, and logs. Instrument Node.js services with the OpenTelemetry SDK. Export traces to Jaeger or Zipkin. Use context propagation via HTTP headers (W3C Trace-Context). Example: an API gateway receives a request, creates a span, and injects trace context into downstream calls. Each service creates child spans. Traces show latency breakdown and error sources. In production, sample traces (e.g., 10%) to reduce overhead. Use automatic instrumentation for Express, gRPC, and database clients.

tracing.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');

const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter({
  endpoint: 'http://jaeger:14268/api/traces',
})));
provider.register();

registerInstrumentations({
  instrumentations: [
    new ExpressInstrumentation(),
    new HttpInstrumentation(),
  ],
});

const express = require('express');
const app = express();

app.get('/api/orders', async (req, res) => {
  // Automatic tracing captures this request
  const response = await fetch('http://order-service/orders');
  const data = await response.json();
  res.json(data);
});

app.listen(3000);
Output
Traces exported to Jaeger; automatic instrumentation for Express and HTTP.
Try it live
🔥Sampling Strategy
Use head-based sampling (e.g., 10%) for production. For high-traffic services, consider tail-based sampling to capture only errors or slow traces.
📊 Production Insight
Store traces in a scalable backend like Jaeger or Grafana Tempo. Set up alerts on trace latency percentiles (p99).
🎯 Key Takeaway
OpenTelemetry provides end-to-end visibility with minimal code changes using automatic instrumentation.

Bulkhead Pattern in Node.js Microservices

The bulkhead pattern isolates resources to prevent cascading failures. In Node.js, implement bulkheads using connection pools, thread pools, or process isolation. For example, separate database connection pools for different services or use worker threads for CPU-intensive tasks. A common approach is to use the generic-pool library to create pools with max limits. When one pool exhausts, it fails fast without starving others. In microservices, apply bulkheads at the service level: run critical and non-critical services in separate processes or containers with dedicated resources. This ensures that a memory leak in a logging service doesn't crash the payment service. Production insight: monitor pool utilization and set alerts when pools reach 80% capacity to proactively scale.

bulkhead-pool.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const genericPool = require('generic-pool');
const { Pool } = require('pg');

const factory = {
  create: () => new Pool({ max: 10, idleTimeoutMillis: 30000 }),
  destroy: (pool) => pool.end()
};

const pool = genericPool.createPool(factory, {
  max: 5, // max pools
  min: 2
});

async function query(sql) {
  const client = await pool.acquire();
  try {
    return await client.query(sql);
  } finally {
    pool.release(client);
  }
}
Output
Pool acquired and released successfully.
Try it live
⚠ Don't Over-Isolate
Too many bulkheads increase complexity and resource overhead. Start with coarse-grained isolation (e.g., separate containers) and refine based on failure patterns.
📊 Production Insight
Combine bulkheads with circuit breakers: when a pool is exhausted, the circuit breaker should open to avoid repeated acquisition attempts.
🎯 Key Takeaway
Bulkhead pattern limits blast radius by partitioning resources. Use connection pools and process isolation to prevent one failing component from taking down the whole system.

Retry and Backoff Strategies for Resilient Communication

Transient failures are inevitable in distributed systems. Implement retry with exponential backoff and jitter to avoid thundering herd. In Node.js, use libraries like async-retry or p-retry. Configure max retries (3-5), initial delay (100ms), and factor (2). Add jitter by randomizing delay up to 50% to spread retries. For idempotent operations (e.g., GET, PUT with idempotency keys), retry safely. For non-idempotent, use a saga or compensate. Production insight: monitor retry rates; high retry counts indicate underlying issues. Use circuit breakers to stop retrying when service is down. Example: retry with exponential backoff for a database call.

retry-backoff.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const retry = require('async-retry');
const axios = require('axios');

async function fetchWithRetry(url) {
  return retry(async (bail) => {
    const response = await axios.get(url);
    if (response.status === 429) {
      throw new Error('Rate limited'); // retry
    }
    return response.data;
  }, {
    retries: 3,
    minTimeout: 100,
    maxTimeout: 1000,
    factor: 2,
    onRetry: (error, attempt) => {
      console.log(`Retry ${attempt}: ${error.message}`);
    }
  });
}
Output
Data fetched after 2 retries.
Try it live
💡Jitter is Critical
Without jitter, retries from multiple clients synchronize and overwhelm the server. Always add random jitter to backoff delays.
📊 Production Insight
Set a global retry budget (e.g., max 1% of requests retried) to prevent retry storms from degrading overall system performance.
🎯 Key Takeaway
Retry with exponential backoff and jitter handles transient failures gracefully. Combine with circuit breakers to avoid retrying when service is down.
REST vs gRPC for Inter-Service Communication Trade-offs in performance, complexity, and tooling REST/HTTP gRPC Protocol HTTP/1.1, text-based (JSON/XML) HTTP/2, binary (Protobuf) Performance Higher latency, larger payloads Low latency, compact messages Streaming Limited (long polling, SSE) Native bidirectional streaming Tooling & Ecosystem Mature, wide client support Growing, strong in microservices Browser Support Native, easy debugging Requires gRPC-web proxy Use Case Public APIs, simple CRUD Internal services, real-time data THECODEFORGE.IO
thecodeforge.io
Microservices Nodejs

Health Check API Standards for Node.js Microservices

Health checks are essential for orchestration and load balancers. Follow the RFC 9560 (Health Check Response Format) standard: expose a /healthz endpoint returning JSON with status (pass, warn, fail), version, and dependencies. Use readiness and liveness probes: readiness indicates service is ready to accept traffic (e.g., DB connected), liveness indicates service is alive (e.g., process not stuck). In Node.js, implement using a library like health-checkup or custom middleware. Example: a health check that pings database and cache. Production insight: include critical dependency status but avoid deep checks that could cause cascading failures. Set timeouts and cache results for 5-10 seconds.

health-check.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require('express');
const app = express();

app.get('/healthz', async (req, res) => {
  const checks = {
    status: 'pass',
    version: '1.0.0',
    checks: {}
  };

  try {
    await db.ping();
    checks.checks.database = { status: 'pass' };
  } catch (err) {
    checks.checks.database = { status: 'fail', message: err.message };
    checks.status = 'fail';
  }

  try {
    await cache.ping();
    checks.checks.cache = { status: 'pass' };
  } catch (err) {
    checks.checks.cache = { status: 'fail', message: err.message };
    checks.status = 'fail';
  }

  res.status(checks.status === 'pass' ? 200 : 503).json(checks);
});
Output
{"status":"pass","version":"1.0.0","checks":{"database":{"status":"pass"},"cache":{"status":"pass"}}}
Try it live
🔥Separate Liveness and Readiness
Kubernetes uses liveness to restart pods and readiness to stop traffic. Use different endpoints: /healthz/live and /healthz/ready.
📊 Production Insight
Avoid checking external services in liveness probes; they can cause cascading failures. Use readiness for deep checks.
🎯 Key Takeaway
Standardized health checks (RFC 9560) with pass/fail status and dependency details enable reliable orchestration and self-healing.
● Production incidentPOST-MORTEMseverity: high

The Case of the Cascading Timeout: How a Slow Downstream Service Took Down Our Entire Node.js Microservices Mesh

Symptom
All API endpoints returning 504 Gateway Timeout. P99 latency spiked from 200ms to 30s. Error rate hit 100% within 2 minutes.
Assumption
The database was overloaded. We assumed a traffic spike caused the DB to throttle, so we scaled up DB replicas and increased connection pool size.
Root cause
A new deployment of the user-service introduced a synchronous Redis call with no timeout. Under load, Redis became slow (due to a misconfigured eviction policy), causing the user-service's HTTP handler to block its thread pool. Since all services called user-service synchronously (via HTTP), they all queued up waiting for responses, exhausting their own connection pools and causing cascading timeouts.
Fix
1) Added a 500ms timeout to the Redis call. 2) Implemented a circuit breaker (using opossum) around the Redis call: after 5 failures in 10 seconds, open the circuit and return a cached fallback for 30 seconds. 3) Changed inter-service calls to use async messaging for non-critical paths. 4) Added bulkheads: separate connection pools for internal vs. external calls.
Key lesson
  • Always set timeouts on all I/O operations, especially in Node.js where a single slow call can block the event loop.
  • Use circuit breakers to fail fast and prevent cascading failures. Don't let a downstream service degrade your entire system.
  • Prefer asynchronous communication (queues/events) for non-critical paths to decouple services.
  • Monitor thread pool and connection pool exhaustion. Set alerts on queue depth and pending connections.
  • Test failure modes in staging: inject latency and observe system behavior before deploying to production.
⚙ Quick Reference
21 commands from this guide
FileCommand / CodePurpose
monolith-vs-microservice.jsconst express = require('express');Why Microservices in Node.js?
bounded-context.jsclass OrderService {Service Boundaries and Domain-Driven Design
grpc-service.jsservice UserService {Inter-Service Communication
api-gateway.jsconst express = require('express');API Gateway Pattern
consul-register.jsconst consul = require('consul')();Service Discovery and Load Balancing
saga-choreography.jsclass OrderSaga {Data Management
tracing-middleware.jsconst { trace, context } = require('@opentelemetry/api');Observability
contract-test.jsconst { Pact } = require('@pact-foundation/pact');Testing Microservices
.githubworkflowsdeploy.ymlname: Deploy User ServiceDeployment and CI/CD for Microservices
anti-pattern-chatty.jsasync function getOrderDetails(orderId) {Common Pitfalls and Anti-Patterns
modular-monolith.jsconst express = require('express');When to Avoid Microservices
decision-tree.txtIs your team > 10 developers? -> Yes -> Do you have independent deployability ne...Conclusion
saga-orchestrator.jsconst { v4: uuidv4 } = require('uuid');Saga Pattern Implementation
event-sourcing.jsconst { Client } = require('pg');CQRS and Event Sourcing
circuit-breaker.jsconst CircuitBreaker = require('opossum');Circuit Breaker Pattern
broker-comparison.jsconst amqp = require('amqplib');Message Broker Deep Dive
canary-deployment.yamlapiVersion: apps/v1Canary and Blue-Green Deployments
tracing.jsconst { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');OpenTelemetry Distributed Tracing
bulkhead-pool.jsconst genericPool = require('generic-pool');Bulkhead Pattern in Node.js Microservices
retry-backoff.jsconst retry = require('async-retry');Retry and Backoff Strategies for Resilient Communication
health-check.jsconst express = require('express');Health Check API Standards for Node.js Microservices

Key takeaways

1
Microservices solve organizational scaling, not technical problems
Extract services only when deployment coupling or team coordination becomes a bottleneck.
2
Bound services by business capability, not technical layers
Use Domain-Driven Design to define bounded contexts and minimize cross-service communication.
3
Invest in observability from day one
Distributed tracing, structured logging, and metrics are essential for debugging and understanding system behavior.
4
Start with a modular monolith
Premature microservices add complexity. Extract services incrementally when the pain of coupling exceeds the cost of distribution.
5
Saga Pattern
Use orchestration for complex workflows; implement compensating transactions and idempotency keys to handle failures in distributed transactions.
6
CQRS/Event Sourcing
Separate read/write models for scalability; event sourcing provides audit trail but adds complexity; avoid unless needed.
7
Circuit Breaker
Protect downstream services with fallbacks; combine with retry/backoff for transient errors; monitor circuit state in production.
8
Bulkhead Pattern
Isolate resources (connection pools, processes) to prevent cascading failures. Combine with circuit breakers for robust fault isolation.
9
Retry with Exponential Backoff
Handle transient failures with retries (3-5 max), exponential backoff, and jitter. Use circuit breakers to stop retrying when service is down.
10
Health Check Standards
Expose /healthz with RFC 9560 format (pass/fail, version, dependencies). Separate liveness (process alive) and readiness (ready for traffic) probes.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What are the main trade-offs between using HTTP/REST vs. message queues ...
Q02SENIOR
How would you handle service discovery in a Node.js microservices deploy...
Q03SENIOR
Explain the Saga pattern and how you would implement it in Node.js for a...
Q04SENIOR
How do you manage shared data models and avoid duplication across micros...
Q05SENIOR
What strategies do you use for logging and monitoring in a Node.js micro...
Q06JUNIOR
How would you implement a health check endpoint in a Node.js microservic...
Q01 of 06SENIOR

What are the main trade-offs between using HTTP/REST vs. message queues for inter-service communication in a Node.js microservices architecture?

ANSWER
HTTP/REST is synchronous, simpler to debug, and works well for request-response patterns, but it introduces tight coupling and latency if services are chained. Message queues (e.g., RabbitMQ, Kafka) enable asynchronous, decoupled communication, better fault tolerance, and load leveling, but add complexity in message ordering, idempotency, and eventual consistency. Choose REST for low-latency queries where consistency is critical; use queues for event-driven workflows and when you need to buffer spikes.
FAQ · 11 QUESTIONS

Frequently Asked Questions

01
What is the difference between a microservice and a monolith?
02
How do microservices communicate with each other?
03
What is a saga pattern?
04
How do you handle database migrations in microservices?
05
What is the API Gateway pattern?
06
When should I avoid microservices?
07
What is the bulkhead pattern and how do I implement it in Node.js?
08
How should I implement retry and backoff strategies for inter-service calls?
09
What are the standard health check API formats for microservices?
10
How do I implement the saga pattern in Node.js without a framework?
11
What are the trade-offs between Kong and Traefik as API gateways for Node.js microservices?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

8 min read · try the examples if you haven't

Previous
GraphQL with Apollo Server in Node.js
43 / 47 · Node.js
Next
Building CLI Tools with Node.js and Commander