Pular para o conteúdo
caua.tolentino
Todos os projetos
Back-End

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%
Painel com as métricas de assinaturas por status e por plano

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.

  1. Requisição validada
  2. Evento gravado e publicado
  3. Fila
  4. Worker atualiza a assinatura
  5. Evento marcado como processado
  6. 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.