← BLOG
DevStation27 de junho de 2026

Projetado para extensibilidade

architecturedddcqrsrpcai

Em um dado momento do DevStation, precisei renomear deploy para install e destroy para uninstall. Parece troca de nome, mas a mudança atravessava contrato, domínio, eventos, persistência e UI. É o tipo de operação que costuma revelar se a estrutura de um projeto ajuda ou atrapalha: quando as fronteiras são implícitas, uma renomeação dessas vira uma caça a referências espalhadas, e alguma sempre escapa. Aqui ela terminou como uma sequência de passos previsíveis, e esse resultado não foi sorte, foi a parte visível de uma aposta feita no começo do projeto.

O DevStation modela clusters, nodes, VMs, images, sizes, vault, provisionamento, stations e blueprints, cada um com regras próprias, e a lista de features que cabem num projeto assim não tem fim. Por isso a pergunta que orientou o design não foi “como entregar a feature de hoje”, e sim “como acomodar a próxima sem comprometer o que já existe”.

Hexagonal: o domínio isolado do mundo

A arquitetura hexagonal (ports & adapters, proposta por Alistair Cockburn em 2005) existe justamente para isolar a lógica de negócio da infraestrutura e permitir testá-la de forma independente. No DevStation, Proxmox, a TUI (a interface que roda dentro do terminal), o sistema de arquivos e o OpenTofu vivem na borda, e o núcleo os enxerga apenas como ports. Essa separação torna possível validar a fronteira com testes de arquitetura a cada commit: o domínio não importa infraestrutura, e o lado de entrada nunca importa o de saída. Até agora, adicionar um provider ou trocar a interface não tocou regra de negócio, que era exatamente o que eu esperava comprar com essa separação.

O desenho coincide com o que Herberto Graça catalogou como Explicit Architecture, a compilação dele de hexagonal, onion e clean num único mapa, a partir do que já se praticava: um App Core (Domain envolvido pela Application) com os ports como fronteira e todas as dependências apontando para dentro. A nomenclatura aqui é inbound (driving) e outbound (driven), no lugar de primário/secundário.

DDD tático + CQRS: contextos pequenos que crescem sem fricção

Os recursos são organizados em bounded contexts isolados (cluster, vault, station, service, images, size, blueprint, auth), cada um com seus agregados, comandos e eventos. Leitura e escrita são separadas seguindo CQRS. Como Martin Fowler descreve o padrão, trata-se de usar modelos distintos para ler e para escrever, e vale lembrar que CQRS não exige banco separado nem event sourcing. Aqui é CQRS de store único, em que as queries projetam registros direto, sem passar pelos agregados.

Onde isso me ajudou de verdade foi na manutenção. Extrair images para um contexto próprio foi replicar um padrão conhecido, não inventar um. Renomear definition para size no stack inteiro foi mecânico porque a fronteira era explícita. E a renomeação de deploy → install que abriu este artigo só se manteve uma operação disciplinada porque cada contexto sabe o que é seu.

Eventos internos entre contextos

O DevStation é um binário único, não um sistema distribuído. Ainda assim, usei eventos de domínio para impedir que os contextos dependessem diretamente uns dos outros: uma policy reage a um evento de outro contexto e dispara um comando do próprio. O context map do projeto registra isso como regra: comunicação cross-context não é direta. Quando uma station conclui uma instalação, o contexto de cluster registra a projeção dos serviços na VM e o vault guarda os secrets publicados, ambos por policy.

Num binário único isso adiciona indireção, e num sistema menor seria exagero. Aceitei o custo por um motivo específico: quero manter explícita uma fronteira que pretendo preservar caso partes do engine sejam separadas no futuro, num serviço compartilhado ou algo parecido. É mais barato manter essa porta aberta agora do que reabri-la depois. Se essa separação nunca acontecer, terei pago indireção por um seguro que não usei, e considero o preço razoável.

Contratos JSON-RPC: a interface é destacável

O engine expõe uma fronteira JSON-RPC sobre stdio, e os contratos (OpenRPC, com código gerado a partir deles) são o limite de verdade. A migração para essa fronteira foi deliberadamente incremental: um único contexto primeiro, sem caminho duplo, com revisão formal antes de expandir aos demais, porque erros de modelagem de envelope e de contrato são caros de reverter depois de espalhados. O resultado é que a TUI em React Ink é apenas um cliente. Do ponto de vista do engine, outra TUI, um app desktop ou uma web seriam o mesmo contrato com outra tela, e o MCP já é um segundo cliente real dessa mesma fronteira.

Complexidade, IA e o papel do harness

Estrutura tem custo, e esse custo virou tema: existem estudos e discussões recentes sobre desenvolvedores experientes ficando mais lentos com IA em repositórios que já dominavam, mesmo estimando o contrário, sobre a taxa de resolução de agentes caindo quando a mudança atravessa muitos arquivos, e sobre modelos degradando à medida que o contexto cresce. Daí uma crítica que ganhou força: cada camada é mais um arquivo para ler, cada indireção é mais um salto para rastrear, cada abstração sem propósito é ruído competindo por atenção. Humanos nunca pagaram por token para entender o código, agentes pagam.

A crítica faz sentido quando aponta para cerimônia. Interface com uma única implementação para sempre, use case que apenas organiza, mapper entre DTOs idênticos: se a resposta honesta para “que problema real essa camada resolve” é “organização”, o custo não se justifica, nem para pessoas, nem para modelos. Nenhum padrão do DevStation está isento dessa pergunta.

Ela deixa de fazer sentido, para mim, quando vira regra geral. Aqui, cada camada responde à pergunta que orientou o design. Os ports existem porque o provider é substituível por decisão, e porque TUI e MCP já são dois clientes do mesmo contrato. Os bounded contexts existem porque o domínio acumula regras de verdade. E tenho uma hipótese em cima disso: um contexto bem delimitado reduz o conjunto de arquivos que um agente precisa carregar para mexer numa feature. No DevStation isso parece diminuir as decisões ambíguas do agente, mas é a experiência de um projeto, e ainda não a considero suficiente para virar regra.

O que dá sustentação prática a tudo isso é o harness somado à verificação. As fronteiras deixaram de depender de eu lembrar de cada regra durante uma mudança: as regras são carregadas pelos agentes, sendo possível ainda usar testes de arquitetura para quebrar o build quando uma fronteira é violada. O custo de manter a disciplina não desapareceu, mas caiu o bastante para deixar de ser o principal argumento contra esse tipo de arquitetura.

O que consigo afirmar até agora

Volto à renomeação do começo. O que consigo afirmar hoje é que, neste projeto, refatorações amplas têm sido baratas e seguras, e que o custo da estrutura tem aparecido onde eu esperava: mais indireção para ler, mais arquivos para tocar numa feature pequena. Se essa troca continua valendo à medida que o DevStation crescer, é cedo para dizer. Até agora, cada mudança grande que atravessou o projeto inteiro saiu como a renomeação: trabalhosa, mas previsível. Nada disso torna a abordagem certa para qualquer sistema, e num projeto menor boa parte dessas camadas seria exagero. Para este, com a direção que planejo, a aposta segue de pé.

Referências