⚡ Últimas

Microsserviços com Node.js: Guia Definitivo 2025 — Arquitetura, Comunicação, Docker, Kubernetes e Padrões Avançados

Sumário

  1. Por que Microsserviços? Monolítico vs Microsserviços
  2. Princípios de Design de Microsserviços
  3. Estrutura de um Ecossistema de Microsserviços
  4. Comunicação Síncrona: REST e gRPC
  5. Comunicação Assíncrona: Mensageria com RabbitMQ e Kafka
  6. API Gateway: O Ponto de Entrada Único
  7. Service Discovery e Load Balancing
  8. Event Sourcing e CQRS na Prática
  9. Saga Pattern: Transações Distribuídas
  10. Observabilidade: Logging, Tracing e Métricas
  11. Segurança em Arquiteturas Distribuídas
  12. Containerização com Docker
  13. Orquestração com Kubernetes
  14. CI/CD para Microsserviços
  15. Antipadrões e Como Evitá-los
  16. 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:

  1. 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.
  1. Escalabilidade seletiva: Se apenas o módulo de processamento de imagens precisa de mais recursos, você é obrigado a escalar o monolítico inteiro.
  1. 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.
  1. Disponibilidade: Um bug em um módulo derruba toda a aplicação.
  1. 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 .proto files
  • 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:

  1. Comece com um Monolítico Bem Estruturado (modular)
  2. Identifique gargalos reais (não hipotéticos) via observabilidade
  3. Extraia o primeiro microsserviço do módulo com mais pressão
  4. 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

🌐 Idioma
🇧🇷 Português
🇺🇸 English
🇪🇸 Español
🇫🇷 Français
🇩🇪 Deutsch
💳Faça um Orçamento