API de controle de assinaturas
Uma API orientada a eventos para planos e assinaturas com cobrança recorrente. Projetei o fluxo assíncrono com RabbitMQ, a segurança do webhook e um painel próprio, com 319 testes e 100% de cobertura exigidos no CI.
- Papel
- Desenvolvedor
- Contexto
- Projeto pessoal
- Stack
- Java 21, Spring Boot, PostgreSQL, RabbitMQ, Flyway, Docker, GitHub Actions
- Time
- Solo
- Período
- 2026
- Testes
- 319
- Cobertura
- 100%

01. Problema
A cobrança chega depois, e o sistema precisa reagir a ela
Um sistema de assinaturas parece um CRUD, mas o ponto difícil está fora dele: quem confirma o pagamento é um provedor externo, que avisa por webhook quando quiser. Até lá, a assinatura precisa estar num estado coerente, e quando o aviso chega, ele precisa ser aplicado uma única vez, mesmo que chegue repetido.
Construí uma API que cuida de planos, assinaturas e todo o ciclo de vida delas: criação, ativação, suspensão por falha de pagamento, renovação, upgrade, downgrade e cancelamento, além de uma trilha de eventos e um relatório de métricas.
02. Arquitetura
Nenhuma cobrança é aplicada dentro da requisição
Decidi que as atualizações de cobrança não acontecem na requisição. Cada ação grava um evento e o publica num exchange do RabbitMQ; dois workers consomem as filas e são eles que mudam a assinatura. A API responde rápido, e o processamento pode falhar e tentar de novo sem o cliente perceber.
- Requisição validada
- Evento gravado e publicado
- Fila
- Worker atualiza a assinatura
- Evento marcado como processado
- falha depois de 3 tentativas: mensagem vai para a dead letter queue
No webhook de pagamento, a API valida o payload, a assinatura e o valor pago, grava o evento e responde 202 Accepted. Nada é alterado ali. O worker é quem ativa, suspende ou renova a assinatura, e a data da próxima cobrança só avança quando o pagamento quita o ciclo vencido. Assim, confirmações repetidas não acumulam meses. O processamento é idempotente: um evento já processado que volta do broker não muda nada.
03. Segurança
Cada rota tem a credencial que faz sentido para ela
A API não usa sessão nem cookie. Defini uma credencial por tipo de rota:
API
Chave de API, nunca em texto puro
A aplicação guarda só o digest SHA-256 de cada chave e compara em tempo constante, para que o tempo de resposta não revele quanto de uma chave errada bateu.
Webhook
Assinatura HMAC e bloqueio de reenvio
O provedor assina o corpo exato com um segredo compartilhado, como fazem os provedores reais. Recuso assinaturas fora de uma janela de 5 minutos e entregas repetidas.
Abuso
Limite de requisições por origem
120 por minuto na API e 60 no webhook, com os cabeçalhos para o cliente se regular. Deixei o health check fora do limite para o orquestrador não reiniciar um processo saudável.
Produção
Sem credencial, a aplicação não sobe
Em desenvolvimento, a chave e o segredo são gerados e mostrados no log. Em produção, a aplicação recusa subir sem eles, e o Swagger e os detalhes do actuator são desligados.
04. Decisões
Cada limite aceito ficou escrito
Algumas escolhas tinham custo, e eu preferi assumi-las de forma explícita a esconder.
Trade-off
Publicar na transação, sem outbox
Se o broker cai, a publicação falha e desfaz a transação inteira: nunca fica evento gravado sem mensagem. Os casos que sobram terminam na DLQ sem alterar dados. Eliminar tudo exigiria um outbox transacional, que deixei registrado como próximo passo.
Domínio
Cancelar não é excluir
Cancelar encerra o ciclo de vida e mantém a assinatura nas métricas. Excluir é um soft delete que tira o registro das listagens. Nenhum dado é apagado fisicamente.
Testabilidade
Relógio injetado
Nenhuma regra chama a data atual direto. Tudo depende de um relógio injetado, com o fuso de negócio configurável, o que torna o cálculo de datas determinístico nos testes.
Contrato
Um formato de erro para tudo
Todo erro sai em problem+json (RFC 7807), com título, detalhe e a lista de campos inválidos. O painel mostra exatamente o que a API devolve.
Para operar a API sem Postman, criei um painel em HTML, CSS e JavaScript puros, servido pela própria aplicação. Ele assina as chamadas ao webhook no navegador e mostra a assinatura antes e depois do processamento, deixando visível que quem a alterou foi o worker.
05. Qualidade
319 testes, e cobertura total como regra do CI
Desenvolvi com TDD, feature por feature. Os testes rodam sobre H2 em memória, com relógio fixo e sem broker, então não dependem de nada externo. O verify falha se a cobertura de linhas ou de branches ficar abaixo de 100%, e o GitHub Actions roda esse gate a cada push, seguido do build da imagem Docker.
- testes automatizados
- 319
- de linhas e branches cobertos
- 100%
- mais código de teste que de produção
- 1,8×
Cobertura total não substitui o teste de verdade: ao rodar contra PostgreSQL e RabbitMQ reais, encontrei e corrigi dois defeitos que a suíte em memória não pegava.
06. Resultado
Uma API completa, com painel próprio, que sobe com um comando
Com docker compose --profile app up --build, sobem o PostgreSQL, o RabbitMQ e a aplicação, com o schema criado pelo Flyway. A API está documentada no Swagger e no README, com arquitetura, fluxos de evento, contrato JSON e endpoints.
07. Próximos passos
O que eu faria para escalar
Um outbox transacional, para eliminar as mensagens publicadas antes do commit, e um limite de requisições global com armazenamento compartilhado, como Redis, para quando a aplicação rodar com várias réplicas.