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
Challenge | Solution |
---|---|
Network latency | Caching, async communication |
Partial failures | Circuit breakers, retries |
Data consistency | Sagas, eventual consistency |
Distributed logging | Centralized logging (ELK) |
Service discovery | Consul, 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);
});
});
Microservices Checklist
- ✅ Clear service boundaries
- ✅ Independent deployability
- ✅ Proper failure handling
- ✅ Centralized observability
- ✅ CI/CD pipelines per service
- ✅ Automated scaling configuration