O desenvolvimento de aplicações escaláveis e resilientes em ambientes distribuídos se tornou um desafio cada vez maior no mundo da tecnologia. Com a crescente adoção de microsserviços, garantir a consistência dos dados e a orquestração adequada de transações tornou-se prioridade para evitar cenários de falha e garantir a boa experiência do usuário.
Neste artigo, você vai aprender como o padrão de design Saga pode ajudar a gerenciar transações distribuídas de forma mais eficiente. Além disso, vamos explorar como integrar esse padrão com Apache Kafka e o NestJS, dois pilares fundamentais para a construção de arquiteturas escaláveis e orientadas a eventos.
Este guia foi escrito em uma linguagem simples e didática, pensando em quem está começando agora na programação ou nunca trabalhou com microsserviços antes. Vamos lá?
O que é o Saga Design Pattern?
O Saga Design Pattern é um padrão arquitetural que lida com transações distribuídas em ambientes de microsserviços. Em outras palavras, ele ajuda a garantir que, ao realizar uma operação que envolve vários serviços, todos os passos sejam executados de forma confiável e que seja possível reverter as alterações em caso de falha.
Imagine que você precisa implementar um sistema de comércio eletrônico. Quando um usuário faz uma compra, podem existir diversos microsserviços envolvidos:
- Serviço de Pagamento
- Serviço de Estoque
- Serviço de Entrega
- Serviço de Notificações
Se algo falhar em um desses serviços depois que o pagamento já foi processado, o sistema deve reverter ou compensar as ações anteriores (por exemplo, estornar o pagamento ou liberar o estoque). O Saga Design Pattern organiza essas etapas e garante que as compensações sejam executadas corretamente em caso de erro.
Existem duas abordagens principais para Sagas:
- Orquestrada: Existe um coordenador (orquestrador) que gerencia todas as etapas, enviando comandos para cada microsserviço.
- Coreografada: Cada microsserviço reage a eventos disparados pelos outros, sem depender de um orquestrador central.
Por que usar Sagas em microsserviços?
Em um ambiente de microsserviços, cada serviço é responsável por uma parte específica do sistema e, geralmente, possui banco de dados próprio. Isso traz benefícios de escalabilidade, mas também cria desafios para a consistência de dados.
Vantagens
- Alta disponibilidade: O sistema torna-se mais tolerante a falhas, pois cada serviço pode ser executado de forma independente.
- Escalabilidade: Cada microsserviço pode ser escalado individualmente, de acordo com a demanda.
- Manutenção facilitada: É possível atualizar ou substituir serviços sem afetar todo o sistema.
Desafios
- Gestão de transações: Se uma operação abrange múltiplos serviços, como garantir que todos os serviços finalizem com sucesso?
- Compensação de falhas: Quando algo dá errado, como desfazer alterações em serviços que já executaram suas tarefas com sucesso?
O Saga Design Pattern entra em cena para suprir exatamente esse desafio de coordenação e compensação de transações distribuídas.
Visão geral do Apache Kafka
Apache Kafka é uma plataforma de streaming de eventos distribuída, projetada para lidar com grandes volumes de dados em tempo real. Ele trabalha com o conceito de tópicos, onde produtores publicam mensagens e consumidores leem essas mensagens.
Características principais do Apache Kafka:
- Alto desempenho: Capaz de processar milhões de mensagens por segundo.
- Escalabilidade: Fácil de adicionar ou remover nós do cluster.
- Persistência de dados: Armazena mensagens em disco, podendo ser configurado para mantê-las por um determinado período.
- Publicar/Assinar (Publish/Subscribe): Produtores publicam mensagens em tópicos e os consumidores as leem em tempo real.
No contexto de Sagas, o Kafka atua como intermediário na comunicação entre serviços, fornecendo um canal confiável para troca de mensagens e disparos de eventos.
O que é NestJS e como ele se encaixa nessa arquitetura?
NestJS é um framework para Node.js que segue a arquitetura Modular, inspirada no Angular, e utiliza fortemente conceitos de Injeção de Dependências (DI). Ele facilita a construção de aplicativos escaláveis e testáveis em Node.js, trazendo boas práticas como SOLID e separação de responsabilidades.
Neste cenário de Sagas e microsserviços:
- O NestJS oferece suporte nativo para construir aplicações orientadas a eventos.
- Facilita a integração com o Kafka através de pacotes como
@nestjs/microservices
ou outros conectores de terceiros. - Fornece uma estrutura clara para organizar o código, tornando a implementação de um Saga mais simples e coerente.
Integração de Saga e Apache Kafka com NestJS: arquitetura básica
A arquitetura básica de uma aplicação que implementa o padrão Saga utilizando Kafka e NestJS pode ser visualizada da seguinte forma:
- Orquestrador (opcional): Caso adote a abordagem orquestrada, terá um serviço central que define as etapas e envia comandos para cada microsserviço.
- Serviços individuais: Cada microsserviço tem um produtor e um consumidor Kafka. Quando um serviço conclui sua tarefa, publica um evento (mensagem) que indica sucesso ou falha.
- Tópicos Kafka: Cada ação ou evento do Saga é mapeado em um tópico Kafka. Os serviços ou o orquestrador ficam “escutando” esses tópicos para tomar decisões.
Funcionamento em alto nível
- Início da Saga: Uma ação inicial (ex.: criar pedido) é disparada, gerando um evento no Kafka.
- Execução das etapas: Cada microsserviço envolvido recebe a mensagem, executa sua parte e responde com um novo evento (sucesso ou falha).
- Compensação em caso de erro: Se um serviço falhar, dispara-se uma mensagem de compensação que orienta os demais serviços a reverter as ações já executadas (estornar pagamento, liberar estoque, etc.).
Exemplo prático de implementação no NestJS
Estrutura do projeto
Para facilitar a compreensão, vamos supor que temos três microsserviços na nossa aplicação:
- PedidosService (responsável por criar, atualizar ou cancelar pedidos)
- PagamentoService (responsável pela cobrança do cliente)
- EstoqueService (responsável pela reserva de itens em estoque)
A estrutura de pastas para cada microsserviço pode ser algo como:
.
├── pedidos-service
│ ├── src
│ │ ├── app.module.ts
│ │ ├── pedidos.controller.ts
│ │ ├── pedidos.service.ts
│ │ ├── ...
│ ├── package.json
│ └── ...
├── pagamento-service
│ ├── src
│ │ ├── app.module.ts
│ │ ├── pagamento.controller.ts
│ │ ├── pagamento.service.ts
│ │ ├── ...
│ ├── package.json
│ └── ...
├── estoque-service
│ ├── src
│ │ ├── app.module.ts
│ │ ├── estoque.controller.ts
│ │ ├── estoque.service.ts
│ │ ├── ...
│ ├── package.json
│ └── ...
└── docker-compose.yml
Cada serviço pode ser executado de forma independente, mas todos se comunicam via Kafka.
Instalação e configuração do Kafka
Para executar o Apache Kafka localmente, é comum usar o Docker e um arquivo docker-compose.yml
que contenha tanto o Kafka quanto o ZooKeeper:
version: '3.7'
services:
zookeeper:
image: bitnami/zookeeper:latest
environment:
- ZOO_ENABLE_AUTH=no
ports:
- '2181:2181'
kafka:
image: bitnami/kafka:latest
ports:
- '9092:9092'
environment:
- KAFKA_BROKER_ID=1
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
- ALLOW_PLAINTEXT_LISTENER=yes
depends_on:
- zookeeper
Depois de salvar esse arquivo, basta executar docker-compose up -d
para subir os contêineres do Kafka e do ZooKeeper.
Produtores e Consumidores no NestJS
O NestJS oferece um Microservice Module que permite criar aplicativos que se comunicam através de diferentes protocolos, incluindo Kafka. Vamos exemplificar como configurar um serviço para se comunicar via Kafka.
No app.module.ts
de cada serviço, podemos configurar um KafkaClient da seguinte forma:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { PedidosController } from './pedidos.controller';
import { PedidosService } from './pedidos.service';
@Module({
imports: [
ClientsModule.register([
{
name: 'PEDIDOS_KAFKA',
transport: Transport.KAFKA,
options: {
client: {
brokers: ['localhost:9092'],
},
consumer: {
groupId: 'pedidos-consumer',
},
},
},
]),
],
controllers: [PedidosController],
providers: [PedidosService],
})
export class AppModule {}
name
: Nome para identificar este cliente (pode ser usado para injetar em outros lugares do código).client.brokers
: Define o endereço do broker Kafka.consumer.groupId
: Define o ID de grupo para o consumidor Kafka.
No controller, podemos escutar eventos (mensagens Kafka) utilizando decoradores do NestJS:
import { Controller } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';
@Controller()
export class PedidosController {
@EventPattern('pagamento-confirmado')
async handlePagamentoConfirmado(@Payload() message) {
console.log('Pagamento confirmado:', message.value);
// Lógica de tratamento...
}
}
@EventPattern('pagamento-confirmado')
: Indica que este método responde a mensagens publicadas no tópicopagamento-confirmado
.@Payload()
: Extrai o conteúdo da mensagem.
Para publicar mensagens em um tópico Kafka, injetamos o cliente no nosso service e chamamos o método emit
ou send
:
import { Injectable, Inject } from '@nestjs/common';
import { ClientKafka } from '@nestjs/microservices';
@Injectable()
export class PedidosService {
constructor(
@Inject('PEDIDOS_KAFKA') private readonly kafkaClient: ClientKafka,
) {}
async criarPedido(dadosPedido: any) {
// Lógica para criar o pedido
console.log('Pedido criado:', dadosPedido);
// Emite evento para o tópico
this.kafkaClient.emit('pedido-criado', {
pedidoId: '1234',
usuarioId: dadosPedido.usuarioId,
// outros dados...
});
}
}
Implementando um Saga simples
Vamos supor que a criação de um pedido envolve os seguintes passos:
- Criar pedido (PedidosService)
- Reservar itens em estoque (EstoqueService)
- Processar pagamento (PagamentoService)
Caso alguma etapa falhe, precisamos reverter as anteriores. Aqui está um fluxo simplificado:
- O PedidosService recebe um comando
criarPedido
. - Ele publica o evento
pedido-criado
no tópicopedido-criado
. - O EstoqueService escuta
pedido-criado
e tenta reservar o estoque. Se bem-sucedido, publicaestoque-reservado
. Caso falhe, publicaestoque-indisponivel
, que dispara a compensação. - O PagamentoService escuta
estoque-reservado
e tenta processar o pagamento. Se bem-sucedido, publicapagamento-confirmado
. Se falhar, publicapagamento-nao-confirmado
, que dispara a compensação. - Se todas as etapas forem concluídas com sucesso, o PedidosService pode atualizar o status do pedido para “Concluído”.
Para ilustrar a compensação, quando o EstoqueService não consegue reservar itens, dispara-se estoque-indisponivel
. O PedidosService, ao receber essa mensagem, pode disparar um evento de cancelar-pedido
e reverter qualquer status interno.
Código exemplo de compensação simplificado
No PedidosService, adicione um consumidor para lidar com estoque-indisponivel
:
@Controller()
export class PedidosController {
@EventPattern('estoque-indisponivel')
async handleEstoqueIndisponivel(@Payload() message) {
const { pedidoId } = message.value;
// Cancelar pedido
console.log(`Estoque indisponível para o pedido ${pedidoId}.`);
// Lógica para reverter: atualizar status para "Cancelado" no banco de dados, etc.
// Emite evento para notificar que o pedido foi cancelado
// (Opcional, mas pode ser útil para notificar outros serviços)
}
}
No EstoqueService:
@Controller()
export class EstoqueController {
@EventPattern('pedido-criado')
async reservarEstoque(@Payload() message) {
const { pedidoId } = message.value;
try {
// Tenta reservar estoque
const sucesso = await this.verificarEReservarEstoque(message.value);
if (!sucesso) {
// Emite evento de falha
this.kafkaClient.emit('estoque-indisponivel', { pedidoId });
return;
}
// Emite evento de sucesso
this.kafkaClient.emit('estoque-reservado', { pedidoId });
} catch (error) {
// Emite evento de falha
this.kafkaClient.emit('estoque-indisponivel', { pedidoId });
}
}
private async verificarEReservarEstoque(dados: any): Promise<boolean> {
// Lógica fake para exemplo
// Retorna true se houver estoque, false se não
return true;
}
}
No PagamentoService:
@Controller()
export class PagamentoController {
@EventPattern('estoque-reservado')
async processarPagamento(@Payload() message) {
const { pedidoId } = message.value;
try {
// Processo de pagamento
const pagamentoOk = await this.realizarPagamento(message.value);
if (!pagamentoOk) {
this.kafkaClient.emit('pagamento-nao-confirmado', { pedidoId });
return;
}
this.kafkaClient.emit('pagamento-confirmado', { pedidoId });
} catch (error) {
this.kafkaClient.emit('pagamento-nao-confirmado', { pedidoId });
}
}
private async realizarPagamento(dados: any): Promise<boolean> {
// Lógica fake de pagamento
// Retorna true se sucesso
return true;
}
}
Com esse fluxo, cada microsserviço fica responsável por sua parte do processamento e emite eventos de sucesso ou falha. Dessa forma, conseguimos implementar um Saga básico em arquitetura de microsserviços com Kafka.
Boas práticas e dicas para produção
- Crie tópicos específicos para cada evento: Isso facilita a organização e a manutenção do código.
- Defina um esquema para as mensagens: Utilizar ferramentas como Avro ou JSON Schema pode garantir consistência dos dados enviados e recebidos.
- Mantenha logs centralizados: Em caso de falhas, é fundamental conseguir rastrear qual serviço falhou e em que etapa.
- Evite lógica de negócio complexa no orquestrador: Se optar pela abordagem orquestrada, mantenha o orquestrador simples, delegando o trabalho duro para os microsserviços.
- Gerenciamento de erros e reintentos: Sempre implemente estratégias de reintento (retry) e fallback para evitar perda de mensagens ou falhas em cascata.
- Monitoração do Kafka: Ferramentas como o Confluent Control Center podem auxiliar a monitorar o Kafka, ver quantas mensagens estão em cada tópico e identificar problemas rapidamente.
Projetos exemplo
No meu Github disponibilizei dois projetos que demonstram basicamente as duas abordagens, Orquestrada e Coreografada. Confira:
https://github.com/clesiosmatos/saga-pattern-orchestration
https://github.com/clesiosmatos/saga-pattern-choreography
Links Úteis
- Documentação oficial do Apache Kafka
- Documentação do NestJS
- Exemplo de microsserviços com NestJS
- Artigo sobre Transações Distribuídas em Microserviços (em inglês)
Conclusão
O Saga Design Pattern é essencial para gerenciar transações distribuídas em um ambiente de microsserviços. Ao utilizar o NestJS e o Apache Kafka, você obtém um ecossistema robusto e escalável para lidar com eventos e mensagens. Neste artigo, vimos como cada microsserviço pode ser responsável por sua parte do processamento, emitindo eventos de sucesso ou falha. Assim, a arquitetura se torna mais resiliente, permitindo compensar e reverter ações quando algo não sai como planejado.
Se você está começando agora, é importante entender que o caminho para dominar essa arquitetura exige prática e estudo contínuo. A boa notícia é que o NestJS possui uma curva de aprendizado relativamente tranquila para quem já tem familiaridade com Node.js, e o Kafka segue sendo uma das soluções mais populares para streaming de dados em tempo real.
O que você achou? Deixe um comentário!
Gostou do artigo? Ficou com dúvidas sobre algum ponto específico da implementação do padrão Saga com Kafka e NestJS? Deixe seu comentário abaixo e compartilhe sua experiência! Sua participação é fundamental para construirmos uma comunidade de desenvolvimento ainda mais forte e colaborativa.