Microservices Architecture: A Complete Guide
Microservices Architecture: A Complete Guide
Microservices architecture has become the go-to approach for building large-scale, distributed systems. This comprehensive guide covers everything you need to know to implement microservices effectively.
What are Microservices?
Microservices are an architectural approach where applications are built as a collection of loosely coupled, independently deployable services. Each service is responsible for a specific business capability.
Key Characteristics
- Single Responsibility: Each service has one business function
- Independence: Services can be developed, deployed, and scaled independently
- Decentralized: No single point of failure
- Technology Diversity: Each service can use different technologies
When to Use Microservices
Benefits
✅ Scalability: Scale individual services based on demand ✅ Technology Flexibility: Use the best tool for each job ✅ Team Autonomy: Independent development teams ✅ Fault Isolation: Failure in one service doesn't bring down the entire system
Drawbacks
❌ Complexity: Increased operational overhead ❌ Network Latency: Inter-service communication adds latency ❌ Data Consistency: Distributed transactions are challenging ❌ Testing: End-to-end testing becomes more complex
Microservices Patterns
API Gateway Pattern
// API Gateway implementation const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const rateLimit = require('express-rate-limit'); const app = express(); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit each IP to 100 requests per windowMs }); app.use(limiter); // Service routing app.use('/api/users', createProxyMiddleware({ target: 'http://user-service:3001', changeOrigin: true, pathRewrite: { '^/api/users': '', // remove /api/users prefix }, })); app.use('/api/orders', createProxyMiddleware({ target: 'http://order-service:3002', changeOrigin: true, pathRewrite: { '^/api/orders': '', }, })); app.listen(3000, () => { console.log('API Gateway running on port 3000'); });
Java/Spring Boot:
// API Gateway with Spring Cloud Gateway @RestController @RequestMapping("/api") public class ApiGatewayController { @Autowired private UserServiceClient userServiceClient; @Autowired private OrderServiceClient orderServiceClient; @GetMapping("/users/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { User user = userServiceClient.getUser(id); return ResponseEntity.ok(user); } @GetMapping("/orders/{id}") public ResponseEntity<Order> getOrder(@PathVariable Long id) { Order order = orderServiceClient.getOrder(id); return ResponseEntity.ok(order); } } // Gateway configuration @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("user-service", r -> r.path("/api/users/**") .filters(f -> f.stripPrefix(2)) .uri("http://user-service:3001")) .route("order-service", r -> r.path("/api/orders/**") .filters(f -> f.stripPrefix(2)) .uri("http://order-service:3002")) .build(); } }
Service Discovery
// Service registry implementation const express = require('express'); const app = express(); const services = new Map(); // Register service app.post('/register', (req, res) => { const { serviceName, serviceUrl, healthCheck } = req.body; services.set(serviceName, { url: serviceUrl, healthCheck, lastHeartbeat: Date.now(), }); res.json({ message: 'Service registered successfully' }); }); // Discover service app.get('/discover/:serviceName', (req, res) => { const service = services.get(req.params.serviceName); if (!service) { return res.status(404).json({ error: 'Service not found' }); } res.json(service); }); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', services: services.size }); });
Circuit Breaker Pattern
// Circuit breaker implementation class CircuitBreaker { constructor(threshold = 5, timeout = 60000) { this.threshold = threshold; this.timeout = timeout; this.failureCount = 0; this.lastFailureTime = null; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN } async call(serviceFunction) { if (this.state === 'OPEN') { if (Date.now() - this.lastFailureTime > this.timeout) { this.state = 'HALF_OPEN'; } else { throw new Error('Circuit breaker is OPEN'); } } try { const result = await serviceFunction(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; } onFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.threshold) { this.state = 'OPEN'; } } } // Usage const userServiceBreaker = new CircuitBreaker(3, 30000); app.get('/api/user/:id', async (req, res) => { try { const user = await userServiceBreaker.call(() => fetch(`http://user-service:3001/users/${req.params.id}`) ); res.json(user); } catch (error) { res.status(500).json({ error: 'Service temporarily unavailable' }); } });
Data Management
Database per Service
// User service with its own database const express = require('express'); const { PrismaClient } = require('@prisma/client'); const app = express(); const prisma = new PrismaClient(); // User service endpoints app.get('/users/:id', async (req, res) => { try { const user = await prisma.user.findUnique({ where: { id: req.params.id }, select: { id: true, email: true, name: true, // Don't expose internal fields } }); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } }); app.post('/users', async (req, res) => { try { const user = await prisma.user.create({ data: req.body, }); res.status(201).json(user); } catch (error) { res.status(400).json({ error: 'Invalid user data' }); } });
Event Sourcing
// Event store implementation const EventStore = { events: [], append(streamId, event) { const eventRecord = { streamId, eventId: generateId(), eventType: event.constructor.name, eventData: event, timestamp: new Date(), version: this.getNextVersion(streamId), }; this.events.push(eventRecord); return eventRecord; }, getEvents(streamId) { return this.events.filter(e => e.streamId === streamId); }, getNextVersion(streamId) { const streamEvents = this.getEvents(streamId); return streamEvents.length; } }; // Event classes class UserCreated { constructor(userId, email, name) { this.userId = userId; this.email = email; this.name = name; } } class UserUpdated { constructor(userId, changes) { this.userId = userId; this.changes = changes; } } // Usage in user service app.post('/users', async (req, res) => { const userId = generateId(); const event = new UserCreated(userId, req.body.email, req.body.name); EventStore.append('users', event); res.status(201).json({ userId, message: 'User created' }); });
Communication Patterns
Synchronous Communication
// HTTP client with retry logic const axios = require('axios'); class ServiceClient { constructor(baseURL, retries = 3) { this.baseURL = baseURL; this.retries = retries; } async request(method, path, data = null) { let lastError; for (let i = 0; i < this.retries; i++) { try { const response = await axios({ method, url: `${this.baseURL}${path}`, data, timeout: 5000, }); return response.data; } catch (error) { lastError = error; if (i < this.retries - 1) { await this.delay(Math.pow(2, i) * 1000); // Exponential backoff } } } throw lastError; } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Usage const userService = new ServiceClient('http://user-service:3001'); const orderService = new ServiceClient('http://order-service:3002');
Asynchronous Communication
// Message queue with Redis const redis = require('redis'); const client = redis.createClient(); class MessageQueue { async publish(topic, message) { await client.lpush(`queue:${topic}`, JSON.stringify(message)); } async subscribe(topic, handler) { while (true) { const message = await client.brpop(`queue:${topic}`, 0); if (message) { const data = JSON.parse(message[1]); await handler(data); } } } } // Event handlers const messageQueue = new MessageQueue(); // Order service publishes events app.post('/orders', async (req, res) => { const order = await createOrder(req.body); // Publish order created event await messageQueue.publish('order.created', { orderId: order.id, userId: order.userId, total: order.total, }); res.json(order); }); // User service subscribes to events messageQueue.subscribe('order.created', async (data) => { // Update user's order history await updateUserOrderHistory(data.userId, data.orderId); });
Monitoring and Observability
Distributed Tracing
// OpenTelemetry setup const { NodeSDK } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const sdk = new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); // Custom tracing const { trace } = require('@opentelemetry/api'); app.get('/api/orders/:id', async (req, res) => { const tracer = trace.getTracer('order-service'); return tracer.startActiveSpan('get-order', async (span) => { try { span.setAttributes({ 'order.id': req.params.id, 'user.id': req.user.id, }); const order = await getOrder(req.params.id); span.setStatus({ code: 1 }); // OK res.json(order); } catch (error) { span.setStatus({ code: 2, message: error.message }); // ERROR res.status(500).json({ error: 'Internal server error' }); } finally { span.end(); } }); });
Health Checks
// Comprehensive health check app.get('/health', async (req, res) => { const health = { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), services: {} }; // Check database try { await prisma.$queryRaw`SELECT 1`; health.services.database = 'healthy'; } catch (error) { health.services.database = 'unhealthy'; health.status = 'unhealthy'; } // Check Redis try { await client.ping(); health.services.redis = 'healthy'; } catch (error) { health.services.redis = 'unhealthy'; health.status = 'unhealthy'; } // Check external services try { await userService.request('GET', '/health'); health.services.userService = 'healthy'; } catch (error) { health.services.userService = 'unhealthy'; health.status = 'unhealthy'; } const statusCode = health.status === 'healthy' ? 200 : 503; res.status(statusCode).json(health); });
Deployment Strategies
Docker Containerization
# Dockerfile for microservice FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["npm", "start"]
Docker Compose
# docker-compose.yml version: '3.8' services: api-gateway: build: ./api-gateway ports: - "3000:3000" environment: - USER_SERVICE_URL=http://user-service:3001 - ORDER_SERVICE_URL=http://order-service:3002 depends_on: - user-service - order-service user-service: build: ./user-service ports: - "3001:3001" environment: - DATABASE_URL=postgresql://user:password@postgres:5432/users depends_on: - postgres order-service: build: ./order-service ports: - "3002:3002" environment: - DATABASE_URL=postgresql://user:password@postgres:5432/orders depends_on: - postgres postgres: image: postgres:15 environment: - POSTGRES_DB=microservices - POSTGRES_USER=user - POSTGRES_PASSWORD=password volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data:
Best Practices
1. Service Boundaries
- Design services around business capabilities
- Keep services loosely coupled
- Avoid shared databases
- Use events for communication
2. Data Consistency
- Implement eventual consistency
- Use saga pattern for distributed transactions
- Handle failures gracefully
- Monitor data consistency
3. Security
- Implement service-to-service authentication
- Use API gateways for external access
- Encrypt data in transit and at rest
- Regular security audits
4. Testing
- Unit tests for each service
- Integration tests for service interactions
- Contract testing between services
- End-to-end tests for critical paths
Conclusion
Microservices architecture offers significant benefits for large-scale applications, but it also introduces complexity. Success requires careful planning, proper tooling, and a team that understands distributed systems.
Remember: Start with a monolith, identify natural boundaries, and extract services gradually. Microservices are not a silver bullet—they're a tool for specific problems.
Related Articles
Incident Playbook for Beginners: Real-World Monitoring and Troubleshooting Stories
A story-driven, plain English incident playbook for new backend & SRE engineers. Find, fix, and prevent outages with empathy and practical steps.
System Design Power-Guide 2025: What To Learn, In What Order, With Real-World Links
Stop bookmarking random threads. This is a tight, no-fluff map of what to study for system design in 2025 - what each topic is, why it matters in interviews and production, and where to go deeper.
DSA Patterns Master Guide: How To Identify Problems, Pick Patterns, and Practice (With LeetCode Sets)
A practical, pattern-first road map for entry-level engineers. Learn how to identify the right pattern quickly, apply a small algorithm template, know variants and pitfalls, and practice with curated LeetCode problems.