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 sobrecore+comume são independentes entre si. O que for compartilhado por mais de uma sobe paracomum/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.: oDebugViewControllerdispara push de teste e reparo de coluna para diagnóstico).viewsnão é uma feature-motor.
Ações tomadas:
- Mover para
comumos serviços/tipos compartilhados que moravam emingest:PropriedadesNomeCurtoService(usado porpowersynceviews) eXadmResponse(usado porpowersync). TambémApiHealthController→comum(health é concern de kernel;comumjá abrigava oHealthControllere oApiHealthRequestRecorder). - Adicionar a guarda ArchUnit
featuresMotorSaoIndependentes: nenhuma das quatro features-motor depende de outra.viewsfica de fora por ser a camada de composição.
Consequências / trade-offs.
- A aresta real
powersync→ingestdesaparece; o grafo das features-motor passa a ser um anti-grafo (sem arestas entre elas), guardado no gate — bem mais forte que "sem ciclos". comumcresce 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
ingestcomo 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 antigocomum→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 —
viewscomo camada de composição ACIMA das features-motor — permanece válida; só o mecanismo de template foi substituído.