API REST de consulta de CEP com cache-aside (PostgreSQL → ViaCEP), resiliência com Polly + Refit, erros RFC 7807 e testes de integração com Testcontainers. A demo mostra o contrato público; a faixa de observabilidade mostra o cache-aside acontecendo de verdade no log.
MyCEP.API — .NET 8 — demo · 0:43narrado
// o que esta demonstração mostra
✓Um endpoint, um contrato público: schema de sucesso e de erro declarados no OpenAPI.
✓Endereço normalizado com código IBGE, SIAFI e DDD; cache público de 24h e headers de segurança por padrão.
✓Cache-aside no log: consulta começa no banco, sai para o ViaCEP no miss e grava antes de responder.
✓Resiliência visível: a chamada externa passa por pipeline de retry com tempo de execução medido.
Problema, arquitetura, decisões e o que foi medido — tudo rastreável às fontes listadas no fim. Sem números inventados.
01Problema
Dado um CEP, devolver o endereço completo numa API pública — dependendo de um provedor externo gratuito e sem SLA (ViaCEP), exposta a abuso e com custo por requisição que precisa ficar previsível. Construída como estudo de práticas de produção, não como CRUD de exemplo.
02Contexto
A demo roda no Fly.io (scale-to-zero) com PostgreSQL serverless no Neon; a pasta infra/terraform mantém a stack AWS completa (VPC, EC2, RDS privado, IAM/OIDC, SSM) como referência de infraestrutura como código. CEP é dado imutável — o que abre espaço para cache em mais de uma camada.
03Arquitetura
Cliente → Caddy (:80, TLS-ready) → ASP.NET Core API (:8080). Fluxo de uma requisição: validação do CEP (8 dígitos) → busca no PostgreSQL → em miss, chamada resiliente ao ViaCEP (Refit + retry/backoff/circuit breaker/timeout) → persistência com índice único parcial → resposta. Erros seguem RFC 7807 (Problem Details) com o status correto (400 / 404 / 429 / 500).
diagrama SVG inline — sem biblioteca, herda o tema da página
Core/API
Controllers e middleware de erro (ProblemDetails).
Core/Features
Casos de uso e DTOs de resposta.
Core/Shared/Domain
Entidades de domínio e contratos (repositório, serviço).
Core/Shared/Infrastructure
EF Core 9 + Npgsql, gateway ViaCEP (Refit), IoC, migrations.
04Decisões e trade-offs
decisão 01
Cache-aside com o próprio PostgreSQL como cache persistente: primeiro o banco; em miss, busca no ViaCEP, persiste e passa a servir localmente nas próximas chamadas.
trade-off
Cada miss numa API pública faz fan-out para terceiro e escreve no banco — é o que justifica o rate limiting e o cache HTTP como camadas complementares.
evidência
README ("O que é" e "Decisões técnicas que valem destaque")
decisão 02
Concorrência resolvida no banco: índice único parcial (WHERE deleted_at IS NULL) em zipcode + tratamento de conflito — duas requisições simultâneas do mesmo CEP geram uma linha, não duplicatas.
em vez de
Coordenação na aplicação (verificar-e-inserir sem garantia do banco).
trade-off
O conflito de unicidade passa a ser caminho esperado do código, não exceção — e precisa de teste de concorrência real para provar.
evidência
README ("Concorrência") · teste de integração de corrida de inserção com Testcontainers
decisão 03
Chamada ao ViaCEP via Refit com AddStandardResilienceHandler (Microsoft.Extensions.Http.Resilience / Polly): retry + backoff + circuit breaker + timeout — porque o ViaCEP é gratuito e sem SLA.
trade-off
Resiliência tem preço em latência no caminho de falha (retries e backoff antes de desistir); o cenário "ViaCEP fora do ar" é coberto por teste de integração.
evidência
README ("Resiliência" e "Testes") · clipe mycep-obs (pipeline de retry com tempo medido)
decisão 04
Contrato de erro único: ProblemDetails (RFC 7807) em toda a superfície, com o schema de sucesso e de erro declarados no OpenAPI; em 5xx o detalhe interno fica só no log, nunca no corpo.
trade-off
O cliente recebe menos detalhe em 5xx — por design, para não vazar internals.
evidência
README ("Contrato de erro") · clipe mycep-demo
decisão 05
Rate limiting nativo do ASP.NET Core (60 req/min/IP) numa API pública anônima que escreve por miss e chama terceiro.
trade-off
Teto fixo por IP (sem distinção de cliente — a API não tem autenticação); o 429 faz parte do contrato RFC 7807.
evidência
README ("Abuso / custo" e "Endpoints")
decisão 06
Cache-Control: public, max-age=86400 na resposta — CEP é imutável; CDN e browser absorvem a maioria dos round-trips.
trade-off
Uma eventual correção de endereço leva até 24 h para chegar a clientes com a resposta em cache.
evidência
README ("Cache HTTP") · clipe mycep-demo (cache público de 24h)
decisão 07
Testes de integração com PostgreSQL real via Testcontainers, não in-memory — incluindo a corrida de inserção e o provedor fora do ar.
em vez de
Provider in-memory do EF Core.
trade-off
Exigem Docker rodando; por isso a suíte unitária é separada e roda rápido sem Docker (dotnet test test/MyCEP.API.UnitTest).
evidência
README ("Testes")
decisão 08
Infraestrutura como código com deploy via OIDC (sem access keys) e senha do banco gerada e guardada no SSM Parameter Store; o ambiente publicado, porém, roda no Fly.io + Neon.
trade-off
A stack AWS é referência validada, não o ambiente que atende a demo — e o README declara isso.
evidência
README ("Infra como código" e nota de deploy)
05Implementação
›Aceita 01001000 ou 01001-000; resposta normalizada com ibgeCode, areaCode (DDD) e siafiCode.
›Headers de segurança por padrão e cache público de 24 h visíveis na resposta (faixa demo).
›Health checks: /health (liveness + readiness, valida o PostgreSQL) e /health/live (não toca o banco).
›Contêiner multi-stage Alpine non-root; Caddy como proxy TLS-ready; docker compose up --build sobe API + PostgreSQL e aplica migrations.
›CI/CD: testes como gate antes do deploy; scans de dependências (dotnet list --vulnerable), imagem (Trivy) e segredos (gitleaks); Dependabot.
06Testes
xUnit + Moq + FluentAssertions + Testcontainers, em duas suítes: unitária (casos de uso e regras de domínio) e integração (API ponta a ponta com PostgreSQL real).
›Integração com PostgreSQL efêmero via Testcontainers — não in-memory.
›Corrida de inserção do mesmo CEP → uma única linha (índice único parcial + tratamento de conflito).
›ViaCEP fora do ar → caminho de falha resiliente coberto.
›Cobertura com quality gate (run-coverage.ps1) no workflow test-coverage.
07Observabilidade
O cache-aside é verificável no log, não só no diagrama — é o que a faixa de observabilidade mostra.
›A consulta começa no PostgreSQL, sai para o ViaCEP no miss — via Refit, com pipeline de retry do Polly e tempo de execução medido — e grava o resultado antes de responder.
›Em 5xx, o detalhe interno vai para o log; o cliente recebe ProblemDetails.
›Health checks separados para liveness e readiness (dependência do banco explícita).
08Resultados e aprendizados
✓Duas requisições simultâneas do mesmo CEP geram uma linha — provado por teste de concorrência contra PostgreSQL real.
✓Provedor externo sem SLA tratado como risco de primeira classe: retry, backoff, circuit breaker, timeout e teste do caminho de falha.
✓Contrato público único de sucesso e erro (OpenAPI + RFC 7807), incluindo 429 do rate limiting.
✓Aprendizado: cache em camadas (banco como cache persistente + Cache-Control de 24 h) e proteção contra abuso são decisões de custo, não só de performance.
09Fontes
Tudo o que está escrito acima é rastreável a estes arquivos e repositórios.