Pular para o conteúdo principal
Arquitetando Software em 2026 · 2 de 6 partesEngineering12 min de leitura

Arquitetura como código tem três funções: descrever, governar, lembrar

Na primeira semana do Bullpen, o modelo de arquitetura planejado ainda citava um provedor de ações que eu tinha recusado, e uma mudança no rodapé e um teste o seguiram. Nada falhou. Uso esse incidente e outros dois para defender que arquitetura como código tem três funções: descrever o sistema, governá-lo e lembrar por que ele tem a forma que tem. Depois sigo a primeira mudança real após as correções, o analytics de produto, pelas três: um registro de decisão antes que o modelo pudesse citá-lo, arquitetura construída e planejada que discordavam, e uma regra que compõe as duas em vez de editar qualquer uma delas.

← Todos os Posts
2/4

Em 26 de setembro, o modelo de arquitetura planejado do Bullpen e o meu registro de decisões discordavam. O momento da Parte 6 ainda citava Alpaca e Massive como provedores de ações, ao lado de um nó Stripe. O registro, fora do repositório, dizia que eu tinha escolhido outra fonte de dados de ações um dia antes, com a Alpaca como reserva; pagamentos nunca estiveram no plano. Uma mudança no rodapé naquele dia seguiu o modelo: escreveu "U.S. stocks by Alpaca, from Part 6" e adicionou um teste que passava porque codificava o mesmo provedor errado. A decisão dizia o contrário. Nada falhou: nada comparava o modelo com as decisões.

Arquitetura como código tem três funções, e aquele incidente mostra todas elas: descrever o sistema, governá-lo e lembrar por que ele tem a forma que tem. Na primeira semana, o Bullpen cumpriu bem a primeira, a segunda em parte e a terceira quase nada. Quem mexer no código depois vai confiar em qualquer arquivo commitado que ler. Por isso, cada registro precisa de uma checagem que o mantenha verdadeiro, e cada regra precisa de um lugar onde falhe.

Três incidentes na primeira semana do Bullpen

O Bullpen é a liga de paper trading que estou construindo em público nesta série, e a Parte 1 explica por que ele começa como um único implantável. Nos primeiros seis dias, três coisas deram errado, e em cada uma delas a regra existia só como prosa, ou nem isso.

DataO que aconteceuOnde a regra vivia
25–27 setO histórico já enviado foi reescrito uma vez, fechando o PR #10; duas correções foram direto para a main; três pull requests foram mesclados com squash, contra as regras de histórico do repositórioProsa: notas de handoff e minhas notas de status
26 setO modelo planejado ainda citava um provedor de ações que eu tinha recusado, e o rodapé e um teste o seguiramMeu registro de decisões, fora do repositório
25–26 setQuatro decisões viraram ADRs, e nenhuma delas chegou à linha do tempo que liga as decisões à arquiteturaEm lugar nenhum: nada comparava os dois

Nenhum deles causou dano duradouro; o PR #12 substituiu o #10, e nada se perdeu. A linha do meio é a pior, porque a decisão nem estava escrita no repositório. Ela vivia só no meu registro de decisões e na minha cabeça.

As três funções da arquitetura como código

Muitas configurações de arquitetura como código que já vi cumprem uma das três funções e dão o trabalho por encerrado. Em uma frase, as três se encaixam assim: o modelo descreve a arquitetura, os registros de decisão guardam a intenção, e o CI verifica se os dois continuam consistentes.

  • Descrever significa ter um modelo do sistema que uma máquina consegue ler e validar. No Bullpen, é um documento FINOS CALM (Common Architecture Language Model) por parte da série, mais um pattern que todo modelo precisa satisfazer. A landing page desenha seu explorador de arquitetura a partir desses arquivos.
  • Governar significa ter regras que o build impõe: regras de estrutura escritas em uma linguagem de definição de arquitetura (ADL), uma checagem de orçamento de chamadas e o Turborepo Boundaries, que reporta uma violação quando um app importa outro sob uma regra de tag que proíbe isso. Uma fitness function é só um teste, e eu a trato assim.
  • Lembrar significa ter registros do porquê: registros de decisão de arquitetura (ADRs) numerados e uma linha do tempo CALM que liga cada momento da arquitetura às decisões que o produziram.

Nada disso é novo isoladamente; já experimentei o par descrever-mais-governar com um workspace Structurizr. O que o Bullpen acrescenta são as três funções em um único sistema que muda todos os dias.

Descrever: um modelo em que se acredita

O CI valida o modelo da Parte 1 do Bullpen e sua linha do tempo com calm validate --strict em todo pull request, e a main está verde. Essa é a armadilha. A validação checa forma, não verdade. Um modelo pode satisfazer todas as regras do schema e ainda citar um provedor que ninguém escolheu.

Este é o fragmento que a mudança no rodapé seguiu, de planned/part-06.architecture.json como estava em 26 de setembro:

json
{
  "unique-id": "alpaca-provider",
  "node-type": "external-system",
  "name": "Alpaca"
}

Os momentos planejados são o meu plano para as Partes 2 a 6. Para quem lê um deles em seguida, pessoa ou agente, um plano em um arquivo JSON commitado parece exatamente um fato. O repositório guardava o modelo e a decisão vivia fora dele, então a mudança ficou do lado do repositório.

Esse é o drift de arquitetura na sua forma mais silenciosa: o código estava certo, e o modelo tinha se afastado das decisões. A correção é uma checagem de drift que roda em todo pull request. Todo sistema externo no momento atual, e em todo momento planejado, precisa estar decidido em um ADR ou listado como em uso ou proposto em docs/data-sources.md. Um nome que aparece apenas como recusado não conta, porque contá-lo deixaria passar o erro da primeira semana. Todo diretório que a ADL define também precisa corresponder a um nó no modelo atual, e vice-versa.

O teste do rodapé e a checagem de drift se parecem, mas são tipos diferentes de checagem. Em uma sessão ao vivo em 1º de outubro, Neal Ford deu um teste de tornassol de uma pergunta só: preciso de algum conhecimento de domínio para escrever este código? O teste do rodapé precisava saber qual provedor o Bullpen usa, então era um teste funcional, tão certo quanto quem o escreveu. A checagem de drift não sabe nada sobre dados de mercado. Ela compara o modelo com os registros, e isso faz dela uma fitness function.

Para ver se ela teria pegado o erro da primeira semana, coloquei o nó da Alpaca de volta no modelo planejado na branch da Parte 2 e rodei as checagens. Duas delas falharam. Esta é a primeira, com as linhas quebradas para caber na página:

text
✗ model currency: every external system in a CALM moment is decided
  in an ADR Decision or listed as in use or proposed in
  docs/data-sources.md
  where:   architecture/calm/planned/part-06.architecture.json
  why:     Node "alpaca-provider" is the external system "Alpaca",
           which ADR-0008 Alternatives records only as turned down
           (ADR-0009).
  fix:     Rename the node to the provider that was chosen, or write
           a new ADR that reverses the rejection before the model
           names it.

A segunda falha veio da checagem que mantém o explorador da landing page em sincronia com o modelo: ela encontrou um nó que o explorador não desenha. A mudança no rodapé, em setembro, teria falhado no CI nas duas antes de poder ser mesclada.

Governar: uma regra precisa falhar no build

Uma regra útil falha no momento do erro, e sua mensagem diz o que fazer no lugar.

As regras estruturais vivem em um único arquivo ADL, escrito no estilo que Ford e Mark Richards usam em Architecture as Code. Este bloco diz quais partes do Bullpen nunca podem depender de quais:

text
# Disallowed dependencies (direct and transitive)
ASSERT(Landing HAS NO DEPENDENCY ON Trading App)
ASSERT(Trading App HAS NO DEPENDENCY ON Landing)
ASSERT(UI HAS NO DEPENDENCY ON Landing, Trading App)
ASSERT(Contracts HAS NO DEPENDENCY ON Landing, Trading App)

O comentário resolve uma questão que a linguagem deixa em aberto: uma dependência lavada por meio de uma biblioteca também conta. Ford tem um nome para o import que este bloco proíbe: trapaça nas dependências (cheating on dependencies). Em um monorepo, o código que você quer está logo ali na pasta ao lado, não importa quem esteja digitando, e nada além de uma regra impede o import.

Estas são fronteiras sem chamada de rede, o tipo mais barato que a Parte 1 descreve. A ADL as mantém honestas até que um dos cinco motivos justifique um serviço. Cada assert do arquivo corresponde a uma checagem capaz de reprová-lo, e uma regra que nenhuma checagem impõe também quebra o build. Toda checagem falha em um único formato: a regra exatamente como a ADL a declara, onde ela quebrou, por que a regra existe, com o número do ADR, e a menor correção possível.

As regras de histórico também saíram da prosa. Um job de CI, commit-policy, verifica o autor e os trailers de cada commit em um pull request, e reconhece a identidade correta por uma impressão digital, então meu endereço não aparece em nenhum arquivo. A proteção de branch na main exige um pull request e as duas checagens, permite apenas merge commits e tem o bypass de admin desligado. Essa última parte é a que me pega, já que os squash merges foram meus.

Lembrar: registros também sofrem drift

A função de lembrar tem seu próprio drift. Na primeira semana, a linha do tempo do Bullpen ligava o momento da Parte 1 do ADR-0001 ao ADR-0003, enquanto outras quatro decisões chegavam naquela mesma semana. A linha do tempo agora liga cada ADR ao momento da sua parte, e a checagem de drift falha quando falta algum.

Os planos levantam uma questão mais difícil: que momento no tempo um arquivo planejado representa? Ele não pode ser, ao mesmo tempo, o plano como está hoje e a previsão que fiz na primeira semana. Por isso o Bullpen mantém dois registros com duas promessas:

  • Validade atual vive em planned/. Esses arquivos são o plano como ele está, e a checagem de drift os mantém fiéis às decisões em vigor hoje. Quando registrei a decisão sobre a fonte de dados de ações em 26 de setembro, Alpaca, Massive e Stripe saíram das Partes 3 a 6.
  • Fidelidade histórica vive no git. As previsões exatamente como as escrevi em 25 de setembro ficam na tag predictions-week-1, e é com ela que a Parte 6 compara o que foi entregue.

Nenhum dos dois é reescrito para combinar com o que foi entregue. Uma semana depois, essa regra teve seu primeiro teste real.

Quando o que roda e o que está planejado discordam

Em 1º de outubro, adicionei analytics de produto ao Bullpen, Google Analytics e Microsoft Clarity, carregados só depois que o visitante dá consentimento. Os dois entraram no modelo da Parte 1 como sistemas externos. Então avancei o explorador até a Parte 2, e os dois desapareceram, junto com suas quatro conexões. A página agora dizia que eu tinha removido o analytics depois da Parte 1. Nada o tinha removido: os planos das Partes 2 a 6 foram escritos antes de o analytics existir, e o explorador desenhava cada parte planejada só a partir do seu plano.

A correção óbvia era adicionar os dois sistemas aos cinco arquivos de plano. Isso teria colocado um fato construído em seis lugares, cada um capaz de sofrer drift por conta própria, e teria tratado o silêncio de um plano como algo a corrigir. Um plano que não diz nada sobre um sistema em execução não está prevendo a sua remoção.

Então o ADR-0011 compõe os dois registros quando o explorador os desenha. O que está construído e ainda em execução é levado adiante para toda parte planejada seguinte, com o rótulo "Built in Part 1" (construído na Parte 1), e nada é escrito nos arquivos de plano. Um plano ainda prevalece em tudo o que nomeia, então uma substituição planejada continua sendo uma substituição: o serviço de snapshot de preços da Parte 1 não é desenhado ao lado dos dois serviços que a Parte 3 planeja no lugar dele.

As outras duas funções seguraram a mudança no lugar. O ADR-0010 registrou a decisão antes que o modelo citasse qualquer um dos dois sistemas, e, na branch da Parte 2, renomear um dos novos nós para um sistema que nenhum ADR menciona faz a checagem model currency falhar, como deveria. A checagem de consistência do explorador só aceita um card levado adiante se ele existir em um momento construído e indicar o momento que o construiu primeiro, então um card levado adiante não pode ser inventado.

Rode as checagens você mesmo

O repositório é público, e cada parte da série vive na sua própria branch. A branch desta parte é post-02-architecture-as-code. Com Node 22 e pnpm 12, um clone novo roda as mesmas checagens de arquitetura que o CI:

bash
git clone https://github.com/tiarebalbi/bullpen.git
cd bullpen
git checkout post-02-architecture-as-code
pnpm install
pnpm check:arch

Em um clone limpo, a saída termina com check:arch passed: 9 checks held, 19 rules enforced. Para ver uma delas falhar, adicione qualquer nó de sistema externo a architecture/calm/planned/part-06.architecture.json e rode de novo.

Onde a arquitetura como código ainda quebra

Essas checagens estreitam a lacuna. Não a fecham.

  • Uma checagem só reprova o que pensei em codificar. Intenção não pode ser checada. Uma afirmação que o código pode refutar pertence a uma checagem; um motivo que ele não pode refutar pertence a um ADR.
  • As próprias checagens podem estar erradas. As checagens do Bullpen foram geradas a partir da ADL, o que Ford chama de fitness functions interpoladas, e ele foi direto: o arquiteto precisa revisar cada uma. Cada uma vem com uma fixture que precisa falhar, e nenhuma deveria proteger o build antes que um humano a tenha lido.
  • Sem um segundo revisor. A proteção de branch exige checagens verdes, mas nenhuma aprovação, porque não há mais ninguém para aprovar. Em um time, uma revisão obrigatória é a única camada capaz de questionar a intenção.
  • Uma checagem pode ser contornada. Sou o admin que desligou o bypass, e posso ligá-lo de volta. O Boundaries também ainda está marcado como experimental, e uma regra construída sobre ele herda esse status.
  • Toda regra é manutenção. A checagem de drift faz do plano algo que eu mantenho, e cada nova checagem é mais uma coisa que toda mudança precisa satisfazer antes de ser entregue.

Um checklist para a próxima regra que eu escrever

Antes de uma regra ir para um documento, um ticket ou um README, agora faço cinco perguntas:

  • Onde ela falha: no CI, na proteção de branch ou em lugar nenhum?
  • A falha nomeia a regra, o lugar, o motivo e a correção?
  • Existe uma fixture que prove que a checagem pode falhar?
  • Qual registro ela cita, e o que mantém esse registro atualizado?
  • Se ela não pode falhar em lugar nenhum, eu a arquivei como ADR e parei de chamá-la de regra?

O objetivo nunca foi manter arquivos de arquitetura sincronizados por eles mesmos. É impedir que um retrato commitado do sistema, como aquele nó da Parte 6 que citava a Alpaca, passe silenciosamente por cima das decisões que ele deveria seguir.

Continue lendo

Curtindo? Talvez goste disso aqui.

Nada parecido — quer tentar outro ângulo?

Arquitetando Software em 2026 · 2 de 6

A parte 3 está em produção.

Novas partes saem às segundas-feiras, 9h (horário do Pacífico) — deixe seu e-mail e eu envio cada uma no dia em que sair. Só isso.

Isso foi útil?

Deixe uma avaliação ou uma nota rápida — me ajuda a melhorar.

Posts Relacionados

Engineering

Quando usar microsserviços em 2026: escalar, por si só, já não é o motivo

No dia em que o Dow registrou uma alta recorde para a época, os clientes da Robinhood não conseguiram operar da abertura ao fechamento. Separar a parte quente para que ela escale sozinha era a resposta antiga; hoje uma plataforma serverless faz isso por função. Abro esta série com o Bullpen, uma liga de paper trading sobre dados de mercado ao vivo, e defendo quando usar microsserviços em 2026: só por um motivo que eu consiga nomear, com um teste, uma medição ou uma fatura por trás.

AI

Fitness Functions São o Control Plane para Agentic Coding

O post anterior perguntou para onde foi o ponto de controle. Aqui está a primeira resposta que estou disposto a defender: ele não desapareceu — parte dele compilou. Fitness functions arquiteturais, apontadas para agentes de código, se tornam o control plane que permite ao desenvolvedor permanecer no comando sem virar o gargalo: julgamento compilado uma vez em gates determinísticos que aplicam a regra na velocidade da máquina, com mensagens de falha escritas como prompt engineering para o loop de retry. Em seguida, a complicação que dá forma ao post inteiro: no momento em que um agente otimiza contra a compilação, o controle compilado vira objeto de ataque — e o design de fitness functions herda uma corrida armamentista, com uma constituição em Kotlin/ArchUnit para tornar tudo concreto.