Basis Spring Application
Padrões da Basis para apps Spring. Pareada com basis-k8s-deploy (esta documenta o lado dev; aquela documenta o lado ops/infra), com basis-java-code-standards (como o código Java é escrito dentro dela) e, quando a app serve mais de um cliente no mesmo banco, com basis-multi-tenant (isolamento por tenant_id e Row Level Security). Quando a app fala com o SGO — criar ocorrência, ler createmeta, escrever TableGrid, subir anexo —, o cliente e as armadilhas do Jira 6.3.13 estão em basis-sgo-jira.
1. Stack default (sempre, sem exceção)
- Java: LTS mais recente (em 04/2026: 25)
- Build: Maven (nunca Gradle — não há benefício para nossos casos)
- Spring: estável mais recente (em 07/2026: Spring 4.1)
- Modulith: versão pareada com Spring (em 07/2026: 2.1)
- Banco: PostgreSQL 17
- Migrações: Flyway (sem exceção — todo sistema TEM ferramenta de migração)
- Persistência: Spring Data JDBC > JPA para novos projetos
- Mensageria: RabbitMQ via Spring Cloud Stream (SCS)
- Web (quando aplicável): Thymeleaf + HTMX + Tailwind + DaisyUI; alternativamente Angular
- Auth (quando autenticada): Keycloak (realm
basis) via OIDC, registration sempre keycloak, autorização por client roles
- Observability: Spring Boot Actuator (obrigatório — health probes pra K8s)
- Dev local: Docker Compose com Postgres + RabbitMQ + Keycloak (quando autenticada), profile
dev pronto pra rodar sem editar config
2. Esqueleto de projeto
- Multi-módulo:
<app>-domain, <app>-core quando aplicável
- Parent pom + Modulith BOM
- Plugins essenciais:
spring-boot-maven-plugin
jib-maven-plugin — para gerar imagem com tag CalVer (passada via property)
frontend-maven-plugin — quando há build de frontend (Tailwind, etc.)
3. Configuração — princípio: usar TUDO que o Spring oferece
Hierarquia
application.yml: defaults env-agnostic, conteúdo válido pra qualquer ambiente
application-{profile}.yml: SÓ os deltas do profile
- Os profiles de ambiente são dois:
dev e prod. Não existe staging. Staging e produção
rodam o mesmo profile prod; o que difere entre os dois é o overlay do kustomize — env
vars, secrets, réplicas, host do ingress —, nunca um arquivo de configuração da aplicação.
Medido nos 8 deployments Spring do iac/argocd-apps: todos declaram SPRING_PROFILES_ACTIVE
em base/ com prod (às vezes prod,kubernetes ou prod,api-docs,kubernetes), e nenhum
overlay o sobrepõe. Em 5 repositórios de aplicação não existe um único application-staging.*
- Além desses, só profiles de recurso, não de ambiente:
test, tls, sgo, spike. O
critério é "liga um pedaço", não "é um ambiente"
- Env vars: sobrepõem qualquer yml via Spring relaxed binding
@ConfigurationProperties: tipa e valida o config no código
Convenção de prefixo (Basis)
- Sempre
application.* para ConfigurationProperties da aplicação (não o nome da app)
- Env var equivalente:
APPLICATION_*
- Ex:
application.opnsense.base-url ← APPLICATION_OPNSENSE_BASE_URL
- Vantagem: carga cognitiva menor, mesmo padrão em qualquer projeto
Anti-padrões
key: ${ENV_VAR} no yml quando o nome do env var pode seguir relaxed binding — deixa o yml limpo, Spring resolve sozinho
- Redeclarar defaults Spring (ex:
spring.rabbitmq.port: 5672) — omitir
- Criar env var custom (
RABBITMQ_HOST) quando o oficial cobre (SPRING_RABBITMQ_HOST)
- Misturar dev/prod no mesmo arquivo
- Chave aninhada sob o pai errado. O Spring liga por caminho completo: um
spring: indentado dentro de server: vira propriedade desconhecida de server e é ignorado sem aviso — sem erro, sem log, sem falha de startup. Num app real, datasource, flyway, modulith e mail viveram meses sob server: e nunca foram aplicados; a app subia com os defaults e ninguém percebia. Config nova se confere pelo efeito (/actuator/env, o log do Flyway, a URL que apareceu no banner), nunca pela presença da linha no arquivo
- Bloco de config de tecnologia que o projeto não usa (
spring.jpa num projeto Data JDBC): não quebra e engana quem lê depois
@ConfigurationProperties
record immutable + @EnableConfigurationProperties(MyProps.class) no @Configuration
- Validações via Bean Validation quando o campo é obrigatório
4. Banco e migrações
Transações — readOnly por default na classe
@Transactional(readOnly = true) na classe de serviço, e @Transactional explícito só nos métodos que escrevem. O ganho não é evitar digitação: é inverter o default para o lado seguro. Sem isso, método de leitura sem anotação nenhuma roda fora de transação e ninguém percebe — não dá erro, não aparece em log, e a consequência só existe em cenário que o teste unitário não cobre.
@Service
@Transactional(readOnly = true)
public class FuncionarioService {
public List<FuncionarioDTO> listar() { ... } // herda readOnly
@Transactional
public FuncionarioDTO criar(NovoFuncionario cmd) { ... } // override explícito
}
O mecanismo que faz disso um guarda-corpo, e não documentação: a anotação mais interna vence, então esquecer o override num método de escrita produz falha, não corrupção. Com routing de datasource, a escrita vai parar na réplica e estoura; sem routing, o Spring propaga o flag via Connection.setReadOnly(true) e o driver do Postgres marca a transação como read-only, então o INSERT falha com cannot execute INSERT in a read-only transaction. Nos dois casos o erro aparece no primeiro teste que exercitar o método — que é o momento certo.
readOnly = true é pré-requisito para roteamento primário/réplica via AbstractRoutingDataSource, que decide pelo TransactionSynchronizationManager. Adotar o padrão agora deixa a porta aberta mesmo em app que hoje tem um banco só
- Em Spring Data JDBC não há dirty checking para pular, então o ganho é o guarda-corpo mais o roteamento — o argumento de performance do mundo Hibernate não se aplica aqui
- Não anote a classe inteira com
@Transactional sem readOnly para "resolver o esquecimento": isso não é default seguro, é escrita liberada em todo método
- Confirme o comportamento no projeto com um teste que tente escrever num método sem override. Se ele passar, o flag não está chegando na conexão e o guarda-corpo não existe
- Referência: Read-write and read-only transaction routing with Spring, Vlad Mihalcea
5. Mensageria assíncrona com SCS
Bindings funcionais
<name>-in-0 / <name>-out-0 (Spring Cloud Function naming)
function.definition: name1;name2;... agrega os consumers
Topologia: ESCOLHER UM dono — não misturar
- K8s dono (recomendado em prod): topologia via CRDs RabbitMQ
- SCS config:
default.consumer.bind-queue: false, auto-bind-dlq: false
- Main queue tem
x-dead-letter-exchange: DLX + x-dead-letter-routing-key: <destination> definidos no CRD
- SCS dono (dev): SCS cria filas/DLQ automaticamente
- SCS config:
default.consumer.bind-queue: true, auto-bind-dlq: true
- Default DLQ name =
<queue>.dlq, default routing key = destination
6. Web stack (quando aplicável)
O padrão de UI (layout com sidebar, tabelas com cabeçalho/rodapé fixos, formulários, página de erro, tema caramellatte, build de CSS/JS) está na skill basis-web-frontend. Resumo do que a app Spring precisa saber:
- Thymeleaf em
src/main/resources/templates/, layout único em layout.html (th:fragment="layout(content, activeMenu)")
- Fragmentos HTMX servidos por Controller dedicado, devolvendo só o trecho (
~{::fragmento})
- Tema DaisyUI
caramellatte; input.css em src/frontend/ (fora do classpath), output gerado em target/classes/static/
frontend-maven-plugin na fase generate-resources roda npm run build
.gitignore: src/main/resources/static/* exceto static/images/ (assets versionáveis)
templates/error.html obrigatório — sem ele a app cai na Whitelabel Error Page do Spring Boot; server.error.whitelabel.enabled: false e include-stacktrace: never
@ControllerAdvice com @ModelAttribute para appVersion (de BuildProperties) e currentUser (do OidcUser), que o layout consome em toda página
Tratamento de exceção em app que serve HTML
- Não use
@RestControllerAdvice numa app que serve HTML. Ele é @ControllerAdvice + @ResponseBody: o retorno do handler é serializado como corpo da resposta, não resolvido como view. Numa app HTMX, uma exceção de negócio passa a injetar JSON dentro do DOM, no lugar onde o fragmento deveria entrar — a tela mostra {"timestamp":...,"message":...} em vez da mensagem de erro. Em app mista, separe: @RestControllerAdvice com @ControllerAdvice(basePackages = ...) limitado aos pacotes de API, e um @ControllerAdvice de view para o resto
- Prefira exceção própria mapeada no handler global a anotação por endpoint com lista de exceções. Anotação de marcação com opt-out (
@ExigeContexto em quase tudo, mais uma lista dos que não exigem) cria uma segunda lista de endpoints especiais ao lado do permitAll do SecurityConfig — duas fontes de verdade sobre a mesma pergunta, que divergem na primeira vez que alguém mexe só numa. Uma exceção lançada onde o contexto falta, mapeada uma vez no handler global, cobre todos os casos sem lista nenhuma
- Se a separação precisar mesmo ser explícita, torne-a estrutural: um prefixo de path (
/api/publico/**) é visível no SecurityConfig, no log e no roteador, em vez de estar espalhado em anotação
7. Autenticação (Keycloak OIDC)
Apps web autenticadas usam Keycloak no realm basis, fluxo OIDC. Padrões:
- Spring Security registration sempre chamada
keycloak — login fica em /oauth2/authorization/keycloak
provider.keycloak.issuer-uri: https://sso.apps.basis.com.br/realms/basis
- Autorização por client roles (não realm roles), prefixadas com nome curto da app:
<APP>_USER, <APP>_ADMIN
- Custom
OidcUserService lê resource_access.<clientId>.roles e mapeia para GrantedAuthority com prefixo ROLE_
SecurityConstants agrupa as roles em uma classe utility (evita typos espalhados)
/actuator/health/** + estáticos sempre permitAll() (probes K8s precisam acesso anônimo)
Ver references/keycloak-oidc.md — config completa, SecurityConfig template, setup do client no Keycloak.
Para Keycloak local (compose + import do realm basis), ver §10 e references/local-dev-compose.md.
8. Observability (Actuator)
Obrigatório. Sem spring-boot-starter-actuator no pom, as probes K8s caem em 404 e o pod entra em crash loop silencioso.
- Endpoints essenciais:
/actuator/health, /actuator/health/liveness, /actuator/health/readiness
- Liveness ≠ readiness: liveness só pode falhar se a JVM/Spring quebrar; readiness inclui dependências externas (DB, Rabbit, LDAP)
- Métricas Prometheus opcional via
micrometer-registry-prometheus + management.endpoints.web.exposure.include
Ver references/actuator.md — config completa, integração com SecurityConfig e probes K8s.
9. Logging e depuração
Convenções gerais de log (SLF4J, {}, níveis, nada sensível) estão em basis-java-code-standards §9. O que é específico de app Spring:
Banner de startup — obrigatório
- Após
app.run(...), logar bloco com nome da aplicação, URL local, URL externa, context path e profiles ativos
- Responde na hora "qual app, em que porta, com que profile" sem acesso ao pod
- Ler
server.port e profiles do Environment (não do yml) — reflete a porta efetiva e o profile realmente ativo
- Nada de segredo no banner
DEBUG nos pontos de entrada — obrigatório
- Todo ponto onde estímulo externo entra loga em
DEBUG o que chegou: Controller, consumer SCS, @Scheduled, listener de evento Modulith, webhook
- Identificador e campos que direcionam o fluxo — não o payload inteiro (payload em
TRACE), nunca PII/segredo
- Consumer loga também o desfecho (processado/rejeitado + motivo) — mensagem sem contraparte no log é o sintoma de consumer travado
DEBUG em processamento complexo
- Método com várias etapas, chamada externa ou regra densa: um
DEBUG no início (entrada) e um no fim (resultado, e duração quando útil), com o mesmo identificador nas duas linhas
- Critério: se falhar em produção, o log de erro sozinho diz com que entrada e até onde foi? Se não, precisa do par
- Duração em
DEBUG é diagnóstico; o que precisa de acompanhamento contínuo vai para Micrometer/Actuator
Níveis
application.yml: br.com.basis.<app>: INFO; application-dev.yml: DEBUG
- Em prod sobe para
DEBUG sem redeploy via LOGGING_LEVEL_* ou /actuator/loggers — é para isso que os DEBUG existem no código
DEBUG/TRACE de pacote de terceiro é investigação pontual, não config commitada (org.springframework.jdbc.core: TRACE loga valores de parâmetro de SQL)
Ver references/logging-e-banner.md — código do banner, exemplos por tipo de entry point, guard isDebugEnabled.
10. Ambiente de desenvolvimento local
Meta: git clone → docker compose up -d → mvn spring-boot:run -Dspring-boot.run.profiles=dev, sem editar arquivo de configuração. compose.yaml e application-dev.yml são escritos como par — os valores do yml são os defaults declarados no compose.
compose.yaml na raiz: Postgres, RabbitMQ, Keycloak (quando autenticada), + o que a app usar (MinIO, GreenMail, Ollama). Portas sempre em 127.0.0.1, healthcheck em todo serviço
Keycloak usa o Postgres da própria aplicação, em banco/role separados (keycloak/keycloak) criados via docker/postgres/init.sql. Motivo principal: permite rodar comandos de administração no container com o Keycloak no ar — em especial kc.sh export do realm, que com o dev-file (H2) esbarra no lock do arquivo. De quebra: um container de banco só, estado sobrevivendo ao down, e mesma topologia de prod
Realm basis importado de ./docker/keycloak via --import-realm; partir de references/basis-realm-dev.json (client <app>-admin, client roles <APP>_USER/<APP>_ADMIN, usuários admin/admin e user/user)
Export de realm nunca vai cru para o repo — carrega chave RSA privada, segredos HMAC/AES, client secret e hashes de senha
application-dev.yml traz datasource apontando para o compose (localhost:5432, usuário/senha = nome da app) e o issuer local (http://localhost:9080/realms/basis)
RabbitMQ não aparece no application-dev.yml: os defaults do Spring (localhost:5672, guest/guest, vhost /) já batem com o container. Vhost dedicado e credencial real só em prod, via env var
Em dev o SCS é dono da topologia (declare-exchange/bind-queue/auto-bind-dlq: true); em prod os CRDs do operator são
Credencial de seed documentada no README. Senha de usuário de teste que só existe como hash na migration é senha que ninguém consegue enunciar — e alguém entrando no projeto vai tentar adivinhá-la. Gere o hash a partir de uma senha declarada (admin123, user123), com o encoder do próprio projeto, e escreva qual é
Anti-padrões
application-dev.yml apontando para infra compartilhada/remota com credencial real — vira segredo versionado, quebra o "clone e roda" e um dev derruba o ambiente do outro
- URL/credencial comentada com a alternativa "de verdade" logo abaixo — o profile deixa de ter config válida
- Senha forte em dev (a porta só escuta em
127.0.0.1); <app>/<app>, guest/guest, admin/admin são melhores por serem obviamente descartáveis
spring-boot-docker-compose no classpath. O starter publica um JdbcConnectionDetails a partir do compose.yaml, e esse bean tem precedência sobre spring.datasource.* — a app conecta onde o starter mandou, não onde o yml diz. Consequências: erro de configuração do datasource fica mascarado (o yml quebrado "funciona"), e não há como apontar a app para uma role diferente da do compose, o que impede o desenho de RLS da basis-multi-tenant. O fluxo desta skill (§10) não depende dele — docker compose up -d explícito é mais previsível
- Deletar migration sem
mvn clean. As antigas continuam em target/classes/db/migration e o Flyway as aplica de lá. Rebaseline em dev é ./mvnw clean junto com docker compose down -v; sem os dois, o banco reconstruído não é o que o repositório descreve
Ver references/local-dev-compose.md — compose completo, init.sql, application-dev.yml, comando de export do realm.
11. Testes
- Unit: Mockito quando faz sentido (controllers, services puros)
- Integração: Testcontainers pra Postgres/RabbitMQ — não mockar infra que sobe local em segundos
- APIs externas opacas (AD/LDAP, Mailcow, Secullum, OPNsense): interface + impl real, teste manual contra infra; mocks só pros happy paths em controller tests
- Suíte só de unitário com repositório mockado não cobre mapeamento objeto-relacional. Trezentos testes verdes não dizem nada sobre
@Id composto, @Embedded, conversor custom ou RLS — e é exatamente aí que uma refatoração de persistência quebra. Antes de mexer em id, herança de entidade ou policy de banco, o pré-requisito é ter pelo menos um IT que salve, busque e liste contra Postgres real
- Não fixe versão que o parent do Spring Boot já gerencia. Versão presa envelhece sozinha e o sintoma não aponta para ela:
failsafe preso em 3.1.2 com o surefire em 3.5.6 falha por provider ausente no repositório local; testcontainers-bom preso em 1.18.3 (2023) falha contra engine Docker atual com erro de API. Nos dois, a correção é remover a versão, não escolher outra
*IT.java fora de pacote de produção. Os testes ArchUnit varrem os pacotes de domínio por reflexão; um IT deixado ali é encontrado e dispara o container durante a suíte unitária, que passa a depender de Docker. O IT vai para pacote próprio
- Antes de escrever o primeiro IT, confira o pom:
failsafe costuma já estar configurado, incluindo **/*IT.java, e nunca ter rodado por não existir nenhum
Estrutura (Modulith) — obrigatório quando a app usa Modulith
ApplicationModules.of(<App>Application.class).verify() num teste — pega acesso a pacote internal de outro módulo, ciclo entre módulos e dependência não declarada
- Sem esse teste, "módulo" é só convenção de pasta: o build aceita qualquer
import entre pacotes internos e a modularidade se dissolve sem sinal nenhum
Documenter no mesmo teste gera os diagramas em target/modulith-docs — documentação que não desatualiza
- Fronteiras críticas do domínio declaradas explicitamente, principalmente as dependências que não podem existir (
containsModuleNamed(...) com isFalse()); alternativa declarativa: @ApplicationModule(allowedDependencies = ...) no package-info.java
@ApplicationModuleTest + Scenario para integração por módulo e para testar evento sem Thread.sleep
Ver references/modulith-tests.md — teste base, fronteiras de domínio, @ApplicationModuleTest.
12. Interface com basis-k8s-deploy
Env vars que o app espera
SPRING_PROFILES_ACTIVE=prod (ativa application-prod.yml)
SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD
SPRING_RABBITMQ_HOST, SPRING_RABBITMQ_USERNAME, SPRING_RABBITMQ_PASSWORD, SPRING_RABBITMQ_VIRTUAL_HOST
APPLICATION_<CONFIGITEM>_* para tudo que está em application.* no yml
O que documentar pro deploy
- Quando criar uma nova ConfigurationProperties section, listar os env vars equivalentes em
references/env-vars.md
- Profile
prod deve assumir que infra-secrets vêm de env (não tem default)
References disponíveis
references/configuration-properties.md — Convenção application.* + APPLICATION_*, hierarquia yml/env/profiles, @ConfigurationProperties records, anti-padrões
references/scs-rabbitmq.md — SCS functional bindings, K8s-dono vs SCS-dono, retry e DLQ, inspeção/retry manual
references/keycloak-oidc.md — OIDC com registration keycloak, client roles, SecurityConfig template
references/actuator.md — Health probes (liveness/readiness), integração com K8s, métricas Prometheus
references/pom-skeleton.md — pom multi-módulo + plugins essenciais (jib, frontend-maven-plugin), parent BOM, módulos típicos
- Build de frontend, layout, tabelas, formulários e páginas de erro: skill
basis-web-frontend
references/flyway-modulith-notes.md — gotchas de schema com Modulith JDBC publisher, naming, baseline em legacy
references/logging-e-banner.md — banner de startup, DEBUG em entry points e processamento complexo, níveis por ambiente
references/local-dev-compose.md — compose.yaml, Keycloak no Postgres da app, init.sql, application-dev.yml espelhando o compose, export do realm
references/basis-realm-dev.json — realm basis mínimo para import em dev: client <app>-admin, client roles, usuários de teste
references/modulith-tests.md — ApplicationModules.verify(), Documenter, fronteiras de domínio, @ApplicationModuleTest
1---2name: basis-spring-app3description: Use when starting, extending, or refactoring a Basis Spring application — Java LTS + Maven + Spring (latest stable) + Spring Modulith, configuration via yaml + env vars + @ConfigurationProperties under the `application.*` prefix, Postgres + Flyway always, RabbitMQ via Spring Cloud Stream, web stack Thymeleaf + HTMX + Tailwind + DaisyUI, local dev via Docker Compose (Keycloak on the app's Postgres), startup banner and DEBUG logging at entry points, Modulith structure tests. Activate for new modules, configuration cleanup, async messaging wiring, local dev environment setup, or web/frontend build questions.4---56# Basis Spring Application78Padrões da Basis para apps Spring. Pareada com `basis-k8s-deploy` (esta documenta o lado dev; aquela documenta o lado ops/infra), com `basis-java-code-standards` (como o código Java é escrito dentro dela) e, quando a app serve mais de um cliente no mesmo banco, com `basis-multi-tenant` (isolamento por `tenant_id` e Row Level Security). Quando a app fala com o **SGO** — criar ocorrência, ler `createmeta`, escrever TableGrid, subir anexo —, o cliente e as armadilhas do Jira 6.3.13 estão em `basis-sgo-jira`.910## 1. Stack default (sempre, sem exceção)1112- **Java**: LTS mais recente (em 04/2026: 25)13- **Build**: Maven (nunca Gradle — não há benefício para nossos casos)14- **Spring**: estável mais recente (em 07/2026: Spring 4.1)15- **Modulith**: versão pareada com Spring (em 07/2026: 2.1)16- **Banco**: PostgreSQL 1717- **Migrações**: Flyway (sem exceção — todo sistema TEM ferramenta de migração)18- **Persistência**: Spring Data JDBC > JPA para novos projetos19- **Mensageria**: RabbitMQ via Spring Cloud Stream (SCS)20- **Web (quando aplicável)**: Thymeleaf + HTMX + Tailwind + DaisyUI; alternativamente Angular21- **Auth (quando autenticada)**: Keycloak (realm `basis`) via OIDC, registration sempre `keycloak`, autorização por client roles22- **Observability**: Spring Boot Actuator (obrigatório — health probes pra K8s)23- **Dev local**: Docker Compose com Postgres + RabbitMQ + Keycloak (quando autenticada), profile `dev` pronto pra rodar sem editar config2425## 2. Esqueleto de projeto2627- Multi-módulo: `<app>-domain`, `<app>-core` quando aplicável28- Parent pom + Modulith BOM29- Plugins essenciais:30 - `spring-boot-maven-plugin`31 - `jib-maven-plugin` — para gerar imagem com tag CalVer (passada via property)32 - `frontend-maven-plugin` — quando há build de frontend (Tailwind, etc.)3334## 3. Configuração — princípio: usar TUDO que o Spring oferece3536### Hierarquia37- `application.yml`: defaults env-agnostic, conteúdo válido pra qualquer ambiente38- `application-{profile}.yml`: SÓ os deltas do profile39- **Os profiles de ambiente são dois: `dev` e `prod`. Não existe `staging`.** Staging e produção40 rodam o **mesmo** profile `prod`; o que difere entre os dois é o overlay do kustomize — env41 vars, secrets, réplicas, host do ingress —, nunca um arquivo de configuração da aplicação.42 Medido nos 8 deployments Spring do `iac/argocd-apps`: todos declaram `SPRING_PROFILES_ACTIVE`43 em `base/` com `prod` (às vezes `prod,kubernetes` ou `prod,api-docs,kubernetes`), e nenhum44 overlay o sobrepõe. Em 5 repositórios de aplicação não existe um único `application-staging.*`45- Além desses, só profiles **de recurso**, não de ambiente: `test`, `tls`, `sgo`, `spike`. O46 critério é "liga um pedaço", não "é um ambiente"47- Env vars: sobrepõem qualquer yml via Spring relaxed binding48- `@ConfigurationProperties`: tipa e valida o config no código4950### Convenção de prefixo (Basis)51- Sempre `application.*` para ConfigurationProperties da aplicação (não o nome da app)52- Env var equivalente: `APPLICATION_*`53- Ex: `application.opnsense.base-url` ← `APPLICATION_OPNSENSE_BASE_URL`54- Vantagem: carga cognitiva menor, mesmo padrão em qualquer projeto5556### Anti-padrões57- `key: ${ENV_VAR}` no yml quando o nome do env var pode seguir relaxed binding — deixa o yml limpo, Spring resolve sozinho58- Redeclarar defaults Spring (ex: `spring.rabbitmq.port: 5672`) — omitir59- Criar env var custom (`RABBITMQ_HOST`) quando o oficial cobre (`SPRING_RABBITMQ_HOST`)60- Misturar dev/prod no mesmo arquivo61- **Chave aninhada sob o pai errado.** O Spring liga por caminho completo: um `spring:` indentado dentro de `server:` vira propriedade desconhecida de `server` e é **ignorado sem aviso** — sem erro, sem log, sem falha de startup. Num app real, `datasource`, `flyway`, `modulith` e `mail` viveram meses sob `server:` e nunca foram aplicados; a app subia com os defaults e ninguém percebia. Config nova se confere pelo efeito (`/actuator/env`, o log do Flyway, a URL que apareceu no banner), nunca pela presença da linha no arquivo62- **Bloco de config de tecnologia que o projeto não usa** (`spring.jpa` num projeto Data JDBC): não quebra e engana quem lê depois6364### `@ConfigurationProperties`65- `record` immutable + `@EnableConfigurationProperties(MyProps.class)` no `@Configuration`66- Validações via Bean Validation quando o campo é obrigatório6768## 4. Banco e migrações6970- Flyway location: `db/migration`71- Naming: `V<timestamp>__<descricao_snake>.sql` (timestamp `YYYYMMDDHHmm`)72- **O Flyway compara versão por partes, numericamente — não como texto.** É o que faz o esquema de timestamp conviver com numeração antiga: `V202608131000` vem depois de `V6` porque 202608131000 > 6, e não pela ordem alfabética, que colocaria `V6` por último. Também é por isso que `V1.10` vem depois de `V1.9`, ao contrário do que a ordenação do `ls` sugere73- **Flyway é a única fonte de verdade do schema.** Nenhum componente cria tabela em runtime — nem framework, nem `ddl-auto`, nem script de inicialização. Não é preferência: schema criado fora do Flyway não tem versão, não aparece em revisão de MR e não passa pelas regras que as migrations aplicam74- **Modulith + Flyway** — na criação da aplicação, não depois:75 ```yaml76 spring:77 modulith:78 events:79 jdbc:80 schema-initialization:81 enabled: false # a tabela event_publication vem do Flyway82 ```83 O publisher de eventos JDBC cria a tabela `event_publication` sozinho por default. Com os dois criando, há race na inicialização; e a estrutura da tabela fica sem controle de versão, o que dói no rolling deploy e em qualquer release que mude as colunas dela. **Vem desligado desde o primeiro commit** — habilitado, a tabela já existe quando alguém percebe, e passa a ser correção de produção em vez de linha de yaml. O DDL entra na baseline; o schema muda a cada release do Modulith, então confira as colunas na doc da versão em uso84 - Schemas separados por módulo: opcional, depende da estratégia adotada85- **Declare qual migration serve de modelo.** Quem vai criar tabela abre a mais recente e copia; se a mais recente for a que esqueceu alguma coisa, o esquecimento se propaga. Nomeie o arquivo-modelo no `AGENTS.md` — "a mais recente" não é instrução, é sorte86- **App multi-tenant tem skill própria.** Isolamento por `tenant_id`, Row Level Security, ordem das migrations de RLS e chave composta `(tenant_id, id)` estão em **`basis-multi-tenant`**. O que é específico daqui: a role do Flyway e a role da aplicação **não são a mesma**, e a tabela `event_publication` do Modulith entra na conta quando carrega `tenant_id`8788### Transações — `readOnly` por default na classe8990**`@Transactional(readOnly = true)` na classe de serviço, e `@Transactional` explícito só nos métodos que escrevem.** O ganho não é evitar digitação: é inverter o default para o lado seguro. Sem isso, método de leitura sem anotação nenhuma roda fora de transação e ninguém percebe — não dá erro, não aparece em log, e a consequência só existe em cenário que o teste unitário não cobre.9192```java93@Service94@Transactional(readOnly = true)95public class FuncionarioService {9697 public List<FuncionarioDTO> listar() { ... } // herda readOnly9899 @Transactional100 public FuncionarioDTO criar(NovoFuncionario cmd) { ... } // override explícito101}102```103104O mecanismo que faz disso um guarda-corpo, e não documentação: a anotação mais interna vence, então **esquecer o override num método de escrita produz falha, não corrupção**. Com routing de datasource, a escrita vai parar na réplica e estoura; sem routing, o Spring propaga o flag via `Connection.setReadOnly(true)` e o driver do Postgres marca a transação como read-only, então o `INSERT` falha com `cannot execute INSERT in a read-only transaction`. Nos dois casos o erro aparece no primeiro teste que exercitar o método — que é o momento certo.105106- `readOnly = true` é **pré-requisito** para roteamento primário/réplica via `AbstractRoutingDataSource`, que decide pelo `TransactionSynchronizationManager`. Adotar o padrão agora deixa a porta aberta mesmo em app que hoje tem um banco só107- Em **Spring Data JDBC** não há dirty checking para pular, então o ganho é o guarda-corpo mais o roteamento — o argumento de performance do mundo Hibernate não se aplica aqui108- **Não anote a classe inteira com `@Transactional` sem `readOnly`** para "resolver o esquecimento": isso não é default seguro, é escrita liberada em todo método109- Confirme o comportamento no projeto com um teste que tente escrever num método sem override. Se ele passar, o flag não está chegando na conexão e o guarda-corpo não existe110- Referência: [Read-write and read-only transaction routing with Spring](https://vladmihalcea.com/read-write-read-only-transaction-routing-spring/), Vlad Mihalcea111112## 5. Mensageria assíncrona com SCS113114### Bindings funcionais115- `<name>-in-0` / `<name>-out-0` (Spring Cloud Function naming)116- `function.definition: name1;name2;...` agrega os consumers117118### Topologia: ESCOLHER UM dono — não misturar119- **K8s dono** (recomendado em prod): topologia via CRDs RabbitMQ120 - SCS config: `default.consumer.bind-queue: false`, `auto-bind-dlq: false`121 - Main queue tem `x-dead-letter-exchange: DLX` + `x-dead-letter-routing-key: <destination>` definidos no CRD122- **SCS dono** (dev): SCS cria filas/DLQ automaticamente123 - SCS config: `default.consumer.bind-queue: true`, `auto-bind-dlq: true`124 - Default DLQ name = `<queue>.dlq`, default routing key = destination125126## 6. Web stack (quando aplicável)127128O padrão de UI (layout com sidebar, tabelas com cabeçalho/rodapé fixos, formulários, página de erro, tema `caramellatte`, build de CSS/JS) está na skill **`basis-web-frontend`**. Resumo do que a app Spring precisa saber:129130- Thymeleaf em `src/main/resources/templates/`, layout único em `layout.html` (`th:fragment="layout(content, activeMenu)"`)131- Fragmentos HTMX servidos por Controller dedicado, devolvendo só o trecho (`~{::fragmento}`)132- Tema DaisyUI **`caramellatte`**; `input.css` em `src/frontend/` (fora do classpath), output gerado em `target/classes/static/`133- `frontend-maven-plugin` na fase `generate-resources` roda `npm run build`134- `.gitignore`: `src/main/resources/static/*` exceto `static/images/` (assets versionáveis)135- `templates/error.html` **obrigatório** — sem ele a app cai na Whitelabel Error Page do Spring Boot; `server.error.whitelabel.enabled: false` e `include-stacktrace: never`136- `@ControllerAdvice` com `@ModelAttribute` para `appVersion` (de `BuildProperties`) e `currentUser` (do `OidcUser`), que o layout consome em toda página137138### Tratamento de exceção em app que serve HTML139140- **Não use `@RestControllerAdvice` numa app que serve HTML.** Ele é `@ControllerAdvice` + `@ResponseBody`: o retorno do handler é serializado como corpo da resposta, não resolvido como view. Numa app HTMX, uma exceção de negócio passa a **injetar JSON dentro do DOM**, no lugar onde o fragmento deveria entrar — a tela mostra `{"timestamp":...,"message":...}` em vez da mensagem de erro. Em app mista, separe: `@RestControllerAdvice` com `@ControllerAdvice(basePackages = ...)` limitado aos pacotes de API, e um `@ControllerAdvice` de view para o resto141- **Prefira exceção própria mapeada no handler global a anotação por endpoint com lista de exceções.** Anotação de marcação com opt-out (`@ExigeContexto` em quase tudo, mais uma lista dos que não exigem) cria uma **segunda lista de endpoints especiais ao lado do `permitAll` do `SecurityConfig`** — duas fontes de verdade sobre a mesma pergunta, que divergem na primeira vez que alguém mexe só numa. Uma exceção lançada onde o contexto falta, mapeada uma vez no handler global, cobre todos os casos sem lista nenhuma142- Se a separação precisar mesmo ser explícita, torne-a estrutural: um prefixo de path (`/api/publico/**`) é visível no `SecurityConfig`, no log e no roteador, em vez de estar espalhado em anotação143144## 7. Autenticação (Keycloak OIDC)145146Apps web autenticadas usam Keycloak no realm `basis`, fluxo OIDC. Padrões:147148- Spring Security registration **sempre** chamada `keycloak` — login fica em `/oauth2/authorization/keycloak`149- `provider.keycloak.issuer-uri: https://sso.apps.basis.com.br/realms/basis`150- Autorização por **client roles** (não realm roles), prefixadas com nome curto da app: `<APP>_USER`, `<APP>_ADMIN`151- Custom `OidcUserService` lê `resource_access.<clientId>.roles` e mapeia para `GrantedAuthority` com prefixo `ROLE_`152- `SecurityConstants` agrupa as roles em uma classe utility (evita typos espalhados)153- `/actuator/health/**` + estáticos sempre `permitAll()` (probes K8s precisam acesso anônimo)154155Ver [`references/keycloak-oidc.md`](references/keycloak-oidc.md) — config completa, SecurityConfig template, setup do client no Keycloak.156Para Keycloak local (compose + import do realm `basis`), ver §10 e [`references/local-dev-compose.md`](references/local-dev-compose.md).157158## 8. Observability (Actuator)159160**Obrigatório**. Sem `spring-boot-starter-actuator` no pom, as probes K8s caem em 404 e o pod entra em crash loop silencioso.161162- Endpoints essenciais: `/actuator/health`, `/actuator/health/liveness`, `/actuator/health/readiness`163- Liveness ≠ readiness: liveness só pode falhar se a JVM/Spring quebrar; readiness inclui dependências externas (DB, Rabbit, LDAP)164- Métricas Prometheus opcional via `micrometer-registry-prometheus` + `management.endpoints.web.exposure.include`165166Ver [`references/actuator.md`](references/actuator.md) — config completa, integração com SecurityConfig e probes K8s.167168## 9. Logging e depuração169170Convenções gerais de log (SLF4J, `{}`, níveis, nada sensível) estão em `basis-java-code-standards` §9. O que é específico de app Spring:171172### Banner de startup — obrigatório173- Após `app.run(...)`, logar bloco com nome da aplicação, URL local, URL externa, context path e **profiles ativos**174- Responde na hora "qual app, em que porta, com que profile" sem acesso ao pod175- Ler `server.port` e profiles do `Environment` (não do yml) — reflete a porta efetiva e o profile realmente ativo176- Nada de segredo no banner177178### DEBUG nos pontos de entrada — obrigatório179- Todo ponto onde estímulo externo entra loga em `DEBUG` o que chegou: Controller, consumer SCS, `@Scheduled`, listener de evento Modulith, webhook180- Identificador e campos que direcionam o fluxo — não o payload inteiro (payload em `TRACE`), nunca PII/segredo181- Consumer loga também o desfecho (processado/rejeitado + motivo) — mensagem sem contraparte no log é o sintoma de consumer travado182183### DEBUG em processamento complexo184- Método com várias etapas, chamada externa ou regra densa: um `DEBUG` no início (entrada) e um no fim (resultado, e duração quando útil), com o mesmo identificador nas duas linhas185- Critério: se falhar em produção, o log de erro sozinho diz *com que entrada* e *até onde foi*? Se não, precisa do par186- Duração em `DEBUG` é diagnóstico; o que precisa de acompanhamento contínuo vai para Micrometer/Actuator187188### Níveis189- `application.yml`: `br.com.basis.<app>: INFO`; `application-dev.yml`: `DEBUG`190- Em prod sobe para `DEBUG` sem redeploy via `LOGGING_LEVEL_*` ou `/actuator/loggers` — é para isso que os `DEBUG` existem no código191- `DEBUG`/`TRACE` de pacote de terceiro é investigação pontual, não config commitada (`org.springframework.jdbc.core: TRACE` loga valores de parâmetro de SQL)192193Ver [`references/logging-e-banner.md`](references/logging-e-banner.md) — código do banner, exemplos por tipo de entry point, guard `isDebugEnabled`.194195## 10. Ambiente de desenvolvimento local196197Meta: `git clone` → `docker compose up -d` → `mvn spring-boot:run -Dspring-boot.run.profiles=dev`, **sem editar arquivo de configuração**. `compose.yaml` e `application-dev.yml` são escritos como par — os valores do yml são os defaults declarados no compose.198199- `compose.yaml` na raiz: Postgres, RabbitMQ, Keycloak (quando autenticada), + o que a app usar (MinIO, GreenMail, Ollama). Portas sempre em `127.0.0.1`, healthcheck em todo serviço200- **Keycloak usa o Postgres da própria aplicação**, em banco/role separados (`keycloak`/`keycloak`) criados via `docker/postgres/init.sql`. Motivo principal: permite rodar comandos de administração no container com o Keycloak no ar — em especial `kc.sh export` do realm, que com o dev-file (H2) esbarra no lock do arquivo. De quebra: um container de banco só, estado sobrevivendo ao `down`, e mesma topologia de prod201- Realm `basis` importado de `./docker/keycloak` via `--import-realm`; partir de [`references/basis-realm-dev.json`](references/basis-realm-dev.json) (client `<app>-admin`, client roles `<APP>_USER`/`<APP>_ADMIN`, usuários `admin`/`admin` e `user`/`user`)202- **Export de realm nunca vai cru para o repo** — carrega chave RSA privada, segredos HMAC/AES, client secret e hashes de senha203- `application-dev.yml` traz datasource apontando para o compose (`localhost:5432`, usuário/senha = nome da app) e o issuer local (`http://localhost:9080/realms/basis`)204- **RabbitMQ não aparece no `application-dev.yml`**: os defaults do Spring (`localhost:5672`, `guest`/`guest`, vhost `/`) já batem com o container. Vhost dedicado e credencial real só em prod, via env var205- Em dev o SCS é dono da topologia (`declare-exchange`/`bind-queue`/`auto-bind-dlq: true`); em prod os CRDs do operator são206207- **Credencial de seed documentada no README.** Senha de usuário de teste que só existe como hash na migration é senha que ninguém consegue enunciar — e alguém entrando no projeto vai tentar adivinhá-la. Gere o hash a partir de uma senha declarada (`admin123`, `user123`), com o encoder do próprio projeto, e escreva qual é208209### Anti-padrões210- `application-dev.yml` apontando para infra compartilhada/remota com credencial real — vira segredo versionado, quebra o "clone e roda" e um dev derruba o ambiente do outro211- URL/credencial comentada com a alternativa "de verdade" logo abaixo — o profile deixa de ter config válida212- Senha forte em dev (a porta só escuta em `127.0.0.1`); `<app>`/`<app>`, `guest`/`guest`, `admin`/`admin` são melhores por serem obviamente descartáveis213- **`spring-boot-docker-compose` no classpath.** O starter publica um `JdbcConnectionDetails` a partir do `compose.yaml`, e esse bean tem **precedência sobre `spring.datasource.*`** — a app conecta onde o starter mandou, não onde o yml diz. Consequências: erro de configuração do datasource fica mascarado (o yml quebrado "funciona"), e não há como apontar a app para uma role diferente da do compose, o que impede o desenho de RLS da `basis-multi-tenant`. O fluxo desta skill (§10) não depende dele — `docker compose up -d` explícito é mais previsível214- **Deletar migration sem `mvn clean`.** As antigas continuam em `target/classes/db/migration` e o Flyway as aplica de lá. Rebaseline em dev é `./mvnw clean` **junto** com `docker compose down -v`; sem os dois, o banco reconstruído não é o que o repositório descreve215216Ver [`references/local-dev-compose.md`](references/local-dev-compose.md) — compose completo, `init.sql`, `application-dev.yml`, comando de export do realm.217218## 11. Testes219220- **Unit**: Mockito quando faz sentido (controllers, services puros)221- **Integração**: Testcontainers pra Postgres/RabbitMQ — não mockar infra que sobe local em segundos222- **APIs externas opacas** (AD/LDAP, Mailcow, Secullum, OPNsense): interface + impl real, teste manual contra infra; mocks só pros happy paths em controller tests223- **Suíte só de unitário com repositório mockado não cobre mapeamento objeto-relacional.** Trezentos testes verdes não dizem nada sobre `@Id` composto, `@Embedded`, conversor custom ou RLS — e é exatamente aí que uma refatoração de persistência quebra. Antes de mexer em id, herança de entidade ou policy de banco, o pré-requisito é ter pelo menos um IT que salve, busque e liste contra Postgres real224- **Não fixe versão que o parent do Spring Boot já gerencia.** Versão presa envelhece sozinha e o sintoma não aponta para ela: `failsafe` preso em 3.1.2 com o `surefire` em 3.5.6 falha por provider ausente no repositório local; `testcontainers-bom` preso em 1.18.3 (2023) falha contra engine Docker atual com erro de API. Nos dois, a correção é **remover** a versão, não escolher outra225- **`*IT.java` fora de pacote de produção.** Os testes ArchUnit varrem os pacotes de domínio por reflexão; um IT deixado ali é encontrado e **dispara o container durante a suíte unitária**, que passa a depender de Docker. O IT vai para pacote próprio226- Antes de escrever o primeiro IT, confira o pom: `failsafe` costuma já estar configurado, incluindo `**/*IT.java`, e nunca ter rodado por não existir nenhum227228### Estrutura (Modulith) — obrigatório quando a app usa Modulith229- `ApplicationModules.of(<App>Application.class).verify()` num teste — pega acesso a pacote `internal` de outro módulo, ciclo entre módulos e dependência não declarada230- Sem esse teste, "módulo" é só convenção de pasta: o build aceita qualquer `import` entre pacotes internos e a modularidade se dissolve sem sinal nenhum231- `Documenter` no mesmo teste gera os diagramas em `target/modulith-docs` — documentação que não desatualiza232- Fronteiras críticas do domínio declaradas explicitamente, principalmente as dependências que **não podem existir** (`containsModuleNamed(...)` com `isFalse()`); alternativa declarativa: `@ApplicationModule(allowedDependencies = ...)` no `package-info.java`233- `@ApplicationModuleTest` + `Scenario` para integração por módulo e para testar evento sem `Thread.sleep`234235Ver [`references/modulith-tests.md`](references/modulith-tests.md) — teste base, fronteiras de domínio, `@ApplicationModuleTest`.236237## 12. Interface com `basis-k8s-deploy`238239### Env vars que o app espera240- `SPRING_PROFILES_ACTIVE=prod` (ativa `application-prod.yml`)241- `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD`242- `SPRING_RABBITMQ_HOST`, `SPRING_RABBITMQ_USERNAME`, `SPRING_RABBITMQ_PASSWORD`, `SPRING_RABBITMQ_VIRTUAL_HOST`243- `APPLICATION_<CONFIGITEM>_*` para tudo que está em `application.*` no yml244245### O que documentar pro deploy246- Quando criar uma nova ConfigurationProperties section, listar os env vars equivalentes em `references/env-vars.md`247- Profile `prod` deve assumir que infra-secrets vêm de env (não tem default)248249## References disponíveis250251- [`references/configuration-properties.md`](references/configuration-properties.md) — Convenção `application.*` + `APPLICATION_*`, hierarquia yml/env/profiles, `@ConfigurationProperties` records, anti-padrões252- [`references/scs-rabbitmq.md`](references/scs-rabbitmq.md) — SCS functional bindings, K8s-dono vs SCS-dono, retry e DLQ, inspeção/retry manual253- [`references/keycloak-oidc.md`](references/keycloak-oidc.md) — OIDC com registration `keycloak`, client roles, SecurityConfig template254- [`references/actuator.md`](references/actuator.md) — Health probes (liveness/readiness), integração com K8s, métricas Prometheus255- [`references/pom-skeleton.md`](references/pom-skeleton.md) — pom multi-módulo + plugins essenciais (jib, frontend-maven-plugin), parent BOM, módulos típicos256- Build de frontend, layout, tabelas, formulários e páginas de erro: skill **`basis-web-frontend`**257- [`references/flyway-modulith-notes.md`](references/flyway-modulith-notes.md) — gotchas de schema com Modulith JDBC publisher, naming, baseline em legacy258- [`references/logging-e-banner.md`](references/logging-e-banner.md) — banner de startup, DEBUG em entry points e processamento complexo, níveis por ambiente259- [`references/local-dev-compose.md`](references/local-dev-compose.md) — `compose.yaml`, Keycloak no Postgres da app, `init.sql`, `application-dev.yml` espelhando o compose, export do realm260- [`references/basis-realm-dev.json`](references/basis-realm-dev.json) — realm `basis` mínimo para import em dev: client `<app>-admin`, client roles, usuários de teste261- [`references/modulith-tests.md`](references/modulith-tests.md) — `ApplicationModules.verify()`, `Documenter`, fronteiras de domínio, `@ApplicationModuleTest`262