Part 17 – Building Microservices

Microservices architecture breaks applications into small, independent services. This guide covers designing, building, and operating Node.js microservices in production.

1. Microservices Fundamentals

Key Principles

  • Single Responsibility: Each service does one thing well
  • Independent Deployment: Deploy services separately
  • Decentralized Data: Each service owns its data
  • Failure Isolation: One service failing shouldn’t crash others

When to Use Microservices

  • Large engineering teams
  • Complex domain with clear boundaries
  • Need for independent scaling
  • Different technology requirements
  • Frequent deployments

2. Service Decomposition

Identifying Service Boundaries

// Example e-commerce domain services:
// - Product Service: product catalog, inventory
// - Order Service: order processing, fulfillment
// - User Service: authentication, profiles
// - Payment Service: payment processing
// - Notification Service: emails, alerts

Domain-Driven Design Concepts

  • Bounded Context: Explicit service boundaries
  • Aggregates: Transactional consistency boundaries
  • Ubiquitous Language: Shared terminology
  • Anti-Corruption Layer: Isolate external systems

3. Communication Patterns

Synchronous (HTTP/RPC)

// Using Axios for HTTP calls
const axios = require('axios');

async function getProductDetails(productId) {
    try {
        const response = await axios.get(
            `http://product-service/products/${productId}`
        );
        return response.data;
    } catch (err) {
        if (err.response?.status === 404) {
            throw new Error('Product not found');
        }
        throw err;
    }
}

Asynchronous (Message Brokers)

// Using RabbitMQ with amqplib
const amqp = require('amqplib');

async function publishOrderEvent(order) {
    const conn = await amqp.connect('amqp://localhost');
    const channel = await conn.createChannel();

    await channel.assertExchange('order_events', 'topic');
    channel.publish('order_events', 'order.created', 
        Buffer.from(JSON.stringify(order)));

    setTimeout(() => conn.close(), 500);
}

// Consumer service
channel.consume('order_queue', (msg) => {
    const order = JSON.parse(msg.content.toString());
    processOrder(order);
    channel.ack(msg);
});

4. Service Implementation

Product Service Example

// product-service/app.js
const express = require('express');
const { Product } = require('./models');

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

// Routes
app.get('/products/:id', async (req, res) => {
    const product = await Product.findById(req.params.id);
    if (!product) return res.status(404).send();
    res.json(product);
});

app.post('/products', async (req, res) => {
    const product = await Product.create(req.body);
    res.status(201).json(product);
});

module.exports = app;

// Standalone server
if (require.main === module) {
    const PORT = process.env.PORT || 3001;
    app.listen(PORT, () => {
        console.log(`Product service running on ${PORT}`);
    });
}

5. API Gateway

Basic Gateway with Express

const express = require('express');
const httpProxy = require('express-http-proxy');

const app = express();

// Service proxies
const productService = httpProxy('http://localhost:3001');
const userService = httpProxy('http://localhost:3002');

// Routes
app.get('/api/products*', productService);
app.post('/api/products*', productService);
app.get('/api/users*', userService);

// Start gateway
app.listen(3000, () => {
    console.log('API Gateway running on port 3000');
});

Advanced Gateway Features

  • Request aggregation
  • Authentication/authorization
  • Rate limiting
  • Response caching
  • Request/response transformation
  • Circuit breakers

6. Distributed System Challenges

Common Issues and Solutions

ChallengeSolution
Network latencyCaching, async communication
Partial failuresCircuit breakers, retries
Data consistencySagas, eventual consistency
Distributed loggingCentralized logging (ELK)
Service discoveryConsul, Eureka, Kubernetes DNS

7. Observability

Distributed Tracing

// Using OpenTelemetry
const { NodeTracerProvider } = require('@opentelemetry/node');
const { SimpleSpanProcessor } = require('@opentelemetry/tracing');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');

const provider = new NodeTracerProvider();
provider.register();

// Configure Jaeger exporter
const exporter = new JaegerExporter({
    serviceName: 'product-service',
    host: 'jaeger-agent'
});

provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

// Instrument HTTP requests
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
new HttpInstrumentation().enable();

Health Checks

// Basic health endpoint
app.get('/health', (req, res) => {
    res.json({
        status: 'UP',
        details: {
            db: checkDatabaseConnection(),
            cache: checkRedisConnection()
        }
    });
});

// With termination signals
process.on('SIGTERM', () => {
    server.close(() => {
        process.exit(0);
    });
});

Next: Serverless Node.js →

Microservices Checklist

  • ✅ Clear service boundaries
  • ✅ Independent deployability
  • ✅ Proper failure handling
  • ✅ Centralized observability
  • ✅ CI/CD pipelines per service
  • ✅ Automated scaling configuration

Leave a Comment

Your email address will not be published. Required fields are marked *