bugl
bugl
HomeLearnPatternsPathsSearch
HomeLearnPatternsPathsSearch

Loading lesson path

Learn/Node.js/Node.js Advanced
Node.js•Node.js Advanced

Node.js Microservices

Flash cards

Review the key moves

1/4
Core idea

What is the main idea behind Node.js Microservices?

Lesson checks

Practice each idea before moving on

Short Mimo-style checks built from this lesson's code, terms, and sequence.

1Quick choice

Which statement best captures the main point of this lesson?

2Fill blank

Complete the missing token from the example code.

// ___-service.js
3Order

Put the learning moves in the order that makes the concept easiest to apply.

Node.js for Microservices
Monoliths vs Microservices
Introduction to Microservices

Introduction to Microservices

Microservices is an architectural style that structures an application as a collection of small, loosely coupled services. Each service is:

  • Focused on a single business capability
  • Independently deployable
  • Independently scalable
  • Potentially written in different programming languages
  • Potentially using different data storage technologies

Microservices architecture enables faster development cycles, better scalability, and improved resilience compared to traditional monolithic applications.

Monoliths vs Microservices

AspectMonolithic ArchitectureMicroservices Architecture
StructureSingle, unified codebaseMultiple small services
DeploymentEntire application deployed at onceServices deployed independently
ScalingEntire application must scale togetherIndividual services can scale independently
DevelopmentSingle technology stackPotentially different technologies per service
Team StructureOften a single teamMultiple teams, each owning specific services
ComplexitySimpler architecture, complex codebaseComplex architecture, simpler individual codebases

Key Principles

  • Single Responsibility - Each microservice should focus on doing one thing well - implementing a single business capability.
  • Decentralization - Decentralize everything: governance, data management, and architecture decisions.
  • Autonomous Services - Services should be able to change and deploy independently without affecting others.
  • Domain-Driven Design - Design services around business domains rather than technical functions.
  • Resilience - Services should be designed to handle failure of other services.
  • Observability - Implement comprehensive monitoring, logging, and tracing across services.

Best Practice: Start with a clear domain model and identify bounded contexts before splitting an application into microservices.

Node.js for Microservices

Node.js is particularly well-suited for microservices architecture for several reasons:

  • Lightweight and Fast - Node.js has a small footprint and starts quickly, making it ideal for microservices that need to scale rapidly.
  • Asynchronous and Event-Driven - Node.js's non-blocking I/O model makes it efficient for handling many concurrent connections between services.
  • JSON Support - First-class JSON support makes data exchange between microservices straightforward.
  • NPM Ecosystem - The vast package ecosystem provides libraries for service discovery, API gateways, monitoring, and more.

Example: Simple Node.js Microservice

// user-service.js
const express = require('express');
const app = express();
app.use(express.json());
// In-memory user database for demonstration
const users = [
 { id: 1, name: 'John Doe', email: 'john@example.com' },
 { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
// Get all users
app.get('/users', (req, res) => {
 res.json(users);
});
// Get user by ID
app.get('/users/:id', (req, res) => {
 const user = users.find(u => u.id === parseInt(req.params.id));
 if (!user) return res.status(404).json({ message: 'User not found' });
 res.json(user);
});
// Create a new user
app.post('/users', (req, res) => {
 const newUser = {
 id: users.length + 1,
 name: req.body.name,
 email: req.body.email
 };
 users.push(newUser);
 res.status(201).json(newUser);
});
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
 console.log(`User service running on port ${PORT}`);
});

Service Communication

Microservices need ways to communicate with each other.

Synchronous Communication

Services directly call each other's APIs, creating a real-time request-response flow:

  • REST : Simple, widely used, stateless communication
  • GraphQL : Flexible queries with a single endpoint
  • gRPC : High-performance RPC framework using Protocol Buffers

Example: REST Communication Between Services

// order-service.js calling the user-service
const axios = require('axios');
async function getUserDetails(userId) {
 try {
 const response = await axios.get(`http://user-service:3001/users/${userId}`);
 return response.data;
 } catch (error) {
 console.error(`Error fetching user ${userId}:`, error.message);
 throw new Error('User service unavailable');
}
}
// Route handler in order service
app.post('/orders', async (req, res) => {
 const { userId, products } = req.body;
 try {
 // Get user data from user service
 const user = await getUserDetails(userId);
 // Check product availability from product service
 const productStatus = await checkProductAvailability(products);
 if (!productStatus.allAvailable) {
 return res.status(400).json({ error: 'Some products are unavailable' });
 }
 // Create the order
 const order = await createOrder(userId, products, user.shippingAddress);
 res.status(201).json(order);
 } catch (error) {
 console.error('Order creation failed:', error);
 res.status(500).json({ error: 'Failed to create order' });
}
});

Note

Synchronous communication creates direct dependencies between services.

If the called service is down or slow, it affects the calling service, potentially causing cascading failures.

Asynchronous Communication

Services communicate through message brokers or event buses without waiting for immediate responses:

  • Message Queues : RabbitMQ, ActiveMQ for point-to-point messaging
  • Pub/Sub : Kafka, Redis Pub/Sub for publishing messages to multiple subscribers
  • Event Streaming : Kafka, AWS Kinesis for handling data streams

Example: Event-Driven Communication with an Event Bus

// order-service.js publishing an event
const axios = require('axios');
async function publishEvent(eventType, data) {
 try {
 await axios.post('http://event-bus:3100/events', {
 type: eventType,
 data: data,
 source: 'order-service',
 timestamp: new Date().toISOString()
 });
 console.log(`Published event: ${eventType}`);
 } catch (error) {
 console.error(`Failed to publish event ${eventType}:`, error.message);
 // Store failed events for retry
 storeFailedEvent(eventType, data, error);
}
}
// Create an order and publish event
app.post('/orders', async (req, res) => {
 try {
 const order = await createOrder(req.body);
 // Publish event for other services
 await publishEvent('order.created', order);
 res.status(201).json(order);
 } catch (error) {
 res.status(500).json({ error: 'Order creation failed' });
}
});

Handling Service Failures

In microservices, you need strategies for handling communication failures:

PatternDescriptionWhen to Use
Circuit BreakerTemporarily stops requests to failing services, preventing cascading failuresWhen services need protection from failing dependencies
Retry With BackoffAutomatically retries failed requests with increasing delaysFor transient failures that might resolve quickly
Timeout PatternSets maximum time to wait for responsesTo prevent blocking threads on slow services
Bulkhead PatternIsolates failures to prevent them from consuming all resourcesTo contain failures within components
Fallback PatternProvides alternative response when a service failsTo maintain basic functionality during failures

Example: Circuit Breaker Implementation

const CircuitBreaker = require('opossum');
// Configure the circuit breaker
const options = {
 failureThreshold: 50, // Open after 50% of requests fail
 resetTimeout: 10000, // Try again after 10 seconds
 timeout: 8080, // Time before request is considered failed
 errorThresholdPercentage: 50 // Error percentage to open circuit
};
// Create a circuit breaker for the user service
const getUserDetailsBreaker = new CircuitBreaker(getUserDetails, options);
// Add listeners for circuit state changes
getUserDetailsBreaker.on('open', () => {
 console.log('Circuit OPEN - User service appears to be down');
});
getUserDetailsBreaker.on('halfOpen', () => {
 console.log('Circuit HALF-OPEN - Testing user service');
});
getUserDetailsBreaker.on('close', () => {
 console.log('Circuit CLOSED - User service restored');
});
// Use the circuit breaker in the route handler
app.get('/orders/:orderId', async (req, res) => {
 const orderId = req.params.orderId;
 const order = await getOrderById(orderId);
 try {
 // Call the user service through the circuit breaker
 const user = await getUserDetailsBreaker.fire(order.userId);
 res.json({ order, user });
 } catch (error) {
 // If the circuit is open or the call fails, return fallback data
 console.error('Could not fetch user details:', error.message);
 res.json({
 order,
 user: { id: order.userId, name: 'User details unavailable' }
 });
}
});
try {
 const response = await axios.get(`http://user-service:8080/users/${userId}`);
 return response.data;
} catch (error) {
console.error('Error fetching user details:', error.message);
throw new Error('User service unavailable');
}
}
// Process an order
app.post('/orders', async (req, res) => {
 try {
 const { userId, products } = req.body;
 // Get user details from the user service
 const user = await getUserDetails(userId);
 // Create the order
 const order = {
 id: generateOrderId(),
 userId: userId,
 userEmail: user.email,
 products: products,
 total: calculateTotal(products),
 createdAt: new Date()
 };
 // Save order (simplified)
 saveOrder(order);
 res.status(201).json(order);
 } catch (error) {
 res.status(500).json({ error: error.message });
}
});

Asynchronous Communication

Services communicate through message brokers or event buses:

  • Message Queues : RabbitMQ, ActiveMQ
  • Streaming Platforms : Apache Kafka, AWS Kinesis
  • Event Buses : Redis Pub/Sub, NATS

Example: Asynchronous Communication with RabbitMQ

// order-service.js publishing an event
const amqp = require('amqplib');
async function publishOrderCreated(order) {
 try {
 const connection = await amqp.connect('amqp://localhost');
 const channel = await connection.createChannel();
 const exchange = 'order_events';
 await channel.assertExchange(exchange, 'topic', { durable: true });
 const routingKey = 'order.created';
 const message = JSON.stringify(order);
 channel.publish(exchange, routingKey, Buffer.from(message));
 console.log(`Published order created event for order ${order.id}`);
 setTimeout(() => connection.close(), 500);
 } catch (error) {
 console.error('Error publishing event:', error);
}
}
// notification-service.js consuming the event
async function setupOrderCreatedConsumer() {
 const connection = await amqp.connect('amqp://localhost');
 const channel = await connection.createChannel();
 const exchange = 'order_events';
 await channel.assertExchange(exchange, 'topic', { durable: true });
 const queue = 'notification_service_orders';
 await channel.assertQueue(queue, { durable: true });
 await channel.bindQueue(queue, exchange, 'order.created');
 channel.consume(queue, (msg) => {
 if (msg) {
 const order = JSON.parse(msg.content.toString());
 console.log(`Sending order confirmation email for order ${order.id}`);
 sendOrderConfirmationEmail(order);
 channel.ack(msg);
 }
 });
}

Best Practice: For operations that don't need immediate responses, use asynchronous messaging to improve resilience and reduce coupling between services.

API Gateway Pattern

An API Gateway acts as a single entry point for all client requests to a microservices architecture.

Responsibilities of an API Gateway

  • Request Routing : Directs client requests to appropriate services
  • API Composition : Aggregates responses from multiple services
  • Protocol Translation : Converts between protocols (e.g., HTTP to gRPC)
  • Authentication & Authorization : Handles security concerns
  • Rate Limiting : Prevents abuse of the API
  • Monitoring & Logging : Provides visibility into API usage

Example: API Gateway Implementation

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();
const PORT = 8080;
// Add security headers
app.use(helmet());
// Apply rate limiting
const apiLimiter = rateLimit({
 windowMs: 15 * 60 * 1000, // 15 minutes
 max: 100, // limit each IP to 100 requests per windowMs
 message: 'Too many requests from this IP, please try again later'
});
app.use('/api/', apiLimiter);
// Authentication middleware
function authenticate(req, res, next) {
 const token = req.headers.authorization;
 if (!token) {
 return res.status(401).json({ error: 'Unauthorized' });
 }
 // Verify token logic would go here
 next();
}
// Service registry (hardcoded for simplicity)
const serviceRegistry = {
 userService: 'http://localhost:3001',
 productService: 'http://localhost:3002',
 orderService: 'http://localhost:3003'
};
// Define proxy middleware for each service
const userServiceProxy = createProxyMiddleware({
 target: serviceRegistry.userService,
 changeOrigin: true,
 pathRewrite: { '^/api/users': '/users' }
});
const productServiceProxy = createProxyMiddleware({
 target: serviceRegistry.productService,
 changeOrigin: true,
 pathRewrite: { '^/api/products': '/products' }
});
const orderServiceProxy = createProxyMiddleware({
 target: serviceRegistry.orderService,
 changeOrigin: true,
 pathRewrite: { '^/api/orders': '/orders' }
});
// Route requests to appropriate services
app.use('/api/users', authenticate, userServiceProxy);
app.use('/api/products', productServiceProxy);
app.use('/api/orders', authenticate, orderServiceProxy);
app.listen(PORT, () => console.log(`API Gateway running on port ${PORT}`));

Best Practice: Use a dedicated API Gateway like Kong , Netflix Zuul , or cloud solutions like AWS API Gateway in production environments instead of building your own.

Service Discovery

Service discovery enables microservices to find and communicate with each other dynamically without hardcoded endpoints.

Service Discovery Methods

MethodDescription
Client-Side DiscoveryClients query a service registry to find service locations and load balance requests themselves
Server-Side DiscoveryClients call a router/load balancer which handles discovering service instances
DNS-Based DiscoveryServices are discovered via DNS SRV records or similar technologies

Example: Client-Side Service Discovery

const axios = require('axios');
// Simple service registry client
class ServiceRegistry {
 constructor(registryUrl) {
 this.registryUrl = registryUrl;
 this.servicesCache = {};
 this.cacheTimeout = 60000; // 1 minute
 }
 async getService(name) {
 // Check cache first
 const cachedService = this.servicesCache[name];
 if (cachedService && cachedService.expiresAt > Date.now()) {
 return this._selectInstance(cachedService.instances);
 }
 // Fetch from registry if not in cache or expired
 try {
 const response = await axios.get(`${this.registryUrl}/services/${name}`);
 const instances = response.data.instances;
 if (!instances || instances.length === 0) {
 throw new Error(`No instances found for service: ${name}`);
 }
 // Update cache
 this.servicesCache[name] = {
 instances,
 expiresAt: Date.now() + this.cacheTimeout
 };
 return this._selectInstance(instances);
 } catch (error) {
 console.error(`Error fetching service ${name}:`, error.message);
 throw new Error(`Service discovery failed for ${name}`);
 }
}
// Simple round-robin load balancing
_selectInstance(instances) {
 if (!instances._lastIndex) {
 instances._lastIndex = 0;
 } else {
 instances._lastIndex = (instances._lastIndex + 1) % instances.length;
}
return instances[instances._lastIndex];
}
}
// Usage example
const serviceRegistry = new ServiceRegistry('http://registry:8500/v1');
async function callUserService(userId) {
 try {
 const serviceInstance = await serviceRegistry.getService('user-service');
 const response = await axios.get(`${serviceInstance.url}/users/${userId}`);
 return response.data;
 } catch (error) {
 console.error('Error calling user service:', error.message);
 throw error;
}
}

Popular Service Discovery Tools

  • Consul : Service discovery and configuration
  • etcd : Distributed key-value store
  • ZooKeeper : Centralized service for configuration and synchronization
  • Eureka : REST-based service discovery for the AWS cloud
  • Kubernetes Service Discovery : Built-in service discovery for Kubernetes

Data Management Strategies

Managing data in a microservices architecture requires different approaches than monolithic applications.

Database Per Service

Each microservice has its own dedicated database, ensuring loose coupling and independent scaling.

Note

The Database Per Service pattern allows each service to choose the most appropriate database technology for its needs (SQL, NoSQL, Graph DB, etc.).

Distributed Transactions

Maintaining data consistency across services without ACID transactions requires special patterns:

A sequence of local transactions where each transaction updates data within a single service. Each local transaction publishes an event that triggers the next transaction.

Next

Node.js WebAssembly