Um dos maiores desafios ao migrar de monolito para microsserviços é a comunicação entre serviços. Chamadas HTTP síncronas parecem simples no começo, mas criam acoplamento temporal: se o serviço de destino estiver fora do ar, a operação falha — e você precisa lidar com retry, timeout e circuit breaker em cada chamada.
Mensageria assíncrona com RabbitMQ resolve isso de forma elegante. O serviço publica uma mensagem e continua executando — não importa se o consumidor está disponível naquele momento. O RabbitMQ garante a entrega quando o consumidor voltar.
Neste guia você vai implementar um cenário real: Serviço de Pedidos publica um evento quando um pedido é confirmado, e o Serviço de Notificações consome esse evento para enviar e-mail.
Stack utilizada
RabbitMQ — message broker open source, battle-tested, com dashboard web incluído.
MassTransit — biblioteca .NET que abstrai o broker (funciona com RabbitMQ, Azure Service Bus, Amazon SQS). Cuida de serialização, retry, dead-letter e registro de consumers automaticamente.
Setup com Docker Compose
# docker-compose.yml
services:
rabbitmq:
image: rabbitmq:3.13-management-alpine
ports:
- "5672:5672" # AMQP
- "15672:15672" # Dashboard web (login: guest/guest)
environment:
RABBITMQ_DEFAULT_VHOST: neryx
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 10s
timeout: 5s
retries: 5
pedidos-api:
build: ./src/PedidosApi
depends_on:
rabbitmq:
condition: service_healthy
environment:
RabbitMQ__Host: rabbitmq
RabbitMQ__VirtualHost: neryx
notificacoes-worker:
build: ./src/NotificacoesWorker
depends_on:
rabbitmq:
condition: service_healthy
environment:
RabbitMQ__Host: rabbitmq
RabbitMQ__VirtualHost: neryx
Pacotes necessários
# Nos dois projetos (publisher e consumer) dotnet add package MassTransit dotnet add package MassTransit.RabbitMQApenas no consumer (worker)
dotnet add package MassTransit.Extensions.DependencyInjection
Definindo os contratos de mensagem
Os contratos (interfaces ou records das mensagens) devem ficar em um pacote compartilhado entre os serviços. Isso garante que publisher e consumer falem a mesma "língua".
// Projeto: Neryx.Contracts (referenciado pelos dois serviços) namespace Neryx.Contracts.Pedidos;public record PedidoConfirmadoEvent { public Guid PedidoId { get; init; } public Guid ClienteId { get; init; } public string ClienteEmail { get; init; } = string.Empty; public string ClienteNome { get; init; } = string.Empty; public decimal Total { get; init; } public DateTime ConfirmadoEm { get; init; } public IReadOnlyList<ItemConfirmadoDto> Itens { get; init; } = []; }
public record ItemConfirmadoDto(string Produto, int Quantidade, decimal PrecoUnitario);
Publisher: Serviço de Pedidos
// Program.cs do PedidosApi builder.Services.AddMassTransit(x => { x.UsingRabbitMq((ctx, cfg) => { cfg.Host(builder.Configuration["RabbitMQ:Host"] ?? "localhost", "/neryx", h => { h.Username("guest"); h.Password("guest"); });cfg.ConfigureEndpoints(ctx); });});
// Handler do command ConfirmarPedido (usando CQRS + MediatR do artigo anterior) public class ConfirmarPedidoHandler : IRequestHandler<ConfirmarPedidoCommand, ConfirmarPedidoResult> { private readonly IPedidoRepository _repository; private readonly IUnitOfWork _unitOfWork; private readonly IPublishEndpoint _publishEndpoint; // MassTransit
public ConfirmarPedidoHandler( IPedidoRepository repository, IUnitOfWork unitOfWork, IPublishEndpoint publishEndpoint) { _repository = repository; _unitOfWork = unitOfWork; _publishEndpoint = publishEndpoint; } public async Task<ConfirmarPedidoResult> Handle( ConfirmarPedidoCommand request, CancellationToken ct) { var pedido = await _repository.GetByIdAsync(request.PedidoId, ct) ?? throw new NotFoundException("Pedido não encontrado."); var cliente = await _clienteRepository.GetByIdAsync(pedido.ClienteId, ct); pedido.Confirmar(); await _unitOfWork.SaveChangesAsync(ct); // Publica o evento — sem saber quem vai consumir await _publishEndpoint.Publish(new PedidoConfirmadoEvent { PedidoId = pedido.Id, ClienteId = pedido.ClienteId, ClienteEmail = cliente.Email, ClienteNome = cliente.Nome, Total = pedido.CalcularTotal(), ConfirmadoEm = DateTime.UtcNow, Itens = pedido.Itens.Select(i => new ItemConfirmadoDto(i.Produto.Nome, i.Quantidade, i.PrecoUnitario)).ToList() }, ct); return new ConfirmarPedidoResult(pedido.Id, pedido.Status.ToString(), pedido.CalcularTotal()); }
}
Consumer: Serviço de Notificações
// NotificacoesWorker — um Worker Service (.NET) // Program.cs builder.Services.AddMassTransit(x => { x.AddConsumer<PedidoConfirmadoConsumer>();x.UsingRabbitMq((ctx, cfg) => { cfg.Host(builder.Configuration["RabbitMQ:Host"] ?? "localhost", "/neryx", h => { h.Username("guest"); h.Password("guest"); }); cfg.ReceiveEndpoint("notificacoes-pedido-confirmado", e => { // Retry automático: 3 tentativas com backoff exponencial e.UseMessageRetry(r => r.Exponential(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5))); e.ConfigureConsumer<PedidoConfirmadoConsumer>(ctx); }); });});
// Consumer public class PedidoConfirmadoConsumer : IConsumer<PedidoConfirmadoEvent> { private readonly IEmailService _emailService; private readonly ILogger<PedidoConfirmadoConsumer> _logger;
public PedidoConfirmadoConsumer(IEmailService emailService, ILogger<PedidoConfirmadoConsumer> logger) { _emailService = emailService; _logger = logger; } public async Task Consume(ConsumeContext<PedidoConfirmadoEvent> context) { var evento = context.Message; _logger.LogInformation("Processando confirmação do pedido {PedidoId}", evento.PedidoId); await _emailService.EnviarConfirmacaoPedido(new EmailConfirmacaoDto( Para: evento.ClienteEmail, NomeCliente: evento.ClienteNome, NumeroPedido: evento.PedidoId.ToString()[..8].ToUpper(), Total: evento.Total, Itens: evento.Itens, DataConfirmacao: evento.ConfirmadoEm )); _logger.LogInformation("E-mail de confirmação enviado para {Email}", evento.ClienteEmail); }
}
Dead-Letter Queue: o que fazer com mensagens que falham
Quando todas as tentativas de retry esgotam, o MassTransit move a mensagem para uma fila especial de erro — a Dead-Letter Queue (DLQ). Isso evita que mensagens problemáticas bloqueiem o processamento das demais.
// Configurar Dead-Letter Exchange no RabbitMQ via MassTransit cfg.ReceiveEndpoint("notificacoes-pedido-confirmado", e => { e.UseMessageRetry(r => r.Exponential(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)));// Após todas as tentativas, vai para a fila de erro e.UseDelayedRedelivery(r => r.Intervals( TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(60))); e.ConfigureConsumer<PedidoConfirmadoConsumer>(ctx);
}); // Mensagens na DLQ ficam visíveis no dashboard do RabbitMQ (porta 15672) // Você pode reprocessá-las manualmente quando o problema for corrigido
Idempotência: o consumidor pode receber a mesma mensagem duas vezes
Em sistemas distribuídos, "at-least-once delivery" é a garantia padrão — a mesma mensagem pode ser entregue mais de uma vez (em caso de falha antes do ACK). Seus consumers precisam ser idempotentes:
public class PedidoConfirmadoConsumer : IConsumer<PedidoConfirmadoEvent> { private readonly IEmailService _emailService; private readonly INotificacaoRepository _repository;public async Task Consume(ConsumeContext<PedidoConfirmadoEvent> context) { var evento = context.Message; // Verificar se já processamos este evento var jaProcessado = await _repository.ExisteNotificacaoAsync(evento.PedidoId); if (jaProcessado) { _logger.LogInformation("Evento {PedidoId} já processado. Ignorando duplicata.", evento.PedidoId); return; } await _emailService.EnviarConfirmacaoPedido(...); // Registrar que processamos await _repository.SalvarNotificacaoAsync(new Notificacao(evento.PedidoId, evento.ClienteEmail, DateTime.UtcNow)); }
}
Monitorando filas no dashboard do RabbitMQ
Com o Docker Compose rodando, acesse http://localhost:15672 (login: guest/guest). Você pode ver em tempo real: mensagens na fila, taxa de publicação/consumo, mensagens em erro na DLQ, e estado dos consumers conectados. É a forma mais rápida de diagnosticar problemas de mensageria em desenvolvimento.
RabbitMQ vs Amazon SQS
RabbitMQ é ideal para ambiente on-premises, desenvolvimento local e controle total sobre a infraestrutura. Amazon SQS (integrado ao MassTransit com o pacote MassTransit.AmazonSqS) é melhor para quem já está na AWS e quer eliminar a operação do broker. A mudança entre os dois exige apenas trocar a configuração do UsingRabbitMq por UsingAmazonSqs — os consumers e publishers ficam idênticos.
Conclusão
Mensageria assíncrona com RabbitMQ e MassTransit desacopla seus microsserviços de forma robusta: o publisher não precisa saber quem consome, retry é automático, e Dead-Letter Queues evitam perda de mensagens. MassTransit adiciona uma camada de abstração que torna o código portável para outros brokers sem reescrita.
Os padrões mostrados aqui — contratos compartilhados, consumers idempotentes, retry com backoff exponencial — são os mesmos usados em sistemas de produção de alta escala.
Se você está desenhando a arquitetura de mensageria de um sistema ou migrando de chamadas HTTP síncronas para eventos, a Neryx pode ajudar com a estratégia e implementação. Consultoria inicial gratuita.
Leitura complementar: