Microservices Architecture: Design Patterns and Best Practices
Last updated: October 2025 โข Reviewed by system architects
Microservices aren't just a buzzword - they're a fundamental shift in how we build software. I've seen monolithic applications that took 30 minutes to deploy become dozens of microservices that deploy independently in seconds. But I've also seen teams jump into microservices without understanding the complexity and create a distributed mess that's harder to manage than the monolith ever was.
This isn't a theoretical guide. We'll cover real patterns used by Netflix, Uber, and Amazon. You'll learn when to use microservices (and when not to), how to design service boundaries, and critically - how to secure distributed systems that have orders of magnitude more attack surface than monoliths.
Monolith
Single deployable unit
Service-Oriented
Large services with shared DB
Microservices
Small, independent services
๐คWhat Are Microservices Really?
A microservice is a small, autonomous service that focuses on doing one thing well. Each service runs in its own process, has its own database, and communicates with other services over the network. Think of them as mini-applications that work together to form your complete system.
โ When NOT to Use Microservices:
- โข Small team (under 10 people)
- โข MVP or early-stage product
- โข Unclear domain boundaries
- โข Limited DevOps expertise
- โข Simple CRUD application
โ When to Use Microservices:
- โข Large, complex applications
- โข Multiple teams working independently
- โข Different scaling requirements per feature
- โข Need for technology diversity
- โข Continuous deployment required
๐จCore Design Patterns
1. API Gateway Pattern
Single entry point for all clients. Handles routing, authentication, rate limiting, and aggregation.
// API Gateway routes requests to microservices
// Client โ API Gateway โ Microservices
// Express.js API Gateway example
const gateway = express();
// Authentication middleware
gateway.use(async (req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
req.user = await verifyToken(token);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
// Route to User Service
gateway.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
}));
// Route to Order Service
gateway.use('/api/orders', createProxyMiddleware({
target: 'http://order-service:3002',
changeOrigin: true
}));
// Aggregate data from multiple services
gateway.get('/api/dashboard', async (req, res) => {
const [user, orders, stats] = await Promise.all([
fetch('http://user-service:3001/api/profile'),
fetch('http://order-service:3002/api/orders'),
fetch('http://analytics-service:3003/api/stats')
]);
res.json({
user: await user.json(),
orders: await orders.json(),
stats: await stats.json()
});
});2. Service Discovery Pattern
Services register themselves and discover other services dynamically. Essential for cloud deployments.
// Using Consul for service discovery
const Consul = require('consul');
const consul = new Consul();
// Service registers itself on startup
async function registerService() {
await consul.agent.service.register({
name: 'user-service',
id: 'user-service-1',
address: process.env.SERVICE_HOST,
port: parseInt(process.env.SERVICE_PORT),
check: {
http: `http://${process.env.SERVICE_HOST}:${process.env.SERVICE_PORT}/health`,
interval: '10s'
}
});
}
// Discover and call another service
async function callOrderService(endpoint) {
// Get healthy instances of order-service
const { services } = await consul.health.service({
service: 'order-service',
passing: true
});
if (services.length === 0) {
throw new Error('Order service unavailable');
}
// Load balance - pick random instance
const service = services[Math.floor(Math.random() * services.length)];
const url = `http://${service.Service.Address}:${service.Service.Port}${endpoint}`;
return fetch(url);
}3. Event-Driven Architecture
Services communicate through events instead of direct calls. Enables loose coupling and scalability.
// Using RabbitMQ/Kafka for event-driven communication
// Order Service publishes event
async function createOrder(orderData) {
const order = await db.orders.create(orderData);
// Publish event - don't wait for other services
await eventBus.publish('order.created', {
orderId: order.id,
userId: order.userId,
total: order.total,
timestamp: new Date()
});
return order;
}
// Inventory Service listens for events
eventBus.subscribe('order.created', async (event) => {
await db.inventory.decrementStock(event.items);
console.log(`Stock updated for order ${event.orderId}`);
});
// Email Service listens for events
eventBus.subscribe('order.created', async (event) => {
await sendEmail(event.userId, 'Order Confirmation', event);
console.log(`Confirmation sent for order ${event.orderId}`);
});
// Benefits: Services don't know about each other
// Easy to add new services that react to events๐Microservices Security: The Hidden Complexity
Here's the uncomfortable truth about microservices security: every service boundary is a potential security vulnerability. In a monolith, you have one perimeter to defend. With microservices, you have dozens or hundreds. Each service-to-service call is a network request that can be intercepted, each service needs authentication and authorization, and a compromised service can become a foothold for attackers to pivot through your entire system.
I've audited microservices architectures where internal services trusted each other completely with no authentication. One compromised container gave attackers access to everything. Another company had services communicating over unencrypted HTTP within their "secure" private network - until an insider threat exploited it. These aren't theoretical risks; they're real breaches that cost millions.
The attack surface explosion is real. A monolith might have 5 external APIs to secure. The same application split into 20 microservices now has 20 services ร 19 potential service-to-service calls = 380 communication paths to secure. Each one needs encryption, authentication, authorization, rate limiting, and monitoring. Miss one, and you have a security hole.
Zero Trust Architecture: Never Trust, Always Verify
The old "trust the internal network" model doesn't work with microservices. You need zero trust: assume every request is potentially malicious, even if it comes from inside your network. Every service must authenticate and authorize every request, no exceptions.
// Service-to-service authentication with JWT
const jwt = require('jsonwebtoken');
// Each service has its own service account token
const SERVICE_TOKEN = process.env.SERVICE_TOKEN;
const SERVICE_SECRET = process.env.JWT_SECRET;
// Middleware to verify calling service
function authenticateService(req, res, next) {
const token = req.headers['x-service-token'];
if (!token) {
return res.status(401).json({
error: 'Service authentication required'
});
}
try {
const decoded = jwt.verify(token, SERVICE_SECRET);
// Verify the service is allowed to call this endpoint
if (!isAuthorized(decoded.service, req.path)) {
return res.status(403).json({
error: 'Service not authorized for this operation'
});
}
req.callingService = decoded.service;
next();
} catch (error) {
return res.status(401).json({
error: 'Invalid service token'
});
}
}
// Service makes authenticated call to another service
async function callUserService(userId) {
const response = await fetch(`http://user-service/api/users/${userId}`, {
headers: {
'X-Service-Token': SERVICE_TOKEN,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Service call failed');
}
return response.json();
}Service Mesh: Security at the Infrastructure Layer
Service meshes like Istio and Linkerd handle security concerns at the infrastructure level, so you don't have to implement them in every service. They provide automatic mTLS encryption, authentication, authorization policies, and traffic management. This is the industry standard for securing large microservices deployments.
# Istio Authorization Policy Example
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: order-service-authz
namespace: production
spec:
selector:
matchLabels:
app: order-service
action: ALLOW
rules:
# Only API gateway can call order service
- from:
- source:
principals: ["cluster.local/ns/production/sa/api-gateway"]
to:
- operation:
methods: ["GET", "POST"]
paths: ["/api/orders/*"]
# User service can only call specific endpoints
- from:
- source:
principals: ["cluster.local/ns/production/sa/user-service"]
to:
- operation:
methods: ["GET"]
paths: ["/api/orders/user/*"]
# This is enforced at the network level by Istio
# Services don't need to implement authorization logicRate Limiting and Circuit Breaking
Distributed systems are vulnerable to cascading failures. One slow service can bring down your entire system. Rate limiting prevents services from overwhelming each other, and circuit breakers stop the cascade before it starts.
// Circuit breaker pattern with retry logic
const CircuitBreaker = require('opossum');
// Configure circuit breaker
const options = {
timeout: 3000, // 3 second timeout
errorThresholdPercentage: 50, // Open after 50% failures
resetTimeout: 30000 // Try again after 30 seconds
};
// Wrap service call in circuit breaker
const callOrderService = new CircuitBreaker(async (orderId) => {
const response = await fetch(`http://order-service/api/orders/${orderId}`);
if (!response.ok) throw new Error('Service error');
return response.json();
}, options);
// Handle circuit breaker states
callOrderService.on('open', () => {
console.error('Circuit breaker opened - service is failing');
// Alert operations team
});
callOrderService.on('halfOpen', () => {
console.log('Circuit breaker testing service recovery');
});
// Use with fallback
async function getOrder(orderId) {
try {
return await callOrderService.fire(orderId);
} catch (error) {
// Circuit is open or service failed
// Return cached data or default response
return getCachedOrder(orderId) || {
error: 'Service temporarily unavailable',
orderId
};
}
}Security in microservices is about defense in depth. No single measure is enough. You need authentication between services, encrypted communication, authorization policies, rate limiting, circuit breakers, comprehensive logging, and monitoring. It's more work than a monolith, but it's the price of distributed systems. Automate what you can with service meshes and API gateways, and make security a requirement from day one, not an afterthought.
๐Deployment and Orchestration
Kubernetes has become the standard for deploying microservices. Here's a minimal setup:
# kubernetes deployment for a microservice
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: myregistry/user-service:v1.2.0
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 3000
type: ClusterIPโ Microservices Architecture Checklist
๐ฏ Final Thoughts
Microservices are powerful but complex. Start with a monolith, identify service boundaries as your application grows, and migrate gradually. Don't split services just because you can - split them when you have a clear reason (different scaling needs, team boundaries, deployment independence). And never forget: the network is not reliable, security is 10x harder, and debugging is a completely different game. Plan accordingly.
About the Team
Written by DevMetrix's architecture team with experience building and scaling microservices at companies from startups to Fortune 500 enterprises. We've seen what works and what fails in production.
โ System architects โข โ Updated October 2025 โข โ Battle-tested patterns
๐งชTest Your Microservices
Use our API tester to verify communication between your microservices. Test authentication, check response times, and ensure your services are properly integrated.
Try API Tester โ