0022 — Organização package-by-feature¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-06-18 · Decidido em: 2026-06-18
Contexto¶
O código nasceu package-by-layer: pacotes globais api/, service/,
repository/, domain/, security/, dto/, exception/ — cada um juntando
arquivos de funcionalidades diferentes. Ao consolidar o padrão Micronaut da
casa (skill micronaut, decisão de Estrutura de código), o default ficou
sendo package-by-feature: cada funcionalidade carrega suas próprias camadas, e
o que é transversal vai para uma base comum. Este app era o outlier; como ele
deve seguir o default da casa salvo divergência justificada (§1.6), migra-se.
Decisão¶
Package-by-feature com camadas Controller → Service → Repository dentro de
cada feature. Pacotes de topo sob br.com.vantroba.bi.transporte:
| Pacote | Papel |
|---|---|
processamento |
pipeline XLS ponta a ponta: controllers, services, repositórios, entidades bi_*/xls_*, storage; subpacote processamento.processing — leitura Excel SAX (POI streaming) e mapeamento de colunas |
powersync |
reset do PowerSync e o que orbita o sinal de reset |
configuracao |
configuração global runtime (admin) |
seguranca |
as duas portas de auth — Bearer estático (/api/**) + login Google das views, handlers de rejeição |
comum |
base transversal, não é feature: comum.config (typed config, modos de storage), comum.exception (só StorageIndisponivelException, 503 do domínio XLS), comum.util, comum.notificacao (Telegram). A infra web (RFC 7807 ProblemDetail + processor, exceções HTTP base, health, Sentry, versão) migrou para a lib br.com.xadm.comum.web — decisão 0024 |
Regras:
- Entidade (
@MappedEntity) não cruza o controller. A borda HTTP fala DTO (record+@Serdeable) na API ou view-model no Thymeleaf. - Injeção por construtor; service concentra regra de negócio e
@Transactional; repository só acessa dado (Micronaut Data JDBC, decisão 0001). comumnão depende de feature — sem exceção. O Telegram fica partido em transporte (comum.notificacao.TelegramService, recebe texto pronto) e formatação (um notificador por feature:ProcessamentoNotificador,ResetarPowersyncNotificador), então quem conhece o domínio é a feature, nãocomum.
Trava: ArchUnit (divergência consciente do default da casa)¶
A skill da casa diz que a governança das camadas é convenção + revisão de PR +
./gradlew check, e descarta ArchUnit. Este app mantém o ArchitectureTest
(que já existia para o layout antigo), reescrito para três invariantes do
package-by-feature:
- sem ciclos entre as features de topo (a invariante central);
- nenhum subpacote de
comum(inclusivecomum.notificacao) depende de feature; processamento.processingnão depende da camada HTTP (nada que termine emController) — a leitura do Excel não conhece HTTP.
Mantido porque o teste já estava no projeto (custo zero), roda em ./gradlew
check (não é dependência nova) e trava exatamente as invariantes que importam no
package-by-feature. É divergência declarada do default da casa → registrada
como feedback §6 para a constituição reavaliar o "ArchUnit descartado"
categórico.
Consequências¶
- O código de uma feature fica junto — some o vai-e-volta entre seis pastas de camada conforme o app cresce.
comumvira a fronteira explícita do que é transversal; o ArchUnit impede que ela volte a depender de feature por descuido.- Migração mecânica (mover arquivo + ajustar
package/imports), sem mudança de comportamento: 108 classes main + 44 de teste movidas, suíte verde (293 testes, ArchUnit e checkstyle limpos).
Alternativas consideradas¶
- Manter package-by-layer: era o estado do app; divergente do default da casa, dispersa os arquivos de uma mesma funcionalidade. Descartado.
- Remover o ArchUnit para alinhar 100% à casa: descartado por ora — o teste já existia e trava as invariantes do package-by-feature de graça. A reavaliação fica para o §6 (se a casa mantiver "sem ArchUnit", removê-lo aqui é trivial).