Pular para conteúdo

0011 — Features-motor independentes + views como camada de composição

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

Contexto. A decisão 0010 reorganizou o código em package-by-feature sobre core+comum, guardado por ArchUnit (core puro, comum só depende de core, sem ciclos). A doc vendia "features constroem sobre core+comum", implicando independência entre features. Uma auditoria mostrou que a prática divergia: powersync importava serviços de ingest (PropriedadesNomeCurtoService, XadmResponse) e views importava de ingest e push. Como tudo apontava para ingest (um DAG), a guarda "sem ciclos" passava — mas não havia regra de independência, então ingest virou uma camada compartilhada de-facto sem ninguém perceber. A guarda era mais fraca do que a doc sugeria.

Decisão. Reafirmar a independência, mas distinguindo dois tipos de pacote de feature:

  • Features-motor (ingest, powersync, push, export) — constroem sobre core+comum e são independentes entre si. O que for compartilhado por mais de uma sobe para comum/core.
  • views — a camada de apresentação/composição, ACIMA das features-motor. Server-rendered (Thymeleaf), ela compõe telas a partir das features e por isso pode orquestrá-las legitimamente (ex.: o DebugViewController dispara push de teste e reparo de coluna para diagnóstico). views não é uma feature-motor.

Ações tomadas:

  1. Mover para comum os serviços/tipos compartilhados que moravam em ingest: PropriedadesNomeCurtoService (usado por powersync e views) e XadmResponse (usado por powersync). Também ApiHealthController → comum (health é concern de kernel; comum já abrigava o HealthController e o ApiHealthRequestRecorder).
  2. Adicionar a guarda ArchUnit featuresMotorSaoIndependentes: nenhuma das quatro features-motor depende de outra. views fica de fora por ser a camada de composição.

Consequências / trade-offs.

  • A aresta real powersync→ingest desaparece; o grafo das features-motor passa a ser um anti-grafo (sem arestas entre elas), guardado no gate — bem mais forte que "sem ciclos".
  • comum cresce um pouco: além de kernel puramente técnico, passa a hospedar serviços de negócio compartilhados (ex. PropriedadesNomeCurtoService). É um custo aceito — o critério é "usado por mais de uma feature-motor", não "é técnico".
  • Alternativa rejeitada (B): declarar ingest como camada compartilhada e só corrigir a doc. Rejeitada por perpetuar um "kernel de negócio" implícito e não-guardado dentro de uma feature.
  • Limite da guarda: o ArchUnit lê bytecode. Uma dependência cross-feature que exista só via constante static final (inlinada pelo compilador) não é detectada — foi o caso do antigo comum→ingest.ApiHealthController.ORIGEM_API_HEALTH, invisível ao gate. Acoplamento real (tipo, injeção, chamada) é detectado normalmente.

Nota (decisão 0025 — JTE). O motor de render das views mudou de Thymeleaf para JTE (templates compilados, native-first). A divisão de camadas desta decisão — views como camada de composição ACIMA das features-motor — permanece válida; só o mecanismo de template foi substituído.