Microservices Architecture: When to Use and When to Avoid
Backend26 de noviembre de 202520 min de lectura1 vistas
MicroservicesArchitectureBackendDevOpsSystem DesignScalability
Compartir:
Microservices Architecture: When to Use and When to Avoid
Microservices are everywhere, but are they right for your project? Let's explore the architecture, patterns, and real-world considerations.
What Are Microservices?
Microservices architecture breaks down applications into small, independent services that:
- Run in their own process
- Communicate via APIs (HTTP, gRPC, message queues)
- Can be deployed independently
- Are organized around business capabilities
Monolith vs Microservices
Monolithic Architecture
┌─────────────────────────────┐
│ Single Application │
│ ┌─────────────────────┐ │
│ │ User Management │ │
│ ├─────────────────────┤ │
│ │ Order Processing │ │
│ ├─────────────────────┤ │
│ │ Payment Processing │ │
│ ├─────────────────────┤ │
│ │ Inventory │ │
│ └─────────────────────┘ │
│ Single Database │
└─────────────────────────────┘
Pros:
- ✅ Simple to develop
- ✅ Easy to test
- ✅ Simple deployment
- ✅ No network overhead
Cons:
- ❌ Tight coupling
- ❌ Difficult to scale
- ❌ Technology lock-in
- ❌ Large codebase
Microservices Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │ DB │ │ │ │ DB │ │ │ │ DB │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└─────────────────┴──────────────────┘
API Gateway
Pros:
- ✅ Independent deployment
- ✅ Technology flexibility
- ✅ Easy to scale
- ✅ Fault isolation
Cons:
- ❌ Complex infrastructure
- ❌ Network latency
- ❌ Data consistency challenges
- ❌ Testing complexity
Microservices Patterns
1. API Gateway Pattern
// API Gateway (Express.js) const express = require('express'); const axios = require('axios'); const app = express(); // Route requests to appropriate services app.get('/api/users/:id', async (req, res) => { const user = await axios.get(`http://user-service:3001/users/${req.params.id}`); res.json(user.data); }); app.get('/api/orders/:id', async (req, res) => { const order = await axios.get(`http://order-service:3002/orders/${req.params.id}`); res.json(order.data); }); app.listen(3000);
2. Service Discovery Pattern
// Using Consul for service discovery const Consul = require('consul'); const consul = new Consul({ host: 'consul-server', port: 8500 }); // Register service consul.agent.service.register({ name: 'user-service', address: 'localhost', port: 3001, check: { http: 'http://localhost:3001/health', interval: '10s' } }); // Discover service async function getServiceUrl(serviceName) { const services = await consul.health.service(serviceName); return `http://${services[0].Service.Address}:${services[0].Service.Port}`; }
3. Circuit Breaker Pattern
const CircuitBreaker = require('opossum'); // Protect against cascading failures const options = { timeout: 3000, errorThresholdPercentage: 50, resetTimeout: 30000 }; const breaker = new CircuitBreaker(callExternalService, options); breaker.fallback(() => ({ status: 'error', message: 'Service temporarily unavailable' })); async function callExternalService(data) { const response = await axios.post('http://payment-service/charge', data); return response.data; } // Use the circuit breaker const result = await breaker.fire({ amount: 100, currency: 'USD' });
4. Event-Driven Architecture
// Publisher (Order Service) const amqp = require('amqplib'); async function publishOrderCreated(order) { const connection = await amqp.connect('amqp://rabbitmq'); const channel = await connection.createChannel(); await channel.assertExchange('orders', 'topic', { durable: true }); channel.publish( 'orders', 'order.created', Buffer.from(JSON.stringify(order)) ); console.log('Published order created event'); } // Subscriber (Inventory Service) async function subscribeToOrderEvents() { const connection = await amqp.connect('amqp://rabbitmq'); const channel = await connection.createChannel(); await channel.assertExchange('orders', 'topic', { durable: true }); const queue = await channel.assertQueue('', { exclusive: true }); await channel.bindQueue(queue.queue, 'orders', 'order.created'); channel.consume(queue.queue, (msg) => { const order = JSON.parse(msg.content.toString()); console.log('Processing order:', order); // Update inventory updateInventory(order.items); channel.ack(msg); }); }
5. Saga Pattern (Distributed Transactions)
// Orchestration-based Saga class OrderSaga { async execute(orderData) { try { // Step 1: Create order const order = await this.orderService.create(orderData); // Step 2: Reserve inventory await this.inventoryService.reserve(order.items); // Step 3: Process payment await this.paymentService.charge(order.total); // Step 4: Confirm order await this.orderService.confirm(order.id); return { success: true, orderId: order.id }; } catch (error) { // Compensating transactions (rollback) await this.inventoryService.unreserve(order.items); await this.orderService.cancel(order.id); return { success: false, error: error.message }; } } }
Database Strategies
Database per Service
┌──────────────┐ ┌──────────────┐
│User Service │ │Order Service │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ Users │ │ │ │ Orders │ │
│ │ DB │ │ │ │ DB │ │
│ └────────┘ │ │ └────────┘ │
└──────────────┘ └──────────────┘
Pros:
- ✅ Loose coupling
- ✅ Independent scaling
Cons:
- ❌ No ACID transactions
- ❌ Data duplication
Shared Database (Anti-pattern)
┌──────────────┐ ┌──────────────┐
│User Service │ │Order Service │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬───────────┘
│
┌──────▼──────┐
│ Shared │
│ Database │
└─────────────┘
Why avoid?
- ❌ Tight coupling
- ❌ Schema changes affect all services
- ❌ Defeats microservices purpose
Communication Patterns
Synchronous (REST/gRPC)
// REST API call const user = await fetch('http://user-service/users/123') .then(res => res.json()); // gRPC call const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); const packageDefinition = protoLoader.loadSync('user.proto'); const userProto = grpc.loadPackageDefinition(packageDefinition).user; const client = new userProto.UserService( 'user-service:50051', grpc.credentials.createInsecure() ); client.getUser({ id: 123 }, (error, user) => { console.log(user); });
Asynchronous (Message Queue)
// RabbitMQ const amqp = require('amqplib'); // Send message async function sendMessage(queue, message) { const connection = await amqp.connect('amqp://rabbitmq'); const channel = await connection.createChannel(); await channel.assertQueue(queue); channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); } // Receive message async function receiveMessages(queue, handler) { const connection = await amqp.connect('amqp://rabbitmq'); const channel = await connection.createChannel(); await channel.assertQueue(queue); channel.consume(queue, (msg) => { handler(JSON.parse(msg.content.toString())); channel.ack(msg); }); }
Deployment Strategies
Docker Compose (Development)
version: '3.8' services: user-service: build: ./user-service ports: - "3001:3001" environment: - DATABASE_URL=postgres://db:5432/users order-service: build: ./order-service ports: - "3002:3002" environment: - DATABASE_URL=postgres://db:5432/orders api-gateway: build: ./api-gateway ports: - "3000:3000" depends_on: - user-service - order-service
Kubernetes (Production)
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: user-service:latest ports: - containerPort: 3001 env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-secrets key: user-db-url --- apiVersion: v1 kind: Service metadata: name: user-service spec: selector: app: user-service ports: - port: 80 targetPort: 3001
When to Use Microservices
✅ Use when:
- Large, complex application
- Multiple independent teams
- Different scaling requirements per feature
- Need for technology diversity
- High availability requirements
❌ Avoid when:
- Small application or startup
- Small team (< 5 developers)
- Tight deadlines
- Limited DevOps expertise
- Simple CRUD application
Migration Strategy
Strangler Fig Pattern
Phase 1: Monolith
┌─────────────────┐
│ Monolith │
└─────────────────┘
Phase 2: Extract first service
┌─────────────┐ ┌──────────┐
│ Monolith │ │ User │
│ (minus │ │ Service │
│ users) │ └──────────┘
└─────────────┘
Phase 3: Extract more services
┌─────────┐ ┌────────┐ ┌─────────┐
│Monolith │ │ User │ │ Order │
│ (core) │ │Service │ │ Service │
└─────────┘ └────────┘ └─────────┘
Conclusion
Microservices aren't a silver bullet:
- Start simple with monolith
- Extract services when needed
- Use proven patterns (API Gateway, Circuit Breaker, Saga)
- Invest in DevOps (CI/CD, monitoring, logging)
- Plan for failure (resilience, fallbacks)
Remember: Complexity is a feature, not a bug. Only add it when you need it! 🎯