Sumário
- Por que Microsserviços? Monolítico vs Microsserviços
- Princípios de Design de Microsserviços
- Estrutura de um Ecossistema de Microsserviços
- Comunicação Síncrona: REST e gRPC
- Comunicação Assíncrona: Mensageria com RabbitMQ e Kafka
- API Gateway: O Ponto de Entrada Único
- Service Discovery e Load Balancing
- Event Sourcing e CQRS na Prática
- Saga Pattern: Transações Distribuídas
- Observabilidade: Logging, Tracing e Métricas
- Segurança em Arquiteturas Distribuídas
- Containerização com Docker
- Orquestração com Kubernetes
- CI/CD para Microsserviços
- Antipadrões e Como Evitá-los
- Conclusão: Quando Usar Microsserviços?
1. Por que Microsserviços? Monolítico vs Microsserviços {#porque}
A decisão de adotar uma arquitetura de microsserviços é uma das mais impactantes que uma equipe de engenharia pode tomar. Não é uma bala de prata, e adotá-la sem entender os trade-offs pode resultar em uma “distribuição de complexidade” que piora muito a situação.
A Evolução Natural: Do Monolítico ao Microsserviço
A grande maioria das aplicações bem-sucedidas começou como monolíticos. O Instagram, o Twitter inicial, o Shopify — todos começaram como aplicações monolíticas. E havia razões muito válidas para isso:
Vantagens do Monolítico:
- Simplicidade operacional: um único deploy, um único processo para monitorar
- Latência de comunicação zero entre módulos (chamadas in-process)
- Transações ACID nativas — sem complexidade de transações distribuídas
- Debug mais simples: toda a stack trace em um único lugar
- Curva de aprendizado menor para novos desenvolvedores
- Menor overhead de infraestrutura
Quando o Monolítico Começa a Doer:
O monolítico tende a se tornar problemático quando:
- Time-to-market: Múltiplos times modificando o mesmo código base criam conflitos de merge constantes, cerimônias de release complexas e medo de deployar.
- Escalabilidade seletiva: Se apenas o módulo de processamento de imagens precisa de mais recursos, você é obrigado a escalar o monolítico inteiro.
- Heterogeneidade tecnológica: Você quer usar Python para ML, Go para um serviço de alta performance e Node.js para o resto, mas o monolítico te prende em uma única stack.
- Disponibilidade: Um bug em um módulo derruba toda a aplicação.
- Tempo de build e teste: O conjunto de testes do monolítico demora 45 minutos para rodar, bloqueando o fluxo de desenvolvimento.
A Realidade dos Microsserviços
Microsserviços resolvem os problemas acima, mas introduzem uma nova classe de problemas:
Problemas introduzidos:
- Latência de rede entre serviços (em vez de chamadas in-process)
- Complexidade de transações distribuídas (não há mais ACID fácil)
- Desafios de debug (trace distribuído é complexo)
- Overhead operacional: dezenas de serviços para monitorar, deployar e manter
- Consistência eventual (em vez de consistência imediata)
- Gestão de versões de APIs entre serviços
- Overhead de infraestrutura significativamente maior
A Netflix tem uma equipe inteira de engenharia de confiabilidade de sites (SRE). A Amazon tem times de dezenas de engenheiros por serviço. Se a sua empresa não tem essa maturidade operacional, microsserviços prematuros podem ser desastrosos.
O Caminho do “Modular Monolith”
Uma abordagem intermediária muito válida é o Monolítico Modular: um único deployment, mas com fronteiras de módulos bem definidas internamente. Se precisar extrair um módulo como microsserviço no futuro, as fronteiras já estão claras.
┌─────────────────────────────────────────┐
│ API Gateway (Nginx) │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Modular Monolith │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Users │ │ Products │ │ Orders │ │
│ │ Module │ │ Module │ │ Module │ │
│ └──────────┘ └──────────┘ └─────────┘ │
│ ↕ Internal Events │
└─────────────────────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ PostgreSQL │
└─────────────────────────────────────────┘
2. Princípios de Design de Microsserviços {#principios}
Single Responsibility Principle no Nível de Serviço
Cada microsserviço deve ter uma única razão para mudar. Não é sobre o tamanho do código, mas sobre coesão de responsabilidade.
❌ Serviço muito genérico:
"backend-service" — gerencia usuários, produtos, pedidos, notificações, relatórios
✅ Serviços com responsabilidade clara:
"user-service" — gerencia identidade e perfil de usuários
"product-catalog-service" — gerencia catálogo de produtos
"order-service" — gerencia ciclo de vida de pedidos
"notification-service" — envia emails, push, SMS
"reporting-service" — gera relatórios e analytics
Bounded Contexts do Domain-Driven Design
O DDD (Domain-Driven Design) oferece as ferramentas conceituais para identificar fronteiras de microsserviços. Um Bounded Context é uma fronteira conceitual dentro da qual um modelo de domínio específico é definido e aplicável.
// O conceito de "User" é diferente em cada contexto:
// No contexto de Autenticação:
interface UserAuth {
id: string;
email: string;
passwordHash: string;
role: Role;
mfaEnabled: boolean;
}
// No contexto de Pedidos:
interface UserOrder {
id: string;
name: string;
email: string;
shippingAddresses: Address[];
defaultPaymentMethod: string;
}
// No contexto de Notificações:
interface UserNotification {
id: string;
email: string;
phone?: string;
preferences: NotificationPreferences;
timezone: string;
}
Autonomous Services: Database per Service
Um dos princípios mais importantes e frequentemente violados: cada microsserviço deve ter seu próprio banco de dados. Compartilhar banco de dados entre serviços cria acoplamento forte e elimina a autonomia.
❌ Banco de dados compartilhado (anti-pattern):
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Service │ │ Order Service│ │Catalog Service│
└──────┬───────┘ └──────┬───────┘ └──────┬────────┘
└──────────────────┼──────────────────┘
│
┌──────────▼──────────┐
│ Shared PostgreSQL │
└─────────────────────┘
✅ Database per Service (correto):
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Service │ │ Order Service│ │Catalog Service│
└──────┬───────┘ └──────┬───────┘ └──────┬────────┘
│ │ │
┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼────────┐
│ PostgreSQL │ │ MongoDB │ │ PostgreSQL │
│ (users db) │ │ (orders db) │ │ (catalog db) │
└──────────────┘ └──────────────┘ └────────────────┘
Design for Failure
Em sistemas distribuídos, falhas são inevitáveis. A questão não é se um serviço vai falhar, mas quando. Projetar para falha significa:
Circuit Breaker Pattern:
Quando um serviço dependente está falhando, o Circuit Breaker evita cascata de falhas “abrindo o circuito” após N falhas consecutivas.
Retry with Exponential Backoff:
async function callWithRetry(
fn: () => Promise,
maxRetries = 3,
baseDelay = 1000
): Promise {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
console.log(`Tentativa ${attempt} falhou. Aguardando ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
Timeout e Bulkhead Pattern:
// Timeout em chamadas externas
async function callWithTimeout(
fn: () => Promise,
timeoutMs: number
): Promise {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout após ${timeoutMs}ms`)), timeoutMs)
);
return Promise.race([fn(), timeout]);
}
3. Estrutura de um Ecossistema de Microsserviços {#estrutura}
Vamos construir um sistema de e-commerce real com microsserviços. A estrutura completa:
ecommerce-microservices/
├── services/
│ ├── api-gateway/ # Kong ou custom Express gateway
│ ├── user-service/ # Node.js + PostgreSQL
│ ├── product-catalog-service/ # Node.js + PostgreSQL + Elasticsearch
│ ├── order-service/ # Node.js + PostgreSQL
│ ├── payment-service/ # Node.js + PostgreSQL
│ ├── notification-service/ # Node.js (email/SMS)
│ ├── inventory-service/ # Node.js + PostgreSQL
│ └── search-service/ # Node.js + Elasticsearch
├── infrastructure/
│ ├── docker-compose.yml
│ ├── kubernetes/
│ │ ├── deployments/
│ │ ├── services/
│ │ ├── ingress/
│ │ └── configmaps/
│ ├── rabbitmq/
│ │ └── definitions.json
│ └── nginx/
│ └── nginx.conf
├── shared/
│ ├── packages/
│ │ ├── @ecommerce/events # Tipos de eventos compartilhados
│ │ ├── @ecommerce/logger # Logger configurado
│ │ └── @ecommerce/errors # Errors classes compartilhadas
│ └── proto/ # Protobuf schemas para gRPC
└── tools/
├── scripts/
└── local-dev/
Estrutura de um Serviço Individual
Cada microsserviço segue a mesma estrutura base para consistência:
user-service/
├── src/
│ ├── application/ # Casos de uso (Use Cases)
│ │ ├── commands/ # Write operations
│ │ │ ├── create-user/
│ │ │ │ ├── create-user.command.ts
│ │ │ │ ├── create-user.handler.ts
│ │ │ │ └── create-user.spec.ts
│ │ │ └── update-user/
│ │ └── queries/ # Read operations
│ │ └── get-user/
│ ├── domain/ # Lógica de domínio pura
│ │ ├── entities/
│ │ │ └── user.entity.ts
│ │ ├── value-objects/
│ │ │ ├── email.vo.ts
│ │ │ └── password.vo.ts
│ │ ├── events/
│ │ │ ├── user-created.event.ts
│ │ │ └── user-deleted.event.ts
│ │ └── repositories/
│ │ └── user.repository.interface.ts
│ ├── infrastructure/ # Implementações concretas
│ │ ├── database/
│ │ │ ├── prisma/
│ │ │ └── user.repository.impl.ts
│ │ ├── messaging/
│ │ │ ├── rabbitmq.connection.ts
│ │ │ └── user.publisher.ts
│ │ └── http/
│ │ ├── controllers/
│ │ └── routes/
│ └── shared/
│ ├── guards/
│ └── middlewares/
├── tests/
├── Dockerfile
├── package.json
└── prisma/
4. Comunicação Síncrona: REST e gRPC {#sincrona}
REST entre Microsserviços: Pontos de Atenção
Quando microsserviços se comunicam via REST, é preciso tratar uma série de questões que não existem em chamadas in-process:
// src/infrastructure/http/http-client.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
import CircuitBreaker from 'opossum';
export class HttpClient {
private client: AxiosInstance;
private breaker: CircuitBreaker;
constructor(
private baseUrl: string,
private serviceName: string
) {
this.client = axios.create({
baseURL: baseUrl,
timeout: 5000, // 5 segundos de timeout
headers: {
'Content-Type': 'application/json',
'X-Service-Name': 'user-service',
},
});
// Interceptor para adicionar trace ID (distributed tracing)
this.client.interceptors.request.use((config) => {
config.headers['X-Trace-ID'] = this.getTraceId();
return config;
});
// Circuit Breaker configuração
this.breaker = new CircuitBreaker(
async (fn: () => Promise) => fn(),
{
timeout: 5000,
errorThresholdPercentage: 50, // Abrir após 50% de falhas
resetTimeout: 30000, // Tentar fechar após 30s
volumeThreshold: 10, // Mínimo de chamadas para avaliar
}
);
this.breaker.on('open', () => {
console.error(`Circuit Breaker ABERTO para ${serviceName}`);
});
this.breaker.on('halfOpen', () => {
console.warn(`Circuit Breaker SEMI-ABERTO para ${serviceName}`);
});
this.breaker.on('close', () => {
console.info(`Circuit Breaker FECHADO para ${serviceName}`);
});
}
async get(path: string, params?: Record): Promise {
return this.breaker.fire(async () => {
const response = await this.client.get(path, { params });
return response.data;
});
}
async post(path: string, data: any): Promise {
return this.breaker.fire(async () => {
const response = await this.client.post(path, data);
return response.data;
});
}
private getTraceId(): string {
// Integrar com OpenTelemetry em produção
return `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
// Uso em um serviço:
// src/infrastructure/clients/product.client.ts
export class ProductClient {
private httpClient: HttpClient;
constructor() {
this.httpClient = new HttpClient(
process.env.PRODUCT_SERVICE_URL || 'http://product-service:3001',
'product-service'
);
}
async getProduct(productId: string): Promise {
return this.httpClient.get(`/products/${productId}`);
}
async checkInventory(productId: string, quantity: number): Promise {
const result = await this.httpClient.post<{ available: boolean }>(
'/inventory/check',
{ productId, quantity }
);
return result.available;
}
}
gRPC para Comunicação de Alta Performance
Para comunicação interna entre serviços onde performance é crítica, gRPC oferece vantagens significativas sobre REST:
- Performance: Protocol Buffers são mais compactos que JSON (3-5x menor)
- Tipagem forte: Contratos definidos em
.protofiles - Streaming bidirecional: Suporte nativo a streaming
- Code generation: Clients gerados automaticamente
# Instalar dependências gRPC
pnpm add @grpc/grpc-js @grpc/proto-loader google-protobuf
pnpm add -D grpc-tools grpc_tools_node_protoc_ts
// shared/proto/user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc GetUserBatch (GetUserBatchRequest) returns (GetUserBatchResponse);
rpc StreamUsers (StreamUsersRequest) returns (stream UserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message GetUserBatchRequest {
repeated string user_ids = 1;
}
message GetUserBatchResponse {
repeated UserResponse users = 1;
}
message StreamUsersRequest {
string role = 1;
}
message UserResponse {
string id = 1;
string email = 2;
string name = 3;
string role = 4;
string created_at = 5;
bool is_active = 6;
}
// src/infrastructure/grpc/user.grpc-server.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
import { userService } from '@/application/user.service';
const PROTO_PATH = path.join(__dirname, '../../../../shared/proto/user.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const userProto = grpc.loadPackageDefinition(packageDefinition) as any;
const userServiceImpl = {
async getUser(
call: grpc.ServerUnaryCall,
callback: grpc.sendUnaryData
) {
try {
const { user_id } = call.request;
const user = await userService.getUserById(user_id);
callback(null, {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
created_at: user.createdAt.toISOString(),
is_active: user.status === 'ACTIVE',
});
} catch (error) {
callback({
code: grpc.status.NOT_FOUND,
message: 'Usuário não encontrado',
});
}
},
async streamUsers(call: grpc.ServerWritableStream) {
const { role } = call.request;
try {
const users = await userService.getUsersByRole(role);
for (const user of users) {
call.write({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
});
// Simular processamento com delay para streaming real
await new Promise(r => setTimeout(r, 10));
}
call.end();
} catch (error) {
call.destroy(error as Error);
}
},
};
export function startGrpcServer(port: number = 50051): grpc.Server {
const server = new grpc.Server();
server.addService(userProto.user.UserService.service, userServiceImpl);
server.bindAsync(
`0.0.0.0:${port}`,
grpc.ServerCredentials.createInsecure(), // Em produção: mTLS
(error, actualPort) => {
if (error) {
console.error('Falha ao iniciar gRPC server:', error);
return;
}
console.log(`gRPC Server rodando na porta ${actualPort}`);
server.start();
}
);
return server;
}
5. Comunicação Assíncrona: Mensageria com RabbitMQ e Kafka {#assincrona}
A comunicação assíncrona via eventos é o coração de uma arquitetura de microsserviços verdadeiramente desacoplada.
RabbitMQ: Broker de Mensagens para Comunicação de Serviços
RabbitMQ usa o protocolo AMQP e é excelente para:
- Work queues (filas de trabalho)
- Publish/Subscribe
- Roteamento de mensagens
- Request/Reply assíncrono
pnpm add amqplib
pnpm add -D @types/amqplib
// src/infrastructure/messaging/rabbitmq.connection.ts
import amqp, { Connection, Channel } from 'amqplib';
import { EventEmitter } from 'events';
export class RabbitMQConnection extends EventEmitter {
private connection: Connection | null = null;
private channel: Channel | null = null;
private isConnecting = false;
private reconnectDelay = 1000;
constructor(private url: string) {
super();
}
async connect(): Promise {
if (this.isConnecting) return;
this.isConnecting = true;
try {
this.connection = await amqp.connect(this.url);
this.channel = await this.connection.createChannel();
// Prefetch: processar N mensagens por vez
await this.channel.prefetch(10);
this.isConnecting = false;
this.reconnectDelay = 1000; // Reset delay
console.log('✅ RabbitMQ conectado');
this.emit('connected');
this.connection.on('error', (err) => {
console.error('RabbitMQ connection error:', err);
this.reconnect();
});
this.connection.on('close', () => {
console.warn('RabbitMQ connection closed. Reconectando...');
this.reconnect();
});
} catch (error) {
this.isConnecting = false;
console.error('Falha ao conectar RabbitMQ:', error);
this.reconnect();
}
}
private reconnect(): void {
const delay = Math.min(this.reconnectDelay * 2, 30000);
this.reconnectDelay = delay;
console.log(`Tentando reconectar em ${delay}ms...`);
setTimeout(() => this.connect(), delay);
}
getChannel(): Channel {
if (!this.channel) throw new Error('RabbitMQ não conectado');
return this.channel;
}
async close(): Promise {
await this.channel?.close();
await this.connection?.close();
}
}
// Singleton
let rabbitmqConnection: RabbitMQConnection;
export function getRabbitMQConnection(): RabbitMQConnection {
if (!rabbitmqConnection) {
rabbitmqConnection = new RabbitMQConnection(
process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672'
);
}
return rabbitmqConnection;
}
// src/infrastructure/messaging/event.publisher.ts
import { Channel } from 'amqplib';
import { getRabbitMQConnection } from './rabbitmq.connection';
interface DomainEvent {
type: string;
version: string;
aggregateId: string;
aggregateType: string;
payload: Record;
metadata?: {
correlationId?: string;
causationId?: string;
userId?: string;
timestamp?: string;
};
}
export const EXCHANGES = {
USERS: 'users.events',
ORDERS: 'orders.events',
PAYMENTS: 'payments.events',
NOTIFICATIONS: 'notifications.events',
} as const;
export class EventPublisher {
private channel: Channel;
constructor() {
this.channel = getRabbitMQConnection().getChannel();
}
async setupExchanges(): Promise {
for (const exchange of Object.values(EXCHANGES)) {
await this.channel.assertExchange(exchange, 'topic', {
durable: true,
autoDelete: false,
});
}
console.log('✅ Exchanges configurados');
}
async publish(
exchange: string,
routingKey: string,
event: DomainEvent
): Promise {
const message = {
...event,
metadata: {
...event.metadata,
timestamp: new Date().toISOString(),
serviceOrigin: process.env.SERVICE_NAME || 'unknown',
},
};
const buffer = Buffer.from(JSON.stringify(message));
return this.channel.publish(exchange, routingKey, buffer, {
persistent: true, // Mensagem sobrevive a restart do broker
contentType: 'application/json',
contentEncoding: 'utf-8',
messageId: `${event.aggregateId}-${Date.now()}`,
timestamp: Date.now(),
headers: {
'x-event-type': event.type,
'x-event-version': event.version,
'x-correlation-id': event.metadata?.correlationId,
},
});
}
// Publicação de eventos de usuário
async publishUserCreated(user: {
id: string;
email: string;
name: string;
role: string;
}): Promise {
await this.publish(EXCHANGES.USERS, 'user.created', {
type: 'UserCreated',
version: '1.0',
aggregateId: user.id,
aggregateType: 'User',
payload: user,
});
}
async publishUserDeleted(userId: string): Promise {
await this.publish(EXCHANGES.USERS, 'user.deleted', {
type: 'UserDeleted',
version: '1.0',
aggregateId: userId,
aggregateType: 'User',
payload: { userId, deletedAt: new Date().toISOString() },
});
}
}
// src/infrastructure/messaging/event.consumer.ts
import { Channel, ConsumeMessage } from 'amqplib';
import { getRabbitMQConnection } from './rabbitmq.connection';
type MessageHandler = (message: any) => Promise;
export const QUEUES = {
USER_NOTIFICATIONS: 'notifications.user-events',
USER_ORDER_SYNC: 'orders.user-sync',
ORDER_PAYMENT: 'payments.order-events',
PAYMENT_NOTIFICATIONS: 'notifications.payment-events',
} as const;
export class EventConsumer {
private channel: Channel;
private handlers = new Map();
constructor() {
this.channel = getRabbitMQConnection().getChannel();
}
async setupQueuesAndBindings(): Promise {
// Queue: notifications precisa de user events
await this.channel.assertQueue(QUEUES.USER_NOTIFICATIONS, {
durable: true,
arguments: {
'x-dead-letter-exchange': 'dlx.notifications',
'x-message-ttl': 86400000, // 24 horas
},
});
await this.channel.bindQueue(
QUEUES.USER_NOTIFICATIONS,
'users.events',
'user.*' // user.created, user.deleted, user.updated
);
// Dead Letter Queue para mensagens com falha
await this.channel.assertExchange('dlx.notifications', 'fanout', { durable: true });
await this.channel.assertQueue('dlq.notifications', { durable: true });
await this.channel.bindQueue('dlq.notifications', 'dlx.notifications', '');
console.log('✅ Queues e bindings configurados');
}
on(eventType: string, handler: MessageHandler): void {
this.handlers.set(eventType, handler);
}
async consume(queue: string): Promise {
await this.channel.consume(queue, async (msg: ConsumeMessage | null) => {
if (!msg) return;
try {
const content = JSON.parse(msg.content.toString());
const eventType = content.type || msg.properties.headers?.['x-event-type'];
console.log(`📨 Evento recebido: ${eventType}`);
const handler = this.handlers.get(eventType);
if (handler) {
await handler(content);
this.channel.ack(msg); // Confirmar processamento
} else {
console.warn(`Sem handler para evento: ${eventType}`);
this.channel.nack(msg, false, false); // Mover para DLQ
}
} catch (error) {
console.error('Erro ao processar mensagem:', error);
const retryCount = (msg.properties.headers?.['x-retry-count'] || 0) as number;
if (retryCount < 3) {
// Requeue com delay (usando delayed message plugin)
this.channel.nack(msg, false, true);
} else {
// Após 3 tentativas, mover para DLQ
console.error('Mensagem movida para DLQ após 3 tentativas');
this.channel.nack(msg, false, false);
}
}
});
}
}
Apache Kafka: Streaming de Eventos em Escala
Para cenários de alta throughput e streaming de dados em tempo real, o Kafka é superior ao RabbitMQ:
pnpm add kafkajs
// src/infrastructure/messaging/kafka.client.ts
import { Kafka, Producer, Consumer, EachMessagePayload } from 'kafkajs';
export class KafkaClient {
private kafka: Kafka;
private producer: Producer | null = null;
private consumers: Map = new Map();
constructor() {
this.kafka = new Kafka({
clientId: process.env.SERVICE_NAME || 'microservice',
brokers: (process.env.KAFKA_BROKERS || 'localhost:9092').split(','),
retry: {
initialRetryTime: 300,
retries: 10,
maxRetryTime: 30000,
factor: 2,
},
ssl: process.env.NODE_ENV === 'production',
sasl: process.env.NODE_ENV === 'production' ? {
mechanism: 'scram-sha-256',
username: process.env.KAFKA_USERNAME!,
password: process.env.KAFKA_PASSWORD!,
} : undefined,
});
}
async getProducer(): Promise {
if (!this.producer) {
this.producer = this.kafka.producer({
idempotent: true, // Exatamente uma entrega
maxInFlightRequests: 5,
transactionTimeout: 30000,
});
await this.producer.connect();
console.log('✅ Kafka Producer conectado');
}
return this.producer;
}
async publish(topic: string, messages: Array<{
key?: string;
value: Record;
headers?: Record;
}>): Promise {
const producer = await this.getProducer();
await producer.send({
topic,
messages: messages.map(msg => ({
key: msg.key ? Buffer.from(msg.key) : null,
value: Buffer.from(JSON.stringify(msg.value)),
headers: {
'content-type': 'application/json',
timestamp: Date.now().toString(),
service: process.env.SERVICE_NAME || 'unknown',
...msg.headers,
},
})),
acks: -1, // Aguardar confirmação de todos os brokers
});
}
async subscribe(
groupId: string,
topics: string[],
handler: (payload: EachMessagePayload) => Promise
): Promise {
const consumer = this.kafka.consumer({
groupId,
sessionTimeout: 30000,
heartbeatInterval: 3000,
maxWaitTimeInMs: 5000,
});
await consumer.connect();
this.consumers.set(groupId, consumer);
await consumer.subscribe({ topics, fromBeginning: false });
await consumer.run({
autoCommit: false, // Commit manual para controle fino
eachMessage: async (payload) => {
try {
await handler(payload);
// Commit apenas após processamento bem-sucedido
await consumer.commitOffsets([{
topic: payload.topic,
partition: payload.partition,
offset: (Number(payload.message.offset) + 1).toString(),
}]);
} catch (error) {
console.error(`Erro ao processar mensagem do tópico ${payload.topic}:`, error);
// Implementar dead letter queue ou retry logic aqui
throw error;
}
},
});
console.log(`✅ Kafka Consumer "${groupId}" inscrito em: ${topics.join(', ')}`);
}
async disconnect(): Promise {
await this.producer?.disconnect();
for (const consumer of this.consumers.values()) {
await consumer.disconnect();
}
}
}
export const kafkaClient = new KafkaClient();
6. API Gateway: O Ponto de Entrada Único {#gateway}
O API Gateway é o ponto de entrada único para todos os clientes externos. Ele é responsável por:
- Roteamento de requisições para os microsserviços corretos
- Autenticação e autorização centralizadas
- Rate limiting
- SSL termination
- Aggregation de respostas (combinar dados de múltiplos serviços)
- Caching
- Logging e observabilidade
// services/api-gateway/src/gateway.ts
import express, { Request, Response } from 'express';
import httpProxy from 'http-proxy-middleware';
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
const app = express();
// Mapa de rotas para serviços
const SERVICES: Record = {
'/api/v1/users': process.env.USER_SERVICE_URL || 'http://user-service:3001',
'/api/v1/auth': process.env.USER_SERVICE_URL || 'http://user-service:3001',
'/api/v1/products': process.env.PRODUCT_SERVICE_URL || 'http://product-service:3002',
'/api/v1/orders': process.env.ORDER_SERVICE_URL || 'http://order-service:3003',
'/api/v1/payments': process.env.PAYMENT_SERVICE_URL || 'http://payment-service:3004',
};
// Rate limiting
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}));
// Autenticação centralizada (exceto rotas públicas)
const PUBLIC_ROUTES = ['/api/v1/auth/login', '/api/v1/auth/register', '/health'];
app.use((req, res, next) => {
if (PUBLIC_ROUTES.some(route => req.path.startsWith(route))) {
return next();
}
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Token não fornecido' });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as any;
// Adicionar info do usuário para os serviços downstream
req.headers['X-User-ID'] = payload.userId;
req.headers['X-User-Role'] = payload.role;
req.headers['X-User-Email'] = payload.email;
next();
} catch {
return res.status(401).json({ message: 'Token inválido' });
}
});
// Proxy dinâmico para cada serviço
for (const [prefix, target] of Object.entries(SERVICES)) {
app.use(prefix, httpProxy.createProxyMiddleware({
target,
changeOrigin: true,
on: {
error: (err, req, res: any) => {
console.error(`Proxy error para ${target}:`, err.message);
res.status(503).json({
message: 'Serviço temporariamente indisponível',
service: target,
});
},
},
}));
}
// Aggregation endpoint: combina dados de múltiplos serviços
app.get('/api/v1/dashboard', async (req: Request, res: Response) => {
try {
const userId = req.headers['X-User-ID'] as string;
const [userResponse, ordersResponse, notificationsResponse] = await Promise.allSettled([
fetch(`http://user-service:3001/api/v1/users/${userId}`),
fetch(`http://order-service:3003/api/v1/orders?userId=${userId}&limit=5`),
fetch(`http://notification-service:3005/api/v1/notifications?userId=${userId}&unread=true`),
]);
const user = userResponse.status === 'fulfilled'
? await userResponse.value.json()
: null;
const orders = ordersResponse.status === 'fulfilled'
? await ordersResponse.value.json()
: [];
const notifications = notificationsResponse.status === 'fulfilled'
? await notificationsResponse.value.json()
: [];
res.json({ user, recentOrders: orders, unreadNotifications: notifications });
} catch (error) {
res.status(500).json({ message: 'Erro ao carregar dashboard' });
}
});
app.listen(3000, () => console.log('API Gateway rodando na porta 3000'));
7. Service Discovery e Load Balancing {#discovery}
Em ambientes dinâmicos como Kubernetes, os serviços entram e saem frequentemente. Service Discovery resolve o problema de "como o Serviço A encontra o Serviço B?"
Service Discovery com Consul
// src/infrastructure/service-discovery/consul.client.ts
import Consul from 'consul';
export class ServiceRegistry {
private consul: Consul.Consul;
private serviceId: string;
constructor(
private serviceName: string,
private servicePort: number
) {
this.consul = new Consul({
host: process.env.CONSUL_HOST || 'consul',
port: 8500,
});
this.serviceId = `${serviceName}-${process.env.POD_NAME || process.pid}`;
}
async register(): Promise {
await this.consul.agent.service.register({
id: this.serviceId,
name: this.serviceName,
port: this.servicePort,
tags: ['nodejs', 'v1'],
meta: {
version: process.env.APP_VERSION || '1.0.0',
},
check: {
http: `http://localhost:${this.servicePort}/health`,
interval: '10s',
timeout: '3s',
deregisterCriticalServiceAfter: '30s',
},
});
console.log(`✅ Serviço registrado no Consul: ${this.serviceId}`);
}
async deregister(): Promise {
await this.consul.agent.service.deregister(this.serviceId);
console.log(`🔌 Serviço desregistrado do Consul: ${this.serviceId}`);
}
async discover(serviceName: string): Promise {
const services = await this.consul.health.service({
service: serviceName,
passing: true, // Apenas instâncias saudáveis
});
return (services as any[]).map(entry =>
`http://${entry.Service.Address}:${entry.Service.Port}`
);
}
async discoverAndBalance(serviceName: string): Promise {
const instances = await this.discover(serviceName);
if (instances.length === 0) {
throw new Error(`Nenhuma instância disponível para: ${serviceName}`);
}
// Round-robin simples
const index = Math.floor(Math.random() * instances.length);
return instances[index];
}
}
8. Event Sourcing e CQRS na Prática {#event-sourcing}
Event Sourcing é um padrão arquitetural onde o estado da aplicação é derivado de uma sequência de eventos imutáveis, em vez de armazenar apenas o estado atual.
// src/domain/event-store.ts
import { prisma } from '@/infrastructure/database';
interface StoredEvent {
id: string;
aggregateId: string;
aggregateType: string;
eventType: string;
eventVersion: string;
payload: Record;
metadata: Record;
sequence: number;
createdAt: Date;
}
export class EventStore {
async append(
aggregateId: string,
aggregateType: string,
events: Array<{
type: string;
version: string;
payload: Record;
metadata?: Record;
}>,
expectedVersion: number
): Promise {
return prisma.$transaction(async (tx) => {
// Verificar versão (optimistic locking)
const currentEvents = await tx.event.count({
where: { aggregateId },
});
if (currentEvents !== expectedVersion) {
throw new Error(
`Conflict: expected version ${expectedVersion}, got ${currentEvents}`
);
}
const storedEvents = await tx.event.createMany({
data: events.map((event, index) => ({
aggregateId,
aggregateType,
eventType: event.type,
eventVersion: event.version,
payload: event.payload,
metadata: event.metadata || {},
sequence: expectedVersion + index + 1,
})),
});
return tx.event.findMany({
where: { aggregateId },
orderBy: { sequence: 'asc' },
skip: expectedVersion,
});
});
}
async getEvents(
aggregateId: string,
fromSequence: number = 0
): Promise {
return prisma.event.findMany({
where: {
aggregateId,
sequence: { gt: fromSequence },
},
orderBy: { sequence: 'asc' },
});
}
async getEventsByType(
eventType: string,
fromDate?: Date
): Promise {
return prisma.event.findMany({
where: {
eventType,
...(fromDate && { createdAt: { gte: fromDate } }),
},
orderBy: { createdAt: 'asc' },
});
}
}
CQRS: Separando Leitura de Escrita
// Command Side (Write)
// src/application/commands/create-order.command.ts
export interface CreateOrderCommand {
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
shippingAddress: Address;
}
// src/application/commands/create-order.handler.ts
export class CreateOrderCommandHandler {
constructor(
private orderRepository: OrderRepository,
private productClient: ProductClient,
private eventPublisher: EventPublisher
) {}
async handle(command: CreateOrderCommand): Promise {
// 1. Validar disponibilidade dos produtos
for (const item of command.items) {
const available = await this.productClient.checkInventory(
item.productId,
item.quantity
);
if (!available) {
throw new Error(`Produto ${item.productId} sem estoque suficiente`);
}
}
// 2. Calcular total
const total = command.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
// 3. Criar pedido
const order = await this.orderRepository.create({
userId: command.userId,
items: command.items,
shippingAddress: command.shippingAddress,
total,
status: 'PENDING',
});
// 4. Publicar evento
await this.eventPublisher.publish('orders.events', 'order.created', {
type: 'OrderCreated',
version: '1.0',
aggregateId: order.id,
aggregateType: 'Order',
payload: {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total,
},
});
return order.id;
}
}
// Query Side (Read) — usando view otimizada para leitura
// src/application/queries/get-order-details.query.ts
export class GetOrderDetailsQuery {
constructor(private readDb: ReadDatabase) {}
async execute(orderId: string) {
// View materializada otimizada para leitura
return this.readDb.query(`
SELECT
o.id,
o.order_number,
o.status,
o.total,
o.created_at,
u.name as user_name,
u.email as user_email,
json_agg(
json_build_object(
'product_id', oi.product_id,
'product_name', p.name,
'quantity', oi.quantity,
'price', oi.price
)
) as items
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.id = $1
GROUP BY o.id, u.name, u.email
`, [orderId]);
}
}
9. Saga Pattern: Transações Distribuídas {#saga}
Sem transações ACID entre serviços, o Saga Pattern é a solução para garantir consistência eventual em operações que envolvem múltiplos serviços.
Choreography Saga — Eventos Reativos
// Fluxo de um pedido via Choreography Saga:
// 1. order-service: cria pedido (PENDING)
// 2. inventory-service: ouve OrderCreated → reserva estoque → publica StockReserved
// 3. payment-service: ouve StockReserved → processa pagamento → publica PaymentProcessed
// 4. order-service: ouve PaymentProcessed → atualiza pedido para CONFIRMED
// Em caso de falha em qualquer etapa: evento de compensação é publicado
// src/infrastructure/sagas/order.saga.ts
export class OrderSaga {
constructor(
private consumer: EventConsumer,
private publisher: EventPublisher,
private orderRepository: OrderRepository
) {}
async start(): Promise {
// Ouvir StockReserved
this.consumer.on('StockReserved', async (event) => {
await this.handleStockReserved(event.payload);
});
// Ouvir StockReservationFailed (compensação)
this.consumer.on('StockReservationFailed', async (event) => {
await this.handleStockReservationFailed(event.payload);
});
// Ouvir PaymentProcessed
this.consumer.on('PaymentProcessed', async (event) => {
await this.handlePaymentProcessed(event.payload);
});
// Ouvir PaymentFailed (compensação)
this.consumer.on('PaymentFailed', async (event) => {
await this.handlePaymentFailed(event.payload);
});
}
private async handleStockReserved(payload: any): Promise {
const { orderId, reservationId } = payload;
await this.orderRepository.update(orderId, {
status: 'STOCK_RESERVED',
reservationId,
});
// Saga continua: publicar evento para payment service
await this.publisher.publish('orders.events', 'order.stock-reserved', {
type: 'OrderStockReserved',
version: '1.0',
aggregateId: orderId,
aggregateType: 'Order',
payload: { orderId, reservationId },
});
}
private async handleStockReservationFailed(payload: any): Promise {
const { orderId, reason } = payload;
// Compensação: cancelar pedido
await this.orderRepository.update(orderId, {
status: 'CANCELLED',
cancellationReason: `Estoque insuficiente: ${reason}`,
});
await this.publisher.publish('orders.events', 'order.cancelled', {
type: 'OrderCancelled',
version: '1.0',
aggregateId: orderId,
aggregateType: 'Order',
payload: { orderId, reason: 'STOCK_UNAVAILABLE' },
});
}
private async handlePaymentProcessed(payload: any): Promise {
const { orderId, paymentId, transactionId } = payload;
await this.orderRepository.update(orderId, {
status: 'CONFIRMED',
paymentId,
transactionId,
confirmedAt: new Date(),
});
await this.publisher.publish('orders.events', 'order.confirmed', {
type: 'OrderConfirmed',
version: '1.0',
aggregateId: orderId,
aggregateType: 'Order',
payload: { orderId, paymentId },
});
}
private async handlePaymentFailed(payload: any): Promise {
const { orderId, reason, reservationId } = payload;
// Compensação: cancelar pedido e liberar estoque reservado
await this.orderRepository.update(orderId, {
status: 'PAYMENT_FAILED',
cancellationReason: reason,
});
// Publicar evento para inventory service liberar o estoque
await this.publisher.publish('inventory.events', 'stock.release', {
type: 'StockReleaseRequested',
version: '1.0',
aggregateId: reservationId,
aggregateType: 'Reservation',
payload: { reservationId, orderId, reason: 'PAYMENT_FAILED' },
});
}
}
10. Observabilidade: Logging, Tracing e Métricas {#observabilidade}
OpenTelemetry — Rastreamento Distribuído
pnpm add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
pnpm add @opentelemetry/exporter-jaeger @opentelemetry/exporter-prometheus
// src/telemetry.ts (deve ser importado ANTES de tudo)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const jaegerExporter = new JaegerExporter({
endpoint: process.env.JAEGER_ENDPOINT || 'http://jaeger:14268/api/traces',
});
const prometheusExporter = new PrometheusExporter({
port: 9464,
endpoint: '/metrics',
});
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'unknown',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION || '1.0.0',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
}),
traceExporter: jaegerExporter,
metricReader: prometheusExporter,
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-http': {
requestHook: (span, request) => {
span.setAttribute('http.request.body.size',
request.headers?.['content-length'] || 0);
},
},
}),
],
});
sdk.start();
console.log('✅ OpenTelemetry iniciado');
process.on('SIGTERM', () => {
sdk.shutdown().then(() => console.log('OpenTelemetry finalizado'));
});
11. Containerização com Docker {#docker}
Docker Compose para Desenvolvimento Local
# docker-compose.dev.yml
version: '3.9'
services:
user-service:
build:
context: ./services/user-service
dockerfile: Dockerfile.dev
volumes:
- ./services/user-service/src:/app/src
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@user-db:5432/userdb
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
- SERVICE_NAME=user-service
ports:
- '3001:3001'
- '50051:50051' # gRPC
depends_on:
- user-db
- rabbitmq
networks:
- microservices-network
product-service:
build:
context: ./services/product-service
dockerfile: Dockerfile.dev
volumes:
- ./services/product-service/src:/app/src
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@product-db:5432/productdb
- ELASTICSEARCH_URL=http://elasticsearch:9200
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
ports:
- '3002:3002'
depends_on:
- product-db
- elasticsearch
- rabbitmq
networks:
- microservices-network
order-service:
build:
context: ./services/order-service
dockerfile: Dockerfile.dev
volumes:
- ./services/order-service/src:/app/src
environment:
- DATABASE_URL=postgresql://postgres:postgres@order-db:5432/orderdb
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
- USER_SERVICE_URL=http://user-service:3001
- PRODUCT_SERVICE_URL=http://product-service:3002
ports:
- '3003:3003'
networks:
- microservices-network
api-gateway:
build:
context: ./services/api-gateway
ports:
- '3000:3000'
environment:
- USER_SERVICE_URL=http://user-service:3001
- PRODUCT_SERVICE_URL=http://product-service:3002
- ORDER_SERVICE_URL=http://order-service:3003
- JWT_SECRET=${JWT_SECRET}
depends_on:
- user-service
- product-service
- order-service
networks:
- microservices-network
# Infraestrutura
user-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: userdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- user-db-data:/var/lib/postgresql/data
networks:
- microservices-network
product-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: productdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- product-db-data:/var/lib/postgresql/data
networks:
- microservices-network
order-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: orderdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- order-db-data:/var/lib/postgresql/data
networks:
- microservices-network
rabbitmq:
image: rabbitmq:3.13-management-alpine
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: admin
ports:
- '5672:5672'
- '15672:15672' # Management UI
volumes:
- rabbitmq-data:/var/lib/rabbitmq
networks:
- microservices-network
elasticsearch:
image: elasticsearch:8.12.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
ports:
- '9200:9200'
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
networks:
- microservices-network
redis:
image: redis:7-alpine
ports:
- '6379:6379'
networks:
- microservices-network
# Observabilidade
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- '16686:16686' # UI
- '14268:14268' # HTTP collector
networks:
- microservices-network
prometheus:
image: prom/prometheus:latest
volumes:
- ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- '9090:9090'
networks:
- microservices-network
grafana:
image: grafana/grafana:latest
ports:
- '3030:3000'
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
networks:
- microservices-network
volumes:
user-db-data:
product-db-data:
order-db-data:
rabbitmq-data:
elasticsearch-data:
grafana-data:
networks:
microservices-network:
driver: bridge
12. Orquestração com Kubernetes {#kubernetes}
# infrastructure/kubernetes/deployments/user-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: ecommerce
labels:
app: user-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: user-service
version: v1
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '9464'
prometheus.io/path: '/metrics'
spec:
serviceAccountName: user-service-sa
containers:
- name: user-service
image: myregistry/user-service:1.0.0
ports:
- containerPort: 3001
name: http
- containerPort: 50051
name: grpc
- containerPort: 9464
name: metrics
env:
- name: NODE_ENV
value: production
- name: SERVICE_NAME
value: user-service
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: user-service-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: shared-secrets
key: jwt-secret
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3001
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ['/bin/sh', '-c', 'sleep 15']
terminationGracePeriodSeconds: 30
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: ecommerce
spec:
selector:
app: user-service
ports:
- name: http
port: 3001
targetPort: 3001
- name: grpc
port: 50051
targetPort: 50051
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
namespace: ecommerce
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
13. Antipadrões e Como Evitá-los {#antipadroes}
1. Distributed Monolith (O Pior dos Mundos)
O antipadrão mais comum: criar microsserviços que são fortemente acoplados entre si.
❌ Antipadrão - Chamadas síncronas em cadeia:
Client → Gateway → Order Service → User Service → Product Service → Inventory Service
Consequências:
- Latência acumulada (N serviços × tempo médio)
- Falha em cascata: se Inventory Service cai, tudo cai
- Impossível escalar independentemente
2. Microsserviços Muito Granulares
Não crie um microsserviço para cada método ou função:
❌ Muito granular:
- calculate-tax-service
- apply-discount-service
- format-price-service
✅ Granularidade adequada:
- pricing-service (calcula preços, aplica descontos, impostos)
3. Ignorar a Consistência Eventual
// ❌ Assumir que dados de outros serviços estão sempre atualizados:
const user = await userService.getUser(userId);
const order = await orderService.createOrder({ userId, ...data });
// user pode estar desatualizado, nome pode ter mudado
// ✅ Tratar consistência eventual:
// Armazenar snapshot dos dados necessários no momento da criação
const order = await orderService.createOrder({
userId,
userSnapshot: { // Snapshot no momento do pedido
name: user.name,
email: user.email,
},
...data,
});
4. Falta de Idempotência
// ✅ Endpoints de criação devem ser idempotentes com chave de idempotência:
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['x-idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ message: 'X-Idempotency-Key header é obrigatório' });
}
// Verificar se já processamos esta requisição
const existing = await redis.get(`idempotency:${idempotencyKey}`);
if (existing) {
return res.status(200).json(JSON.parse(existing));
}
// Processar e armazenar resultado
const result = await orderService.create(req.body);
await redis.setex(`idempotency:${idempotencyKey}`, 86400, JSON.stringify(result));
return res.status(201).json(result);
});
14. Conclusão: Quando Usar Microsserviços? {#conclusao}
Microsserviços são uma solução poderosa para problemas específicos. Antes de adotá-los, faça estas perguntas:
Use microsserviços quando:
- Sua equipe tem mais de 15-20 desenvolvedores e há contention no monolítico
- Partes da aplicação têm requisitos de escalabilidade muito diferentes
- Precisa de heterogeneidade tecnológica (ML em Python, API em Node.js)
- Disponibilidade de 99.99%+ é um requisito crítico
- Times autônomos precisam fazer deploys independentes
Fique com monolítico quando:
- Equipe pequena (< 10 desenvolvedores)
- Produto ainda em validação de mercado
- Pouca maturidade operacional (DevOps, SRE)
- Domínio do problema ainda não está bem compreendido
- Sem necessidade imediata de escala diferenciada
O caminho recomendado:
- Comece com um Monolítico Bem Estruturado (modular)
- Identifique gargalos reais (não hipotéticos) via observabilidade
- Extraia o primeiro microsserviço do módulo com mais pressão
- Itere, aprendendo com cada extração
A arquitetura de microsserviços é sobre trade-offs conscientes, não sobre estar "na moda". O objetivo final é sempre entregar valor ao usuário de forma confiável e eficiente.
Publicado em 2025 | Categoria: Arquitetura de Software | Tags: Microsserviços, Node.js, RabbitMQ, Kafka, Kubernetes, Docker, Event Sourcing, CQRS