Pular para conteúdo

0030 — Adota o package-by-feature

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-14 · Decidido em: 2026-09-14

Contexto

A norma da casa é package-by-feature: controller, service, repository e entidade convivem no pacote da feature, e o transversal vive numa base comum (Java/Micronaut, Arquitetura). O bi-comercial-xls estava organizado por camada — api/, service/, repository/, processing/, domain/, dto/, security/ e exception/ —, e o processador irmão, bi-transporte-xls, já tinha migrado (decisão 0022 daquele repo). A migração para a constituição 2.0.0 é o refactor estrutural em que a troca entra.

Duas classes misturavam features e impediam o corte limpo: o TelegramService montava as mensagens do processamento e do nuke, e o StartupRecoveryService recuperava os dois no boot. Em qualquer fatia, cada uma forçaria uma dependência de volta para a outra feature.

Decisão

O código é package-by-feature sob br.com.onpetro.bi.comercial, com as camadas (controller, service, repository, entidade, DTO, view-model) dentro da fatia. Na raiz ficam só Application e BearerTokenEnv.

Fatia Papel
processamento o pipeline XLS: API de ingestão e consulta, telas /processamentos/**, recepção do .zip, fila assíncrona, lote, storage do binário e backfill, mensagens de Telegram do processamento e o recovery no boot; entidades xls_processamento e xls_conjunto_lote
processamento.planilha a leitura das planilhas: ArquivoDetector, TipoArquivo, ExcelSheetReader, ZipXlsExtrator, Segmento e os 9 {Tipo}Processor
dados as tabelas bi_* de negócio: entidades, repositórios e a escrita em lote (BulkUpserter, ColunasConteudo, UpsertResultado)
importacao a página aberta de import manual (/xls/importar) — ImportacaoXlsController
powersync o reset (nuke) da replicação: orquestrador, API REST, tela /admin, Mongo, slot, Coolify, auditoria, mensagens de Telegram do nuke e o recovery no boot
configuracao bi_configuracao: leitura com cache e a tela /admin/configuracoes
diagnostico as rotas /test e /test/glitchtip
seguranca a policy de acesso das views: ViewSecurityRule, ViewWhitelist e os handlers de rejeição
comum base transversal, não é feature: comum.exception (StorageIndisponivelException, 503) e comum.notificacao (TelegramService)
%% lint-mermaid: LR-ok
flowchart LR
    I[importacao] --> P[processamento]
    P --> PL[processamento.planilha]
    PL --> D[dados]
    P --> D
    P --> C[comum]
    PS[powersync] --> CF[configuracao]
    PS --> C
  • dados é folha: não depende de nenhuma fatia. Quem grava as bi_* depende dela, nunca o contrário.
  • comum não conhece feature. O Telegram se parte em transporte e formatação: o comum.notificacao.TelegramService só envia texto pronto; o ProcessamentoNotificador e o NukeNotificador montam as mensagens da sua feature. O recovery de boot se parte do mesmo jeito: ProcessamentoStartupRecovery (PROCESSANDO vira ERRO, PENDENTE volta à fila) e NukeStartupRecovery (nuke órfão vira ERRO), ambos atrás de app.recovery.enabled e depois do backfill do storage (@Order(-100)).
  • processamento.planilha não conhece o pipeline: depende de dados, nunca do pacote processamento (fila, lote, storage, controllers).
  • A exceção local de storage mora em comum.exception; as exceções HTTP genéricas vêm da xadm-comum-web. A decisão 0012 fica obsoleta.
  • O ComprasImportController passa a ImportacaoXlsController: o nome antigo era cosmético e o rename estava deferido (0019).
  • Os testes espelham as fatias; ficam na raiz os transversais (ArchitectureTest, bases de teste, TestXlsxFactory, /health, RFC 7807, serde).

Travas ArchUnit (ArchitectureTest, no check, sobre a RegrasArquitetura da xadm-comum-teste): import não-vazio; sem ciclos entre as fatias; comum não depende de fatia; dados é folha; processamento.planilha não depende do pacote processamento; controller não acessa repository; nada depende de controller; SQL cru pela conexão só nas classes de bulk da allowlist (as de dados e powersync.PostgresReplicationAdmin); entidade livre de infra; escrita de controller de view declara @Consumes. As regras de fronteira citam o nome completo da fatia: o padrão curto (..seguranca..) casaria também o pacote br.com.xadm.comum.seguranca da lib.

Consequências

  • O código de uma feature fica junto; o transversal fica explícito em comum, e o ArchUnit barra a volta de dependência por descuido.
  • Migração mecânica (git mv, pacote e imports), sem mudança de contrato: rotas, JSON, OpenAPI, schema e envs seguem iguais.
  • A guarda foi provada na migração: uma violação temporária por regra de fronteira reprovou cada uma, e o verde voltou ao desfazê-las. Rename futuro de pacote leva o literal da regra junto e repete a prova (norma, Rename de pacote).
  • As telas de admin recebem view-model (NukeView, ConfiguracaoView), nunca a entidade.
  • bi-comercial-xls e bi-transporte-xls ficam no mesmo layout; o corte difere onde o domínio pede — lá as bi_* moram em processamento, aqui são a fatia dados.

Alternativas consideradas

  • Manter package-by-layer como exceção (a versão anterior desta decisão, não publicada): descartada. A exceção não tinha motivo de domínio, e o custo do rename é o mesmo agora ou depois.
  • bi_* dentro de processamento, como no bi-transporte-xls: descartada. Como fatia folha, o modelo de BI se lê sem puxar o pipeline, e a regra "a planilha grava sem conhecer o pipeline" fica verificável.