📖 Antes da aula
Leia para reconhecer o problema. Nada aqui exige que você já saiba mensageria.
O pedido continua. O pagamento ganha seu tempo. [01]
No Cap 01, construímos uma API: um programa que recebe requisições HTTP, aplica regras e guarda pedidos no PostgreSQL, um banco de dados que roda dentro de um container Docker. Até aqui, tudo acontecia dentro de um único programa: o pedido chegava, a regra rodava, o banco salvava, a resposta voltava.
Agora vamos separar dois trabalhos que parecem um só: receber a solicitação de pagamento e processar o pagamento. Eles vão rodar em programas diferentes, em momentos diferentes. Para ligá-los, usaremos uma fila de mensagens.
O Pedido 4711 é nosso apelido no desenho. Na API, cada pedido recebe um UUID (identificador universal único): um código como 89e83d74-29b8-4a52-aaf0-2f33877c9f99. Você usará o ID retornado pelo seu projeto.
Hoje usamos pagamentos simulados: APROVAR e RECUSAR são cenários controlados de aula. Não há banco financeiro, cartão, Pix ou dinheiro real envolvido.
Uma tarde, uma história verificável [02]
A tarde tem quatro partes. Primeiro entendemos por que filas existem e como o RabbitMQ entrega mensagens — com desenhos e fluxos animados. Depois colocamos a mão no painel, sem código. Só então o agente constrói os dois serviços, e terminamos provocando falhas de propósito.
| Início | Tempo | Etapa | Pontos |
|---|---|---|---|
| 13:30 | 15 min | Abertura: retomada e diagnóstico com o agente trabalhando em paralelo | 03 |
| 13:45 | 20 min | Parte 1 · Por que uma fila: síncrono × assíncrono, a conta da dependência | 04–05 |
| 14:05 | 20 min | Parte 1 · O que viaja: anatomia da mensagem; comando, evento e consulta | 06–07 |
| 14:25 | 25 min | Parte 2 · Como o RabbitMQ entrega: peças, exchanges, fila e ack | 08–10 |
| 14:50 | 25 min | Lab A · RabbitMQ no Docker e primeira fila de treino no painel | 11–12 |
| 15:15 | 15 min | ☕ Intervalo | — |
| 15:30 | 10 min | Parte 3 · O fluxo completo e o mapa da bancada | 13–14 |
| 15:40 | 20 min | Contrato, spec e leitura do código Spring AMQP antes de gerá-lo | 15–16 |
| 16:00 | 25 min | Pedidos publica; bancada no Insomnia; consumidor desligado | 17–19 |
| 16:25 | 25 min | Pagamentos consome; o resultado volta para Pedidos | 20–21 |
| 16:50 | 25 min | Parte 4 · Garantias, três confirmações, duplicidade e falhas | 22–25 |
| 17:15 | 15 min | Desafio curto e fechamento | 26–27 |
Cada etapa prática segue entender → prever → orientar o agente → executar → conferir. O prompt funciona em Claude Code, Codex CLI ou outro agente capaz de trabalhar no projeto. A decisão continua com você.
📓 Abrir caderno livre · Baixar collection do Insomnia · Guia de execução e recuperação
🏫 Em sala
Entenda, preveja, execute e confira
Coloque o agente para ler enquanto conversamos [03]
Um diagnóstico leva alguns minutos: o agente precisa ler arquivos e rodar testes. Em vez de esperar parado, dispare o prompt agora e siga para a teoria. Quando voltar ao terminal, a resposta estará pronta para ser revisada.
Queremos um diagnóstico com evidências: versão do Java, banco, endpoints e testes. Uma lista de arquivos não prova que a aplicação funciona; um teste passando, sim.
[CONTEXTO] Vou continuar a API de Pedidos do Cap 01 com pagamentos simulados por RabbitMQ. [TAREFA] Sem criar nem editar arquivos, leia AGENTS.md, CLAUDE.md e as specs relevantes. Identifique endpoints, status do pedido, configuração do Postgres e Docker Compose existente. Execute os testes unitários pelo Maven Wrapper. [LIMITES] Não instale dependências nem reinicie ou remova containers. Não presuma que concluí as tarefas anteriores. [ACEITE] Separe funcionando com prova, falhando e ainda ausente. Mostre como criar e consultar um pedido de 2 cafés a 18.90. [PAUSA] Proponha o menor ponto de retomada e aguarde meu OK antes de editar.
A regra da tarde: cada etapa aprovada vira um commit
Hoje o agente vai mexer em muitos arquivos, em dois serviços. Para não perder o fio, todo prompt a partir do 2 termina com uma linha [COMMIT]: depois que você revisa o diff e dá OK, o agente grava a etapa num commit e faz push para a branch main do seu repositório no GitHub.
Commit é uma foto do projeto guardada no Git da sua máquina, com uma mensagem dizendo o que mudou. Push envia esses commits para o repositório remoto, no GitHub. São coisas diferentes: commit sem push só existe no seu computador.
🌿 Só para quem ainda NÃO tem este projeto no GitHub
Já subiu o projeto de Pedidos no Cap 01? Pule este bloco. Só confira no terminal que git remote -v mostra o endereço do GitHub e que git status não tem mudanças que você não reconhece.
Se o seu projeto ainda não está em nenhum repositório remoto, faça agora, antes do Prompt 2:
- Em github.com/new, crie um repositório. Nome sugerido:
pedidos. Público ou privado, como preferir; se for privado, adicione como colaborador quem vai revisar seu trabalho. - Não marque “Add a README”, “.gitignore” nem “license”. O repositório precisa nascer vazio; se tiver um README, o primeiro push encontra outro histórico e falha.
- Na página que aparece, copie o link do repositório (HTTPS, como
https://github.com/seu-usuario/pedidos.git, ou SSH). - Copie o prompt abaixo, troque
COLE-AQUI-O-LINKpelo seu link e envie ao agente.
[CONTEXTO] Meu projeto de Pedidos ainda não está em nenhum repositório remoto. Criei um repositório vazio no GitHub: COLE-AQUI-O-LINK [TAREFA] Mostre git status e git remote -v. Se a pasta ainda não for um repositório Git, inicialize com a branch main. Revise ou crie o .gitignore para Java, Maven e IntelliJ (target/, .idea/, *.iml, .env, logs). Mostre a lista exata de arquivos que entrariam no primeiro commit. [LIMITES] Nunca inclua tokens, senhas pessoais ou arquivos .env. Não use force push e não reescreva histórico. Se o push pedir autenticação, pare e me explique o que fazer: eu mesmo faço o login (gh auth login ou chave SSH). [ACEITE] Após meu OK na lista: commit inicial em português, remote origin apontando para o link, push da main com upstream. Mostre git log --oneline -3, git remote -v e o endereço da branch main no GitHub. [PAUSA] Aguarde meu OK antes do commit e antes do push.
Prova: abra o link no navegador e veja seus arquivos na branch main. A senha aula-local do RabbitMQ que entrará no Compose é uma credencial didática, só da sua máquina; senhas e tokens de verdade nunca vão para o repositório.
Por que uma fila?
Antes de qualquer ferramenta: qual problema a mensageria resolve, quanto ela custa e o que exatamente viaja de um programa para outro.
Esperar na ligação ou deixar uma comanda? [04]
Duas formas de um programa pedir algo a outro
Comunicação síncrona: quem chama espera a resposta daquela chamada antes de continuar. É o HTTP que você já conhece: o Insomnia envia um POST e fica aguardando o status. Se Pedidos chamasse Pagamentos por HTTP e esperasse a aprovação, a resposta à cliente dependeria de Pagamentos estar no ar, rápido, naquele exato momento.
Comunicação assíncrona: quem pede entrega a solicitação a um intermediário e segue sua vida. O resultado chega depois, por outro caminho. Pedidos não conversa diretamente com Pagamentos: deixa uma mensagem num broker (o “trilho de comandas”), e Pagamentos a busca quando puder.
O nome técnico do problema da primeira lanchonete é acoplamento temporal: os dois programas precisam estar vivos ao mesmo tempo para o trabalho acontecer. A fila quebra esse acoplamento: Pedidos pode aceitar solicitações às 14h, Pagamentos pode processá-las às 14h05.
Avance o fluxo abaixo um passo por vez. Antes de cada clique, diga em voz alta o que espera ver na faixa de cima e na de baixo.
Ler o percurso sem animação
Na nossa API, o HTTP continua tendo resposta. O que muda é seu significado: 202 Accepted é um código HTTP que quer dizer “aceito para processamento, ainda não terminei”. Para saber o resultado, a cliente consulta de novo (GET /pedidos/{id}). Não vamos manter a requisição HTTP aberta esperando o pagamento.
| Código HTTP | Significa | Quando aparece hoje |
|---|---|---|
| 200 OK | Deu certo; aqui está a resposta | Consultar o pedido; repetir uma solicitação já concluída |
| 201 Created | Criei algo novo | Criar pedido |
| 202 Accepted | Aceitei; vou processar depois | Solicitar pagamento |
| 400 Bad Request | Seu pedido veio mal formatado | JSON inválido, cenário desconhecido |
| 404 Not Found | Não encontrei | Pedido que não existe |
| 409 Conflict | Conflita com o estado atual | Pedido já pago; mesma chave com outro conteúdo |
| 503 Service Unavailable | Não consigo atender agora | RabbitMQ fora do ar ao publicar |
| 504 Gateway Timeout | Esperei outro serviço e desisti | Só no desenho síncrono do fluxo acima |
Regra de bolso: códigos 2xx são sucesso, 4xx são problema no que o cliente mandou, 5xx são problema do lado do servidor.
Antes de revelar: a cliente recebeu 202. O pedido está pago?
Escolha uma previsão. Depois confira a explicação.
A conta da dependência [05]
“Assíncrono é mais robusto” é uma frase fácil de repetir e difícil de justificar. Vamos fazer a conta.
Disponibilidade se multiplica em cadeia
Disponibilidade é a fração do tempo em que um serviço responde corretamente. 99,9% parece quase perfeito, mas num mês de 30 dias (43.200 minutos) significa 43 minutos fora do ar.
Quando um serviço depende de outro numa chamada síncrona, a chance de os dois estarem de pé ao mesmo tempo é o produto das disponibilidades: 99,9% × 99% = 98,9%. O elo mais fraco puxa a cadeia inteira para baixo — e cada serviço novo na cadeia multiplica de novo.
O mesmo vale para o tempo de resposta (latência): na chamada síncrona, a espera da cliente é a soma das esperas de todos os serviços chamados. Se Pagamentos leva 3 s, a cliente leva pelo menos 3 s.
Na versão assíncrona, aceitar a solicitação depende de Pedidos e do broker — não de Pagamentos. Mas atenção: o pagamento em si continua dependendo de Pagamentos. A fila não faz Pagamentos funcionar; ela só permite que a solicitação espere.
Calculadora: mexa em Pagamentos e compare
Premissas fixas: Pedidos 99,9%; RabbitMQ 99,9%; mês de 30 dias; limite de espera (timeout) de 5 s.
Só Pagamentos, sozinho, fica fora .
Síncrono (Pedidos chama e espera)
Espera da cliente:
Chance de aceitar a solicitação:
Cliente sem conseguir pagar:
Assíncrono (fila no meio)
Espera da cliente:
Chance de aceitar a solicitação:
Cliente sem conseguir solicitar:
A barra compara minutos por mês em que a solicitação não pode ser aceita. No assíncrono, o resultado ainda espera Pagamentos voltar.
Outro benefício: a fila absorve picos
Imagine uma promoção: 500 pedidos de pagamento chegam no mesmo minuto e Pagamentos só processa 100 por minuto. Na chamada síncrona, 400 clientes esperam até o timeout ou recebem erro. Com a fila, as 500 solicitações são aceitas; Pagamentos processa no seu ritmo, e a fila esvazia em cinco minutos. Esse efeito de “amortecedor” se chama nivelamento de carga (load leveling).
Antes de revelar: qual situação justifica MENOS uma fila?
Escolha uma previsão. Depois confira a explicação.
O que viaja dentro de uma mensagem [06]
Falamos em “mensagem” o tempo todo. Mas o que é, fisicamente, a coisa que sai de Pedidos e chega em Pagamentos?
Envelope e carta
Uma mensagem é um pacote de dados com duas partes. As propriedades (também chamadas de cabeçalhos) funcionam como o envelope: dizem o formato do conteúdo, se ela deve ser guardada em disco, qual é sua identificação. O corpo (body) é a carta: os dados de negócio, que no nosso caso são um JSON.
O broker lê o envelope para saber como tratar a mensagem, mas não abre a carta. Para o RabbitMQ, o corpo é só uma sequência de bytes. Quem entende o JSON é o consumidor. Além do envelope, cada publicação informa para qual exchange vai e com qual chave de roteamento — veremos isso na Parte 2.
Serialização: de objeto Java a bytes, e de volta
Um objeto Java vive na memória de um programa. Para atravessar a rede, precisa virar bytes: isso é serializar. Do outro lado, os bytes voltam a ser objeto: desserializar. Usaremos JSON porque é texto legível, que você consegue conferir no painel. A biblioteca Jackson, que o Spring já usa no HTTP, faz a conversão.
- Pedidos monta um objeto
Solicitacaocom os dados do pedido. - Jackson transforma o objeto em JSON, e o JSON em bytes.
- Os bytes viajam dentro da mensagem até a fila.
- Pagamentos recebe os bytes e Jackson reconstrói um objeto
Solicitacao— uma cópia, em outro programa.
public record Solicitacao(int versao, String solicitacaoId, String pedidoId,
BigDecimal valor, String cenario) { }Um record é uma classe Java curta, só com dados: o compilador gera construtor e métodos de leitura. Repare no campo versao. Os dois serviços são implantados separadamente; um dia, Pedidos pode mandar um formato novo enquanto Pagamentos ainda roda o antigo. O número da versão permite ao consumidor reconhecer o formato e recusar o que não entende, em vez de interpretar errado.
Antes de revelar: Pedidos mandou "valor": "trinta e sete". Quem percebe o erro?
Escolha uma previsão. Depois confira a explicação.
Comando, evento e consulta [07]
Nem toda mensagem tem a mesma intenção. Dar o nome certo evita desenhos confusos — e diz quem é dono de cada decisão.
Por que a diferença importa
Um comando tem dono: “Pagamentos, processe isto”. Se o nome for ProcessarPagamento e houver três serviços ouvindo, algo está errado — quem de fato processa? Um evento é o contrário: quem publica PagamentoProcessado não precisa saber quem se interessa. Amanhã um serviço de e-mail pode começar a ouvir sem que Pagamentos mude uma linha. Essa liberdade tem nome: baixo acoplamento.
No nosso laboratório, o nome das exchanges revela a intenção: pagamentos.comandos leva a solicitação; pagamentos.eventos traz o resultado.
Classifique: comando, evento ou consulta?
ReservarEstoque { pedidoId, itens }PedidoCriado { pedidoId, total }GET /pedidos/89e83d74…PagamentoRecusado { solicitacaoId, motivo }EnviarEmailConfirmacao { email, pedidoId }Qual o total do pedido 4711?Como o RabbitMQ entrega
As peças do broker, as regras de roteamento e a vida de uma mensagem dentro da fila — tudo o que você vai ver acontecer no painel daqui a pouco.
Seis peças, uma função cada [08]
Um broker de mensagens (intermediário de mensagens) é um programa servidor especializado em receber, guardar e entregar mensagens entre outros programas. Usaremos o RabbitMQ, um broker de código aberto muito usado com Java. Ele fala o protocolo AMQP (Advanced Message Queuing Protocol) — as regras de conversa entre aplicação e broker, como o HTTP é para navegador e servidor.
| Peça | Significado | No nosso caso |
|---|---|---|
| Produtor | Programa que publica uma mensagem | Pedidos envia a solicitação |
| Mensagem | Envelope (propriedades) + carta (corpo) | ID da solicitação, pedido, valor e cenário |
| Broker | Programa que recebe, guarda e encaminha mensagens | RabbitMQ |
| Exchange | Ponto de roteamento dentro do broker; não guarda mensagens | pagamentos.comandos |
| Fila | Lugar onde mensagens aguardam entrega, em ordem de chegada | pagamentos.solicitacoes |
| Consumidor | Programa que recebe e processa | Serviço de Pagamentos |
Duas palavras completam o endereço. Uma routing key (chave de roteamento) é um texto enviado junto com a publicação, como pagamento.solicitar. Um binding (ligação) é uma regra configurada no broker: “desta exchange, mande para esta fila as mensagens com tal chave”.
Conexão e canal: como a aplicação fala com o broker
A aplicação Java abre uma conexão de rede (TCP, o mesmo tipo de conexão que o HTTP usa por baixo) com o RabbitMQ na porta 5672 e, dentro dela, canais — conversas leves e independentes que compartilham a mesma conexão. Você não vai programar isso: o Spring AMQP cuida. Mas ajuda a ler o painel, que mostra abas Connections e Channels. Se Pagamentos está “ligado” mas não aparece em Connections, ele não está conversando com o broker.
O painel também fala em virtual host (vhost): um “espaço” separado dentro do mesmo broker, com filas e permissões próprias — como pastas diferentes num mesmo disco. Usaremos só o padrão, chamado /.
Na volta, os papéis se invertem: Pagamentos vira produtor do resultado (na exchange pagamentos.eventos) e Pedidos vira consumidor (da fila pedidos.resultados). Produtor e consumidor são papéis numa conversa, não tipos de programa.
Antes de revelar: o serviço de Pagamentos está desligado. Quem guarda a solicitação?
Escolha uma previsão. Depois confira a explicação.
A exchange roteia; não guarda [09]
Quatro tipos de exchange, uma pergunta: para quais filas copio?
Quando uma mensagem chega, a exchange compara a chave de roteamento com os bindings de cada fila ligada a ela e copia a mensagem para todas as que casarem. A regra de comparação depende do tipo da exchange:
- direct — casa quando a chave é exatamente igual à do binding. Um destino claro por tipo de mensagem. É o nosso caso.
- fanout — ignora a chave e copia para todas as filas ligadas. Útil para avisar vários interessados sobre um fato.
- topic — a chave é dividida em palavras separadas por ponto, e o binding pode usar curingas:
*vale exatamente uma palavra;#vale zero ou mais.pagamento.*casa compagamento.aprovado, mas não compagamento.cartao.aprovado. - headers — compara propriedades da mensagem em vez da chave. Raro; só para você reconhecer o nome.
Existe ainda a exchange padrão, sem nome (""), do tipo direct: toda fila fica ligada a ela automaticamente, usando o próprio nome como chave. É por isso que dá para “publicar direto na fila” pelo painel — na verdade, você publica na exchange padrão.
Ler o percurso sem animação
Simulador: publique e veja quem recebe
Antes de revelar: exchange topic, binding pagamento.*, chave pagamento.cartao.aprovado. A fila recebe?
Escolha uma previsão. Depois confira a explicação.
Fila: Ready, Unacked e ack [10]
Entregar não é o mesmo que terminar
Uma fila guarda mensagens em ordem de chegada (FIFO, first in, first out: a primeira que entra é a primeira que sai). Cada mensagem passa por estados que o painel mostra como contadores:
- Ready (pronta): está na fila, disponível, esperando um consumidor.
- Unacked (entregue, sem confirmação): o RabbitMQ entregou a um consumidor, mas continua guardando uma cópia até receber a confirmação.
- Ack (acknowledgment, confirmação): o consumidor avisa “terminei”. Só agora a mensagem é apagada.
- Se a conexão do consumidor cair antes do ack, a mensagem volta para Ready, marcada como
redelivered(reentregue), e irá para o próximo consumidor disponível.
O prefetch limita quantas mensagens sem ack um consumidor pode segurar ao mesmo tempo. Com prefetch 1, ele recebe uma, confirma, e só então recebe a próxima. Com dois consumidores na mesma fila — os consumidores concorrentes — cada mensagem vai para um deles, nunca para os dois: é assim que se divide trabalho entre instâncias.
Quando o consumidor não consegue: nack, requeue e fila de erros
Nem toda entrega termina em ack. O consumidor tem outras respostas possíveis:
- Nack (negative acknowledgment, “não consegui”) ou reject (“recuso”): as duas dizem ao RabbitMQ que o processamento falhou. A diferença entre elas é técnica e não importa hoje.
- Junto com o nack vai uma escolha, o requeue (“recolocar na fila”). Com
requeue = true, a mensagem volta para Ready e será entregue de novo. Comrequeue = false, ela sai da fila. - Saiu com
requeue = false? Se a fila tiver uma fila de erros configurada, a mensagem vai para lá em vez de sumir. O nome técnico é DLQ (dead letter queue, fila de mensagens “mortas”), o mesmo usado no AWS SQS. No nosso kit são as filas terminadas em.erros.
Cuidado com requeue = true para um erro que sempre acontece: a mensagem volta, falha, volta, falha… para sempre. Por isso limitamos as tentativas (três, configuradas no consumidor — ponto 16) e depois mandamos para a fila de erros.
Durável ou temporária?
Ao criar uma fila, você escolhe se ela é durável (Durable): a fila continua existindo depois que o RabbitMQ reinicia. A alternativa, Transient (temporária), desaparece no reinício. Todas as filas da aula são duráveis. Atenção: durável fala da fila; para as mensagens sobreviverem, elas também precisam ser publicadas como persistentes (ponto 17).
Ler o percurso sem animação
| Indicador do painel | Leia assim |
|---|---|
| Ready | Disponível para ser entregue a um consumidor |
| Unacked | Entregue, mas ainda sem confirmação de processamento |
| Total | Ready + Unacked |
| Consumers | Quantidade de consumidores conectados à fila |
Antes de revelar: o painel mostra Ready 0 e Unacked 1 há cinco minutos. O que isso indica?
Escolha uma previsão. Depois confira a explicação.
Mão no broker, sem código
Subir o RabbitMQ e brincar com uma fila de treino no painel. Tudo o que vimos na teoria vira contador na tela.
Uma fila precisa de um lugar para existir [11]
O RabbitMQ é outro programa. Vamos executá-lo em um container, como já fizemos com o Postgres. A imagem com sufixo management inclui o painel visual de administração. Um healthcheck é uma verificação automática de saúde: indica se o serviço está pronto para responder. Um volume guarda os dados do broker fora do container, para sobreviverem a reinícios.
| Porta | Protocolo | Quem usa |
|---|---|---|
| 5672 | AMQP | As aplicações Java, para publicar e consumir |
| 15672 | HTTP | Você, pelo navegador, para ver o painel |
[CONTEXTO] A API já usa Postgres no Docker. Quero adicionar RabbitMQ à infraestrutura atual. [TAREFA] Inspecione o Compose e proponha adicionar RabbitMQ 4.1-management com volume nomeado, healthcheck, usuário local aula e senha didática aula-local. Publique 5672 para as aplicações e 15672 para o painel, somente em 127.0.0.1. Confira conflitos de porta. [LIMITES] Preserve o Postgres e seus volumes. Não use down -v, não apague dados e não crie outro Postgres por conveniência. Não altere Java nesta etapa. Credenciais são apenas para ambiente local. [ACEITE] Após meu OK, suba somente o necessário. Mostre healthcheck saudável e painel acessível. Explique a diferença entre 5672 (mensagens) e 15672 (painel). [PAUSA] Mostre o plano e aguarde meu OK. [COMMIT] Só depois do meu OK sobre o diff: commit apenas dos arquivos desta etapa, mensagem em português dizendo o que mudou, e push para origin main. Sem segredos e sem force push. Mostre git log -1 --stat.
Como abrir o painel do RabbitMQ
localhost é o nome que todo computador usa para se referir a ele mesmo. O container do RabbitMQ roda na sua máquina e o Compose “publica” a porta 15672 dele na sua máquina; por isso o navegador o encontra em localhost. O 127.0.0.1 no Compose garante que só você acessa: ninguém da rede da sala consegue abrir o seu painel.
- Espere o container ficar saudável. O RabbitMQ leva uns 15–20 segundos para ligar; o agente mostra o status
healthynodocker compose ps. - No navegador, abra
http://localhost:15672(éhttp, sem “s”). - Entre com usuário
aulae senhaaula-local— os mesmos que o Prompt 2 colocou no Compose. São credenciais didáticas, válidas só na sua máquina. - Você cai na aba Overview, com gráficos zerados. Nas abas de cima ficam Connections, Channels, Exchanges e Queues and Streams.
| Aba do painel | O que mostra | Quando você vai olhar |
|---|---|---|
| Overview | Resumo geral: gráficos de mensagens por segundo, totais | Para ver se algo está chegando |
| Connections | Programas conectados ao broker | Pedidos e Pagamentos estão realmente ligados? |
| Channels | As conversas dentro de cada conexão | Raramente; diagnóstico avançado |
| Exchanges | As exchanges e seus bindings; permite publicar à mão | Pontos 12 e 24 |
| Queues and Streams | As filas e seus contadores Ready, Unacked, Consumers | A tarde toda |
| Admin | Usuários, permissões, virtual hosts | Não usaremos |
Streams, que aparecem no nome da aba, são outro recurso do RabbitMQ (um registro de mensagens que pode ser relido). Não usaremos hoje.
O painel não abre ou o login falha
Página não carrega: o container ainda está subindo ou caiu; peça ao agente docker compose ps e os logs do serviço rabbitmq. Porta ocupada: outro programa usa a 15672; troque a porta do host no Compose, por exemplo 127.0.0.1:15673:15672, e abra localhost:15673. Usuário ou senha recusados: o usuário é criado só na primeira vez que o volume nasce; se o volume já existia com outra senha, use a antiga ou peça ao agente um diagnóstico — não apague o volume sem entender o que há nele.
Se algo falhar
Conexão recusada: confirme Docker e container. Porta ocupada: escolha outra porta do host e ajuste a aplicação. Painel aberto, Java sem conexão: confira a porta AMQP e as credenciais. Não apague volumes para tentar novamente.
Sua primeira fila, à mão [12]
Antes de qualquer Java, vamos provocar no painel tudo o que o fluxo animado mostrou: Ready, entrega sem ack, reentrega, consumo e mensagem sem rota. Usaremos uma fila de treino descartável, treino.cafe. Nas filas pagamentos.* e pedidos.*, depois, só observe.
- Abra o painel.
http://localhost:15672, usuárioaula, senhaaula-local(veja o ponto 11). Algumas seções das páginas vêm recolhidas: clique no título para abri-las. Os contadores atualizam a cada poucos segundos. - Crie a fila. Aba Queues and Streams → Add a new queue (fica no fim da página). Preencha: Virtual host
/(o padrão), Type deixe o padrão, Nametreino.cafe, Durability Durable, Arguments em branco → Add queue. - Publique três mensagens. Clique em
treino.cafe→ seção Publish message. Payload é o corpo da mensagem, a “carta” do ponto 06. Nele, escreva{"texto":"café 1"}e publique. Repita com café 2 e café 3. Preveja antes de olhar: quanto marca Ready? - Espie sem consumir. Seção Get messages. O campo Ack Mode diz o que o painel responde ao RabbitMQ depois de pegar a mensagem:
Nack message requeue true(“não processei, devolva”),Automatic ack(“confirmado”, a mensagem some), e duas variantes de Reject. EscolhaNack message requeue true, Messages = 1 → Get Message(s). Você vê a mensagem, e ela volta para a fila. Faça de novo e procure o campo Redelivered. - Consuma de verdade. Mesmo lugar, Ack Mode =
Automatic ack→ Get Message(s). Preveja: e agora, Ready? - Roteie por uma exchange topic. Aba Exchanges →
amq.topic(já vem pronta: as exchanges com prefixoamq.são criadas pelo próprio RabbitMQ) → Add binding from this exchange: para a filatreino.cafe, chavecafe.#→ Bind. Publique nessa exchange com chavecafe.expresso. Depois publique com chavecha.verdee leia o aviso que aparece. - Limpe só o que é seu. Na fila
treino.cafe, Delete Queue. Nenhuma outra.
| Depois do passo | Sua previsão | 👀 O que deve aparecer |
|---|---|---|
| 3 · três publicações | Ready = ? | verReady 3, Unacked 0, Consumers 0. Ninguém consome, e as mensagens esperam. |
| 4 · Get com Nack requeue | Ready = ? Redelivered = ? | verReady continua 3. Na segunda espiada, Redelivered: ● — é a mesma mensagem, entregue de novo. |
| 5 · Get com Automatic ack | Ready = ? | verReady 2. A mensagem foi confirmada na entrega e sumiu, sem ninguém processá-la. É isto que o ack automático antes do trabalho faz: se o consumidor cair, perde-se a mensagem. |
| 6 · chave cafe.expresso | chega à fila? | verSim:cafe.# casa. Ready 3. A publicação na aba da fila usa a exchange padrão; esta usou amq.topic. |
| 6 · chave cha.verde | chega à fila? | verNão. O painel avisa Message published, but not routed. A publicação “deu certo” e a mensagem sumiu — o erro silencioso do ponto 09. |
☕ Intervalo · 15:15–15:30
Deixe o RabbitMQ ligado. Na volta, o agente constrói os dois serviços.
Do desenho ao código
O percurso completo, o mapa da bancada e a construção guiada: primeiro o contrato, depois quem publica, depois quem consome, por fim a volta do resultado.
Siga a mensagem. Observe o estado. [13]
Agora juntamos tudo: HTTP, banco, exchange, fila, consumidor e o caminho de volta. Avance um passo por vez. Primeiro diga o que espera ver; depois confira estado do pedido, lugar da mensagem e evidência. O desenho é uma simulação didática, sem conexão com seu Docker.
Ler o percurso sem animação
Dois serviços, cada um com seus dados [14]
localhost. Um Postgres hospeda dois bancos para economizar recursos; nenhum serviço toca o banco do outro.No desenho, host é o seu computador, do lado de fora dos containers. Docker executa o PostgreSQL e o RabbitMQ. As duas aplicações Java rodam em terminais separados no IntelliJ. Não é preciso colocar tudo em containers para usar mensageria.
Por que cada serviço tem o próprio banco
Se Pagamentos pudesse escrever direto na tabela de pedidos, bastaria um UPDATE para pular a fila — e as regras de Pedidos (não reabrir pedido pago, conferir valor) seriam ignoradas. Com bancos separados, a única forma de mudar um pedido é pedir a Pedidos, por HTTP ou mensagem. Cada serviço é dono dos seus dados e das suas regras. Em produção, credenciais e permissões diferentes reforçam essa separação; no kit, a separação é lógica.
Antes do código, combine a mensagem [15]
O ponto 07 separou comando (“processe este pagamento”) de evento (“este pagamento foi aprovado ou recusado”). Agora escrevemos o formato exato de cada um. O JSON é o contrato que os dois serviços precisam entender — e, como são implantados separadamente, ele precisa ser explícito.
| Campo da solicitação | Por que existe |
|---|---|
| versao: 1 | Identifica o formato conhecido pelos dois serviços |
| solicitacaoId | Identifica esta tentativa; deve ser reutilizado no reenvio |
| pedidoId | Liga o pagamento ao pedido correto |
| valor: 37.80 | Valor calculado pela API a partir dos itens |
| cenario: APROVAR ou RECUSAR | Controla exclusivamente o simulador da aula |
Transação local: o que ela protege e o que não protege
Uma transação local agrupa mudanças no mesmo banco: ou todas são confirmadas (COMMIT), ou todas são desfeitas. Vamos usá-la para mudar o estado do pedido junto com o registro da solicitação — nunca um sem o outro. Mas o RabbitMQ é outro sistema: a publicação não entra nessa transação. Guarde essa frase; ela volta no ponto 25.
A resposta de negócio preserva os IDs e o valor e acrescenta aprovado e motivo. O cliente não escolhe o valor enviado ao serviço; a API o lê do pedido.
[TAREFA] Escreva uma spec curta para pagamentos assíncronos simulados, antes de implementar. Declare desde o início as filas pagamentos.solicitacoes.erros e pedidos.resultados.erros. Configure as filas principais para encaminhar falhas a elas, evitando mudar argumentos de filas já existentes durante a aula. Contrato HTTP: POST /pedidos/{pedidoId}/pagamentos recebe solicitacaoId UUID e cenario APROVAR ou RECUSAR. O valor vem do pedido, não do cliente. Use POST /pedidos e GET /pedidos/{id} existentes. Estados: ABERTO passa a EM_PAGAMENTO; aprovação passa a PAGO; recusa volta a ABERTO. Itens não mudam durante EM_PAGAMENTO. Pedido ausente: 404; estado incompatível ou chave reutilizada com outro conteúdo: 409; formato inválido: 400. Mensagens JSON versão 1: solicitação com solicitacaoId, pedidoId, valor, cenario; resultado com os mesmos IDs/valor, aprovado e motivo. Exchange direct pagamentos.comandos + chave pagamento.solicitar → fila pagamentos.solicitacoes. Na volta: pagamentos.eventos + pagamento.resultado → pedidos.resultados. [ACEITE] 202 só após confirmação do broker e verificação de roteamento; nunca significa aprovado. Defina repetição persistente, resultado conflitante, reinício e recuperação após falha de publicação. [LIMITES] Sem cobrança real, sem banco compartilhado entre serviços, sem afirmar transação única entre Postgres e RabbitMQ. O endpoint síncrono antigo de aprovação deve sair da versão assíncrona. [PAUSA] Liste dúvidas, arquivos e testes em etapas pequenas. Aguarde meu OK. [COMMIT] Só depois do meu OK sobre o diff: commit apenas dos arquivos desta etapa, mensagem em português dizendo o que mudou, e push para origin main. Sem segredos e sem force push. Mostre git log -1 --stat.
O que o agente vai escrever: Spring AMQP em três peças [16]
O agente vai gerar dezenas de linhas. Você só consegue revisar o que reconhece. Spring AMQP é a biblioteca que conecta uma aplicação Spring ao RabbitMQ; ela resume o trabalho em três peças. Leia cada uma pelo efeito, não pela sintaxe.
1. Declarar a topologia: exchange, fila e binding
Ao subir, a aplicação pede ao broker que crie (se ainda não existirem) a exchange, a fila e a ligação entre elas. Por isso a fila aparece no painel assim que Pedidos liga.
@Bean
Declarables topologia() {
var comandos = new DirectExchange("pagamentos.comandos"); // 1. a exchange
var solicitacoes = QueueBuilder.durable("pagamentos.solicitacoes") // 2. a fila durável
.deadLetterExchange("") // falhou de vez? vai para...
.deadLetterRoutingKey("pagamentos.solicitacoes.erros") // ...esta fila de erros
.build();
var ligacao = BindingBuilder.bind(solicitacoes) // 3. o binding
.to(comandos).with("pagamento.solicitar");
return new Declarables(comandos, solicitacoes, ligacao);
}2. Publicar: o RabbitTemplate
O RabbitTemplate é para mensageria o que um cliente HTTP é para requisições: você diz para onde vai e o que vai. O objeto Java é convertido em JSON no caminho.
rabbitTemplate.convertAndSend(
"pagamentos.comandos", // exchange
"pagamento.solicitar", // chave de roteamento
solicitacao); // objeto Java -> JSONA referência da aula vai um passo além: espera a confirmação do broker e verifica se a mensagem não voltou sem rota antes de seguir. Se qualquer das duas falhar, lança exceção — e a API responde 503 em vez de 202.
var correlacao = new CorrelationData();
rabbit.send(exchange, chave, mensagem, correlacao);
var confirmacao = correlacao.getFuture().get(5, TimeUnit.SECONDS); // espera o broker
if (!confirmacao.isAck() || correlacao.getReturned() != null) // recusou ou voltou sem rota?
throw new IllegalStateException("Publicação não confirmada ou sem rota.");3. Consumir: o @RabbitListener
Uma anotação transforma um método comum em consumidor. O Spring abre a conexão, busca mensagens da fila e chama o método para cada uma. Ninguém faz requisição HTTP para Pagamentos.
@RabbitListener(queues = "pagamentos.solicitacoes")
public void receber(Message mensagem) throws Exception {
var solicitacao = json.readValue(mensagem.getBody(), Solicitacao.class); // bytes -> objeto
var resultado = decisoes.registrar(solicitacao); // COMMIT da decisão
publicador.enviar("pagamentos.eventos", "pagamento.resultado", resultado);
} // terminou sem exceção -> o Spring envia o ackE a configuração que liga tudo
spring:
rabbitmq:
publisher-confirm-type: correlated # quero o publisher confirm
publisher-returns: true # quero saber de mensagem sem rota
template:
mandatory: true # devolva se não houver rota
listener:
simple:
acknowledge-mode: auto # ack depois que o método termina bem
prefetch: 1 # uma mensagem por vez
default-requeue-rejected: false # falhou de vez? não volta para a fila
retry:
enabled: true
max-attempts: 3 # 3 tentativas, depois fila de erros@Configuration e @Bean (que você já viu no Cap 01): a classe marcada com @Configuration contém receitas; cada método @Bean devolve um objeto que o Spring cria na subida e entrega a quem precisar. Aqui, a receita devolve a topologia, e o Spring AMQP a envia ao RabbitMQ.
deadLetterExchange("") configura a DLX (dead letter exchange) da fila: para onde ela manda as mensagens rejeitadas sem requeue. Usamos a exchange padrão "", que entrega pelo nome da fila; por isso a deadLetterRoutingKey é o nome da fila de erros.
CorrelationData é uma etiqueta que liga cada publicação à confirmação que volta do broker. getFuture().get(5, SECONDS) quer dizer “espere até 5 segundos pela confirmação”; getReturned() diz se a mensagem voltou por falta de rota.
acknowledge-mode: auto é o ponto que mais confunde. No Spring, “auto” quer dizer: o Spring manda o ack automaticamente depois que seu método termina sem exceção. Não é o “automatic ack” do painel, que confirma na entrega, antes de qualquer trabalho.
retry + default-requeue-rejected: false juntos evitam o loop infinito: três tentativas dentro do consumidor; esgotou, a mensagem é rejeitada sem voltar para a fila e cai na fila de erros configurada na topologia.
Antes de revelar: o método do listener lança exceção nas três tentativas. Onde a mensagem termina?
Escolha uma previsão. Depois confira a explicação.
Pedidos publica. Ainda não aprova. [17]
Vamos construir somente a saída de Pedidos. Um publisher confirm é a confirmação enviada pelo broker ao produtor sobre a publicação. Também precisamos detectar mensagem devolvida por falta de rota; confirmar publicação não substitui conferir a fila de destino.
[CONTEXTO] Implemente somente a primeira etapa da spec aprovada: Pedidos recebe e publica a solicitação. [TAREFA] Adicione Spring AMQP compatível com o Boot do projeto. Declare exchanges, filas duráveis e bindings, incluindo as filas de erro e seus encaminhamentos previstos na spec. Use JSON e mensagem persistente. Registre solicitacaoId e estado no Postgres, atualizando o pedido na mesma transação local. Publique após o registro usando confirmação correlacionada e detecção de retorno sem rota. [LIMITES] Não marque o pedido como PAGO ao publicar. Não implemente Pagamentos ainda. Preserve o núcleo sem dependências de RabbitMQ. Remova o atalho HTTP antigo que recebia aprovação do cliente; registre a mudança de contrato. [ACEITE] Com consumidor parado: solicitação válida recebe 202 após publicação confirmada; pedido EM_PAGAMENTO; mensagem visível na fila. Falha de publicação responde 503 e preserva a chave para recuperação, sem fingir sucesso. Repetir a mesma chave/conteúdo pendente permite republicar; conteúdo diferente é 409. Não há outbox nem reenvio automático neste laboratório. [PAUSA] Mostre testes, diff e o roteiro de prova. Pare antes da próxima etapa. [COMMIT] Só depois do meu OK sobre o diff: commit apenas dos arquivos desta etapa, mensagem em português dizendo o que mudou, e push para origin main. Sem segredos e sem force push. Mostre git log -1 --stat.
Antes de revelar: o RabbitMQ confirmou a publicação. Quem já terminou?
Escolha uma previsão. Depois confira a explicação.
Monte sua bancada no Insomnia [18]
Uma collection reúne requisições salvas. Um ambiente guarda valores reutilizáveis, como a URL da API e o ID do pedido. A collection não executa todo o roteiro sozinha: você envia, observa e atualiza as variáveis indicadas.
Importe insomnia-pedidos.json pela opção de importação de arquivo. Abra o ambiente base: base_url começa em http://localhost:8080. Envie “01 · Criar pedido”; copie o campo id da resposta para pedido_id.
| Requisição | Antes de enviar | Resultado esperado |
|---|---|---|
| 01 · Criar pedido | 2 cafés, preço 18.90 | 201, ABERTO, total 37.80 |
| 02 · Consultar pedido | Copiar ID retornado para pedido_id | 200 com o estado atual |
| 03 · Solicitar aprovação | Pagamentos parado; ID da solicitação estável | 202, registro PENDENTE |
| 04 · Repetir a solicitação | Mesma chave e mesmo conteúdo | 202 se pendente; 200 se concluída |
| 05 · Solicitar recusa | Novo pedido em pedido_recusa_id | 202; depois ABERTO |
| 06 · Conteúdo conflitante | Chave da aprovação, mas cenário RECUSAR | 409 |
| 07 · Pedido inexistente | UUID que não existe no banco | 404 |
[TAREFA] Confira a collection fornecida e adapte apenas URLs e campos que divergirem do contrato aprovado. Preserve a ordem criar → consultar → solicitar → consultar novamente. [ACEITE] Use variáveis de ambiente base_url, pedido_id, pedido_recusa_id e chaves estáveis de solicitação. Explique quais valores eu copio da resposta e onde colo. Inclua aprovação, recusa, repetição, conflito e pedido ausente, com resposta esperada na descrição. [LIMITES] Não gere um novo UUID em cada reenvio. Não crie endpoint para fingir aprovação e não substitua a API por mock. Não inclua credenciais pessoais. [PAUSA] Mostre o arquivo importável e um roteiro de execução manual. [COMMIT] Só depois do meu OK sobre o diff: commit apenas dos arquivos desta etapa, mensagem em português dizendo o que mudou, e push para origin main. Sem segredos e sem force push. Mostre git log -1 --stat.
Desligue o consumidor. A fila continua ali. [19]
Este é o momento em que o fluxo do ponto 04 vira realidade na sua máquina. Mantenha a API de Pedidos e o RabbitMQ ligados. O serviço de Pagamentos fica parado (ou ainda nem existe). Envie a solicitação, consulte o pedido e abra a fila pagamentos.solicitacoes no painel.
Não use “Purge” nem “Delete”. Essas ações removem mensagens ou a fila. Para apenas observar, os contadores bastam. Também não use “Get messages” durante a prova: no ponto 12 você viu que isso entrega a mensagem a você, e pode consumir ou reenfileirar e alterar sua observação.
Antes de revelar: agora desligamos o próprio RabbitMQ. A API consegue garantir 202?
Escolha uma previsão. Depois confira a explicação.
Pagamentos recebe uma tarefa [20]
Um listener é o método configurado para receber as entregas da fila — o @RabbitListener do ponto 16. O Spring AMQP conecta esse método ao RabbitMQ. Ele não precisa de uma requisição HTTP para cada pagamento.
O simulador é determinístico: a mesma entrada sempre dá o mesmo resultado, sem sorteio — APROVAR aprova, RECUSAR recusa. Pagamentos terá sua própria memória durável no Postgres: uma decisão por solicitacaoId, protegida por uma restrição única no banco (a tabela recusa duas linhas com a mesma chave). Se a mesma solicitação chegar outra vez, lê a decisão já salva e devolve o mesmo resultado.
[CONTEXTO] A API publica solicitações versão 1 na fila pagamentos.solicitacoes. [TAREFA] Crie a aplicação pagamentos na pasta pagamentos/, dentro deste mesmo repositório, como projeto Maven independente (não é módulo do pom de Pedidos), Java 21 e a mesma versão Boot de Pedidos, com seu Maven Wrapper. Use Spring AMQP e seu próprio banco pagamentos. Não leia tabelas de Pedidos. Configure porta 8081 para não conflitar. Receba JSON validando versão, IDs, valor positivo e cenário. Simule APROVAR ou RECUSAR de forma determinística. Persista uma única decisão por solicitacaoId, com restrição única. Mesma chave com outro pedido, valor ou cenário deve falhar. Após salvar a decisão, publique resultado na exchange pagamentos.eventos com chave pagamento.resultado. Preserve IDs e valor. Se receber novamente, reutilize a decisão salva e republique o mesmo resultado. Espere confirmação e detecte falta de rota. [LIMITES] Sem SDK de banco financeiro, aleatoriedade, cartão ou cobrança real. Não confirme processamento antes de salvar e publicar. Configure tentativas limitadas e filas de erro para não criar loop infinito; documente sem aprofundar a operação de recuperação. [ACEITE] Ao ligar o serviço, a mensagem pendente sai de Ready; o resultado aparece em pedidos.resultados se seu consumidor estiver parado. Um registro de pagamento sobrevive ao reinício. [PAUSA] Mostre o plano antes de criar; após meu OK, execute, teste e mostre as provas. [COMMIT] Só depois do meu OK sobre o diff: commit apenas dos arquivos desta etapa, mensagem em português dizendo o que mudou, e push para origin main. Sem segredos e sem force push. Mostre git log -1 --stat.
O resultado precisa voltar [21]
Uma fila esvaziar não atualiza o banco de Pedidos por mágica. Agora Pedidos se torna consumidor do resultado. Ele confere a identificação, aplica a transição da máquina de estados do ponto 13 e registra que aquela solicitação terminou.
[TAREFA] Implemente em Pedidos o consumidor da fila pedidos.resultados. Valide versão 1, solicitacaoId existente, mesmo pedido e valor. Neste laboratório determinístico, resultado também deve corresponder ao cenário pedido. Resultado repetido já concluído não altera novamente o pedido. Resultado conflitante deve falhar com evidência e ir para tratamento de erro limitado. Na mesma transação local, conclua a solicitação e aplique: aprovado → PAGO; recusado → ABERTO. Preserve ID, itens e total. Confirme processamento somente após concluir essa transação. Não reabra pedido pago por evento atrasado. [ACEITE] Prove aprovação, recusa, resultado duplicado após reiniciar e conteúdo incompatível. Consultar o pedido deve revelar o estado final; logs sozinhos não bastam. [LIMITES] Sem endpoint que aceite aprovado do cliente. Sem transação global entre bancos e RabbitMQ. Não capture exceções para fingir que tudo funcionou. [PAUSA] Mostre diff, testes e evidências do fluxo completo. [COMMIT] Só depois do meu OK sobre o diff: commit apenas dos arquivos desta etapa, mensagem em português dizendo o que mudou, e push para origin main. Sem segredos e sem force push. Mostre git log -1 --stat.
Quando algo dá errado
Mensagens se repetem, processos caem no meio, publicações ficam incertas. A pergunta profissional não é “vai falhar?”, é “quando falhar, o que acontece com o pedido?”.
Quantas vezes uma mensagem chega? [22]
Três promessas de entrega
Todo sistema de mensagens escolhe, por configuração, uma destas garantias de entrega:
- No máximo uma vez (at-most-once): o consumidor confirma ao receber, antes de trabalhar. Se cair no meio, a mensagem já foi apagada: pode perder, nunca repete. Foi o “Automatic ack” do painel.
- Pelo menos uma vez (at-least-once): o consumidor confirma depois de trabalhar. Se cair no meio, a mensagem volta: nunca perde (com fila durável, mensagem persistente e publisher confirm), mas pode repetir. É a nossa escolha.
- Exatamente uma vez (exactly-once): a promessa que todo mundo quer. Na entrega entre sistemas independentes, ela não existe de graça — sempre há um instante em que “já fiz, mas ainda não avisei”.
A saída prática é combinar: entrega pelo menos uma vez + consumidor idempotente = efeito exatamente uma vez. A mensagem pode chegar duas vezes; o pagamento só acontece uma.
Antes de revelar: um consumidor com ack automático na entrega processa notificações de “pedido saiu para entrega”. Aceitável?
Escolha uma previsão. Depois confira a explicação.
Três confirmações que não significam a mesma coisa [23]
| Sinal | Quem envia → quem recebe | O que prova |
|---|---|---|
| ① HTTP 202 | API → cliente do Insomnia | Solicitação aceita conforme o contrato |
| ② Publisher confirm | RabbitMQ → produtor | Broker assumiu a publicação; não é resultado do negócio |
| ③ Consumer ack | Consumidor → RabbitMQ | Essa entrega foi processada segundo o consumidor |
O diagrama abaixo é um diagrama de sequência: cada coluna é um participante, o tempo corre de cima para baixo e cada seta é uma mensagem. Os números nas setas são as três confirmações da tabela.
Ler o percurso sem animação
Na configuração AUTO do Spring AMQP, o framework confirma após o listener terminar com sucesso. Isso é diferente de mandar o broker considerar a entrega concluída antes do processamento — o “Automatic ack” que você testou no painel.
Antes de revelar: o banco salvou o resultado, mas o processo caiu antes do ack. O que pode acontecer?
Escolha uma previsão. Depois confira a explicação.
A mesma mensagem pode chegar duas vezes [24]
Idempotência: repetir sem mudar o resultado
Uma operação é idempotente quando executá-la uma ou várias vezes produz o mesmo efeito. Compare:
Idempotente
status = PAGO — repetir deixa PAGO.
PUT /pedidos/4711 com o mesmo corpo.
“Grave a decisão de S1, se ainda não existir.”
Não idempotente
saldo = saldo - 37.80 — repetir desconta de novo.
POST /pedidos — cada chamada cria outro pedido.
“Cobre R$ 37,80.”
Como pagamento é, por natureza, não idempotente, nós o tornamos idempotente com uma chave de idempotência: a solicitacaoId. Antes de agir, o consumidor pergunta ao próprio banco: “já processei esta chave?”. Se sim, devolve o que já decidiu. A restrição única na tabela garante isso mesmo que duas entregas cheguem ao mesmo tempo: só uma linha entra.
E quando a chave é a mesma mas o conteúdo mudou (outro valor, outro cenário)? Isso não é repetição, é conflito — alguém reutilizou a chave de forma errada. O consumidor rejeita, sem alterar a decisão anterior.
Ler o percurso sem animação
Simulação rápida · S1 chega três vezes
Escolha uma estratégia e observe o número de efeitos.
[TAREFA] Proponha e execute uma prova de duplicidade usando somente os serviços e dados deste laboratório. Com Pagamentos parado, publique duas vezes a mesma solicitacaoId com o mesmo conteúdo. Ligue o consumidor e confira uma decisão persistida e o pedido correto. Reinicie Pagamentos e Pedidos; republique o mesmo JSON diretamente no painel didático para testar o consumidor, não só a proteção do HTTP. [ACEITE] Conte registros por chave, consulte o pedido e mostre que reiniciar não apaga a proteção contra duplicidade. Teste também a mesma chave com valor incompatível: deve ser rejeitada, sem mudar a decisão anterior. [LIMITES] Não purgue filas nem apague volumes. Faça as publicações manuais apenas no ambiente local de aula. Explique por que enviar com UUID novo seria outra solicitação. [PAUSA] Mostre o roteiro antes de executar ações; depois apresente resultados observados. [COMMIT] Se esta etapa criou ou alterou arquivos (testes, roteiro de prova), só depois do meu OK: commit desses arquivos em português e push para origin main. Sem segredos e sem force push.
👀 Como conferir sem depender de uma mensagem de log
No banco de Pagamentos, conte registros com a mesma chave: deve existir um. No banco de Pedidos, a solicitação mantém um resultado. Consulte o pedido para conferir estado e total. Para republicar após reiniciar, use a exchange e a chave corretas e o JSON original, como no guia de execução.
Como republicar uma mensagem pelo painel (usado no Prompt 8)
Repetir o POST de uma solicitação já concluída não chega ao consumidor: Pedidos responde 200 com o resultado guardado e nem publica. Para testar a proteção do consumidor, publique a mensagem à mão:
- Aba Exchanges →
pagamentos.comandos→ seção Publish message. - Routing key:
pagamento.solicitar. - Properties:
content_type=application/jsonedelivery_mode=2(persistente). - Payload: o JSON original da solicitação, com os IDs reais do seu pedido e da sua solicitação:
{"versao":1,"solicitacaoId":"ID-DA-SOLICITACAO","pedidoId":"ID-DO-PEDIDO","valor":37.80,"cenario":"APROVAR"}Depois de publicar, confira: continua existindo um único registro para aquela chave em Pagamentos, e o pedido não mudou. O passo a passo completo, inclusive para republicar um resultado, está no guia de execução.
Quando o fluxo para, encontre a fronteira [25]
O problema da escrita dupla
Pedidos precisa fazer duas coisas: gravar no Postgres e publicar no RabbitMQ. São dois sistemas, sem uma transação que abranja os dois. Isso se chama escrita dupla (dual write), e sempre deixa uma janela: se o processo cair entre uma e outra, o mundo fica pela metade. É a janela A do ponto 23.
Nossa referência escolhe a ordem “grava, depois publica” e deixa o registro PENDENTE como pista: a cliente recebe 503 e pode reenviar a mesma chave, que republica sem duplicar. Não há republicação automática. Um padrão chamado outbox resolve isso gravando a mensagem numa tabela, na mesma transação, e publicando depois por um processo separado — fica para a continuação de mensageria. Não vamos afirmar que já o implementamos.
| Observação | Hipótese a conferir | Próxima evidência |
|---|---|---|
| Ready cresce, Consumers = 0 | Consumidor desligado ou não conectado | Processo Pagamentos, fila e credenciais |
| Unacked por muito tempo | Listener ainda trabalhando ou travado | Logs, banco e duração do processamento |
| Pedido EM_PAGAMENTO; fila de solicitações vazia | Resultado ainda pendente ou em erro | Fila pedidos.resultados e filas de erro |
| HTTP 503 ao solicitar | Não foi possível confirmar a publicação | Saúde do broker; reenvio com a mesma chave |
| Mensagem em *.erros | Contrato inválido ou falha persistente | Exceção e conteúdo, antes de reenviar |
Uma fila de erro (em inglês, dead letter queue, DLQ) guarda mensagens que não concluíram após as tentativas configuradas. Neste kit, são pagamentos.solicitacoes.erros e pedidos.resultados.erros. É uma contenção básica: a mensagem para de travar a fila e fica disponível para análise. Reprocessamento e políticas mais completas ficam para a continuação.
[CONTEXTO] Tenho um pedido EM_PAGAMENTO que ainda não terminou. [TAREFA] Investigue antes de alterar. Siga solicitacaoId pelos registros e pelas filas. Confira Ready, Unacked, Consumers, mensagens de erro e conexão dos dois serviços. Separe observações de hipóteses. [ACEITE] Localize a última fronteira comprovada e proponha a menor recuperação. Se publicação ficou incerta, reutilize a mesma chave e conteúdo. Se o contrato está inválido, explique a causa antes de reprocessar. [LIMITES] Não apague volumes, não purgue filas, não marque manualmente o pedido como PAGO e não reenvie em loop infinito. [PAUSA] Mostre diagnóstico e plano; aguarde meu OK. [COMMIT] Se aprovarmos uma correção de código, commit só dela, em português, e push para origin main. Diagnóstico sem mudança não gera commit.
Antes de revelar: um pedido está EM_PAGAMENTO há uma hora e a fila de solicitações está vazia. Qual a primeira ação?
Escolha uma previsão. Depois confira a explicação.
Conduza e explique
Um experimento curto, sozinho, e cinco perguntas para responder sem abrir o código.
Agora conduza uma recusa e uma nova tentativa [26]
Crie um pedido novo. Solicite RECUSAR e prove que ele volta a ABERTO sem alterar itens ou total. Depois solicite APROVAR com outra solicitacaoId e prove que termina PAGO. Reenvie o resultado antigo da recusa: ele não deve reabrir o pedido.
Em sala, faça pelo menos a previsão escrita e a primeira metade (recusa). O restante segue para casa.
Ajuda de prompt
[CONTEXTO] Fiz recusa e depois aprovação para o mesmo pedido, com duas solicitacaoId distintas. [TAREFA] Revise minhas evidências e o diff sem editar arquivos. Confira correlação, estado final, preservação do total e tratamento do resultado antigo repetido. [ACEITE] Aponte problemas concretos com reprodução, ou justifique por que o comportamento está correto. Não invente um erro para preencher a revisão. [LIMITES] Não adicionar framework ou serviço. Compare com a spec aprovada. [PAUSA] Entregue um parecer curto: evidência, risco e correção mínima quando necessária.
👀 Resultado de referência
Tentativa A recusada → pedido ABERTO. Tentativa B aprovada → pedido PAGO. A mensagem repetida de A encontra seu próprio registro já concluído e não aplica nova transição. Trocar o ID da solicitação dentro de um resultado não é repetição válida.
Explique o sistema sem decorar anotações [27]
Antes de revelar: a fila ficou vazia. Qual evidência fecha a história?
Escolha uma previsão. Depois confira a explicação.
- Quem guarda o trabalho quando Pagamentos está desligado — e quem não guarda?
- Qual a diferença entre 202, publisher confirm e consumer ack?
- Por que escolhemos “pelo menos uma vez”, e o que isso exige do consumidor?
- Por que usar a mesma chave ao reenviar, e o que uma chave nova significa?
- O que ainda não resolvemos entre banco e publicação?
🏠 Em casa
Consolide o que você consegue demonstrar
Traga uma prova e uma dúvida [28]
Reproduza o fluxo a partir de uma pasta limpa usando o guia de execução. Registre aprovação, recusa e duplicidade após reinício. Termine o desafio do ponto 26. Escolha uma falha, peça um diagnóstico ao agente (prompt 9) e confira se ele preservou a evidência.
Entregue o link do seu repositório (branch main, com um commit por etapa), sua spec, os testes e uma explicação curta: “Meu pagamento estava pendente porque… A evidência foi… A recuperação foi…” Não basta uma captura de “BUILD SUCCESS”.
Para rever a teoria, percorra de novo os seis fluxos animados e tente narrar cada passo antes de clicar em avançar. Se consegue explicar a janela A e a janela B do ponto 23 sem olhar, você entendeu o essencial do dia.
O aprofundamento de OO/SOLID começa a partir do Cap 03. Mensageria volta adiante com recuperação, outbox, políticas de erro e outras decisões. O capítulo anterior de OO/SOLID foi preservado como material de referência para reorganização.
Referências para consulta
- RabbitMQ: conceitos do modelo AMQP (exchanges, filas, bindings)
- RabbitMQ: filas de trabalho com Spring AMQP
- RabbitMQ: publisher confirms e consumer acknowledgements
- RabbitMQ: confiabilidade e reentrega
- RabbitMQ: dead letter exchanges
- Spring AMQP: publicação e retornos
- Insomnia: importar e exportar collections
Glossário de bolso [29]
Os termos do dia, na ordem em que aparecem.
- Síncrono
- Quem chama espera a resposta daquela chamada antes de continuar.
- Assíncrono
- Quem pede entrega a solicitação a um intermediário e recebe o resultado depois, por outro caminho.
- Acoplamento temporal
- Dois programas precisam estar vivos ao mesmo tempo para o trabalho acontecer.
- 202 Accepted
- Código HTTP: “aceito para processamento, ainda não terminei”.
- Disponibilidade
- Fração do tempo em que um serviço responde corretamente. Em cadeia síncrona, as disponibilidades se multiplicam.
- Consistência eventual
- Os dados ficam corretos “um pouco depois”, não no mesmo instante da solicitação.
- Mensagem
- Propriedades (envelope) + corpo (carta, aqui um JSON).
- Payload
- O corpo da mensagem, como o painel chama.
- Serializar
- Transformar um objeto em bytes para viajar; desserializar é o caminho de volta.
- Comando
- Mensagem que pede uma ação a um destinatário que sabe executá-la. Pode ser recusado.
- Evento
- Mensagem que informa um fato já ocorrido, para quem se interessar.
- Broker
- Programa que recebe, guarda e entrega mensagens. Aqui, o RabbitMQ.
- AMQP
- Protocolo de mensagens usado entre aplicações e o RabbitMQ (porta 5672).
- Virtual host (vhost)
- Espaço separado dentro do broker, com filas e permissões próprias. Usamos o padrão,
/. - localhost / host
- O seu próprio computador, visto por ele mesmo; “host” é o lado de fora dos containers.
- Exchange
- Ponto de roteamento do broker. Copia mensagens para filas; não guarda.
- Routing key
- Chave de roteamento enviada com a publicação.
- Binding
- Regra que liga uma exchange a uma fila, com uma chave ou padrão.
- Direct / fanout / topic
- Tipos de exchange: chave igual / todas as filas / padrões com * e #.
- Fila
- Onde as mensagens aguardam entrega, em ordem de chegada.
- Ready / Unacked
- Disponível para entrega / entregue mas ainda sem confirmação.
- Ack
- Confirmação do consumidor: “terminei, pode apagar”.
- Nack / reject
- Resposta do consumidor: “não consegui processar”.
- Requeue
- Recolocar na fila. Com requeue a mensagem volta para Ready; sem ele, sai da fila (e vai para a DLQ, se houver).
- Prefetch
- Quantas mensagens sem ack um consumidor pode segurar de uma vez.
- Consumidores concorrentes
- Várias instâncias na mesma fila; cada mensagem vai para uma só.
- Redelivered
- Marca de mensagem que está sendo entregue de novo.
- Durável / persistente
- Fila que sobrevive ao reinício do broker (o oposto é Transient) / mensagem gravada em disco.
- Publisher confirm
- Confirmação do broker ao produtor: “assumi esta publicação”.
- Mandatory
- Opção de publicação: se não houver rota, devolva a mensagem ao produtor.
- At-least-once
- Garantia “pelo menos uma vez”: nunca perde, pode repetir.
- Idempotência
- Repetir a operação preserva o mesmo efeito.
- Chave de idempotência
- Identificador estável de uma tentativa (a solicitacaoId).
- DLX
- Dead letter exchange: para onde uma fila manda as mensagens rejeitadas sem requeue.
- Fila de erros (DLQ)
- Destino de mensagens que falharam além do limite de tentativas.
- Escrita dupla
- Gravar no banco e publicar no broker sem uma transação comum; deixa uma janela de falha.
- Outbox
- Padrão que grava a mensagem numa tabela na mesma transação e publica depois. Não implementado hoje.