Skip to content
Learni
View all tutorials
Architecture

How to Architect Scalable Microservices in 2026

Lire en français

Introduction

Microservices architectures allow you to break down monolithic applications into independent services, each responsible for a specific business domain. This approach improves scalability, resilience, and deployment speed. In 2026, teams routinely use tools like Kubernetes for orchestration and message brokers for asynchronous communication. This tutorial guides you step by step through building a complete system consisting of two Node.js services communicating via RabbitMQ, containerized with Docker, and deployed on Kubernetes. Each step includes production-ready code.

Prerequisites

  • Node.js 20+
  • Docker Desktop
  • Kubernetes (Minikube or cloud cluster)
  • Solid knowledge of TypeScript and distributed architecture
  • RabbitMQ installed locally for testing

User Service

user-service/src/index.ts
import express from 'express';
import amqp from 'amqplib';

const app = express();
app.use(express.json());

let channel: any;

async function connectRabbit() {
  const connection = await amqp.connect('amqp://localhost');
  channel = await connection.createChannel();
  await channel.assertQueue('user_created');
}

app.post('/users', async (req, res) => {
  const user = { id: Date.now(), ...req.body };
  await channel.sendToQueue('user_created', Buffer.from(JSON.stringify(user)));
  res.status(201).json(user);
});

connectRabbit().then(() => {
  app.listen(3001, () => console.log('User service on 3001'));
});

This Express service exposes a POST endpoint and publishes an event to RabbitMQ after user creation. The connection is established once at startup to avoid resource leaks.

Order Service

order-service/src/index.ts
import express from 'express';
import amqp from 'amqplib';

const app = express();
app.use(express.json());

let channel: any;

async function connectRabbit() {
  const connection = await amqp.connect('amqp://localhost');
  channel = await connection.createChannel();
  await channel.assertQueue('user_created');
  channel.consume('user_created', (msg) => {
    if (msg) {
      const user = JSON.parse(msg.content.toString());
      console.log('Nouvel utilisateur reçu:', user);
      channel.ack(msg);
    }
  });
}

app.post('/orders', (req, res) => {
  res.status(201).json({ orderId: Date.now(), userId: req.body.userId });
});

connectRabbit().then(() => {
  app.listen(3002, () => console.log('Order service on 3002'));
});

The Order service consumes user_created events. Using channel.ack ensures the message is only removed from the queue after successful processing.

User Service Dockerfile

user-service/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3001
CMD ["node", "dist/index.js"]

Minimal Alpine image to reduce the attack surface. npm ci ensures reproducible production builds.

Docker Compose

docker-compose.yml
version: '3.8'
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"
  user-service:
    build: ./user-service
    ports:
      - "3001:3001"
    depends_on:
      - rabbitmq
  order-service:
    build: ./order-service
    ports:
      - "3002:3002"
    depends_on:
      - rabbitmq

Compose orchestrates the three containers with explicit dependencies. RabbitMQ also exposes its management interface for debugging.

Kubernetes Deployment

k8s/user-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 3001

Three replicas ensure high availability. The label selector allows a Kubernetes Service to route traffic automatically.

Best Practices

  • Use message queues for asynchronous communication to decouple services
  • Implement the Circuit Breaker pattern to handle partial failures
  • Version your APIs and events
  • Centralize logs and metrics with ELK or Prometheus
  • Automate contract testing between services

Common Mistakes to Avoid

  • Sharing a database between services (creates tight coupling)
  • Ignoring duplicate message handling (idempotency is required)
  • Forgetting Kubernetes health checks
  • Not limiting payload size in message queues

Going Further

Deepen your knowledge of observability and service mesh with Istio in our Learni training courses.