Introduction
As applications grow, a single codebase (a monolith) becomes hard to scale, test, and deploy. Microservices solve this by splitting the system into small, independent services. In this article, we build a simple microservices setup with Node.js and cover the patterns that keep it scalable.
What Are Microservices?
A microservice is a small service that owns one business capability, such as users, orders, or payments. Each service:
- Runs independently
- Has its own database
- Communicates over HTTP or messaging
- Can be deployed and scaled separately
Architecture Overview
Client -> API Gateway -> User Service
-> Order Service
-> Payment ServiceThe API Gateway is the single entry point. It routes requests, handles authentication, and applies rate limiting.
Step 1: Create a Basic Service
// user-service/index.js
const express = require("express");
const app = express();
app.use(express.json());app.get("/users/:id", (req, res) => { res.json({ id: req.params.id, name: "Asha" }); });
app.get("/health", (req, res) => res.sendStatus(200));
app.listen(3001, () => console.log("User service on 3001")); ```
The /health endpoint lets load balancers and orchestrators check that the service is alive.
Step 2: Add an API Gateway
// gateway/index.js
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();app.use("/users", createProxyMiddleware({ target: "http://localhost:3001", changeOrigin: true })); app.use("/orders", createProxyMiddleware({ target: "http://localhost:3002", changeOrigin: true }));
app.listen(3000, () => console.log("Gateway on 3000")); ```
Step 3: Containerize with Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3001
CMD ["node", "index.js"]Run all services together with Docker Compose:
services:
gateway:
build: ./gateway
ports: ["3000:3000"]
user-service:
build: ./user-service
order-service:
build: ./order-serviceStep 4: Asynchronous Communication
Direct HTTP calls between services create tight coupling. For events like "order placed", use a message broker such as RabbitMQ or Kafka. The Order service publishes an event, and the Payment and Notification services consume it independently. If one service goes down, messages wait in the queue instead of failing the whole request.
Scalability Best Practices
- Keep services stateless so you can add instances behind a load balancer.
- Database per service to avoid shared-schema bottlenecks.
- Caching with Redis for frequently read data.
- Circuit breakers (e.g., the
opossumlibrary) to stop cascading failures. - Centralized logging and monitoring with tools like Prometheus and Grafana.
- Horizontal scaling using Kubernetes or Docker Swarm.
Common Pitfalls
- Splitting too early. Start with a well-structured monolith if the product is small.
- Services that are too fine-grained, causing excessive network calls.
- No versioning on APIs, which breaks consumers on every change.
- Skipping distributed tracing, which makes debugging very hard.
Lessons Learned
Microservices trade code complexity for operational complexity. The payoff is independent deployments, fault isolation, and the ability to scale only the parts under load. Invest early in automation, monitoring, and clear service boundaries.
Conclusion
Node.js is a strong fit for microservices thanks to its lightweight runtime, non-blocking I/O, and rich ecosystem. Start small, define clear boundaries, containerize everything, and use asynchronous messaging where it makes sense. That combination gives you a system that scales with demand.