Em sistemas com alta densidade de domínio, onde o valor central do software depende da correção de cálculos científicos, técnicos ou regulatórios, existe uma armadilha arquitetural silenciosa: a lógica de computação se acumula dentro dos serviços de aplicação.
Tudo começa pequeno. Uma função de estimativa embutida em um caso de uso. Uma função auxiliar de cálculo importada por um controller. Uma classe de serviço que "também faz as contas". Cada adição parece razoável no momento. O problema só surge quando alguém faz uma pergunta simples e inevitável:
"Qual fórmula gerou este número, e consigo reproduzi-la exatamente agora?"
Se a resposta exigir git blame, rastreamento de contexto ou tentativa e erro, o sistema já está quebrado.
Este artigo explica por que isolamos a computação determinística em uma biblioteca interna autocontida, o Core Engine, e as restrições arquitetônicas que motivaram essa decisão.
O custo de cálculos espalhados
Quando os cálculos ficam espalhados entre serviços de aplicação, eles inevitavelmente produzem quatro falhas estruturais.
1. Duplicação e deriva de fórmulas. Com o tempo, a mesma fórmula aparece em vários lugares com variações sutis: constantes ligeiramente diferentes, tratamento divergente de casos limite ou arredondamento inconsistente. Cada versão "parece certa" para quem a escreveu, mas não existe uma fonte canônica da verdade.
2. Não reprodutibilidade histórica. Se um resultado calculado em 2026 precisar ser auditado em 2028, a pergunta "qual era a fórmula quando isso foi gerado?" muitas vezes não terá uma resposta rastreável. O serviço mudou. A fórmula foi "corrigida". O resultado original se tornou irrecuperável.
3. Auditabilidade estruturalmente impossível. Sistemas com obrigações regulatórias ou técnicas devem comprovar que uma determinada saída foi produzida por uma fórmula conhecida e aprovada. Se essa fórmula estiver entrelaçada com I/O, lógica de ciclo de vida, feature flags e regras de negócio, isolá-la para fins de auditoria não é um problema de ferramentas. É um problema estrutural.
4. Testabilidade comprometida. Você acaba precisando rodar o serviço inteiro, incluindo mocks de banco de dados, apenas para validar uma equação matemática. Isso, por si só, já é um problema arquitetural.
Restrições que moldaram o design
Para resolver isso, tivemos que lidar com diversas forças inegociáveis:
Reprodutibilidade como contrato: um resultado gerado hoje deve ser calculável usando o mesmo algoritmo anos depois. Versões antigas de fórmula não podem ser modificadas retroativamente.
Proveniência obrigatória: o domínio exige que toda saída carregue proveniência algorítmica explícita, não como metadados opcionais, mas como campo obrigatório persistido em cada artefato de cálculo.
Múltiplos contextos de execução: os mesmos cálculos precisam rodar na API do backend, no frontend, no aplicativo móvel e em pipelines de IA/ML. Incorporar isso em um único serviço força a duplicação nos consumidores downstream.
Separação de responsabilidades: a aplicação decide quando calcular, com quais entradas e onde persistir o resultado. A fórmula decide apenas como calcular.
A solução: um Core Engine determinístico
Concluímos que toda lógica de computação determinística precisa viver em uma biblioteca interna autocontida, o Core Engine, governada por regras estritas e não negociáveis.
Regra 1: pureza absoluta
Toda função do Core Engine é pura. Dadas as mesmas entradas, sempre retorna a mesma saída. Não há efeitos colaterais, nem I/O, nem acesso a banco de dados, nem leitura de feature flags. O Core Engine não tem noção de organização, usuário, plano de assinatura ou contexto de execução.
graph LR
subgraph "Camada de aplicação"
AS[Serviço de Aplicação]
REPO[(Repositório)]
CONTEXT["org_id / user_id / flags"]
end
subgraph "Core Engine"
direction TB
FORMULA["calculateMonthlyPayment_v1(principal, rate, term)"]
NOTE["Função pura
Sem I/O · Sem contexto · Sem estado"]
end
AS -->|apenas primitivas| FORMULA
FORMULA -->|número| AS
AS --> REPO
CONTEXT -.->|proibido| FORMULARegra 2: apenas entradas primitivas
O Core Engine aceita apenas tipos primitivos ou tipos de valor planos. Entidades de domínio complexas, como LoanApplicationEntity, são estritamente proibidas como entrada. Antes de invocar o Core Engine, o serviço de aplicação deve extrair os valores primitivos necessários. Isso garante que o Core Engine nunca adquira dependências implícitas de contratos de domínio em evolução.
// Correto — primitivas extraídas antes de chamar o Core Engine
const payment = calculateMonthlyPayment_v1(
draft.principalAmount,
draft.annualRate,
draft.termMonths
);
// Proibido — passar uma entidade de domínio complexa
const payment = calculateMonthlyPayment_v1(loanApplication);Regra 3: versionamento explícito e imutável
Toda fórmula tem uma versão explícita codificada diretamente no nome, como calculateMonthlyPayment_v1 e calculateMonthlyPayment_v2, cada uma vivendo em seu próprio arquivo.
Versões antigas são matematicamente imutáveis. Corrigir um bug que muda o resultado do cálculo significa criar uma nova versão, nunca sobrescrever a existente.
Nota: a imutabilidade se aplica à lógica matemática. Se uma versão congelada depender de uma biblioteca externa que receba um patch de segurança para CVE, atualizar essa dependência é permitido, desde que o comportamento computacional da fórmula permaneça idêntico byte a byte. Alterar constantes, coeficientes ou passos algorítmicos sob um identificador de versão existente é estritamente proibido.
timeline
title Ciclo de vida de versões de fórmula
section Pagamento mensal
2024-01 : calculateMonthlyPayment_v1 liberada
2025-03 : calculateMonthlyPayment_v2 liberada (suporte a taxa variável)
: v1 congelada — continua executável, sem alteração
section Escore de risco
2024-01 : calculateRiskScore_v1 liberada
2024-09 : calculateRiskScore_v2 liberada (tabela atuarial atualizada)
: v1 congelada — disponível em paralelo para replay históricoRegra 4: exceções apenas para invariantes matemáticos
O Core Engine lança exceções apenas para violações de invariantes matemáticos: principal negativo, prazo zero, divisão por zero ou valores NaN como entrada. Ele não lança exceções de negócio, nem retorna null ou -1 como sinais de erro. A distinção entre falha matemática e falha de domínio é explícita e tipada:
class EngineInvariantError extends Error {
constructor(message: string) {
super(message);
this.name = 'EngineInvariantError';
}
}
// Uso — quem chama sabe exatamente qual tipo de falha esperar
try {
const payment = calculateMonthlyPayment_v1(principal, rate, term);
} catch (e) {
if (e instanceof EngineInvariantError) {
// A matemática está quebrada — os dados de entrada são estruturalmente impossíveis
}
// Todos os outros erros se propagam normalmente
}Regra 5: persistir proveniência é responsabilidade do serviço
O Core Engine calcula e retorna o resultado. O serviço de aplicação persiste o resultado com a versão da fórmula utilizada como um campo obrigatório em cada artefato persistido:
// Serviço de aplicação — orquestra, persiste e assume a proveniência
async function priceLoan(application: LoanApplication): Promise<PricingSnapshot> {
// 1. Extrai primitivas das entidades de domínio
const principal = application.requestedAmount;
const rate = application.offeredAnnualRate;
const term = application.termMonths;
// 2. Invoca o Core Engine — computação pura, sem contexto
const monthlyPayment = calculateMonthlyPayment_v1(principal, rate, term);
const riskScore = calculateRiskScore_v2(application.creditScore, application.debtRatio);
const effectiveRate = calculateEffectiveRate_v1(rate, application.fees, term);
// 3. Persiste o resultado com proveniência explícita da fórmula
return snapshotRepository.save({
application_id: application.id,
outputs: { monthlyPayment, riskScore, effectiveRate },
metadata: {
formula_versions: {
// obrigatório — é isso que torna possível o replay
monthlyPayment: 'v1',
riskScore: 'v2',
effectiveRate: 'v1',
},
},
});
}Sem essa persistência, a reprodutibilidade histórica não existe. A versão da fórmula ficaria inferida implicitamente, o que invalida qualquer alegação de auditabilidade.
Alternativas consideradas
A1 — cálculos incorporados diretamente em casos de uso ou serviços
A alternativa mais natural e a mais perigosa. A computação coexiste com a lógica de ciclo de vida, a validação de negócio e a persistência. Conveniência máxima no início. Mas:
sem versionamento explícito, a primeira "correção" já quebra a reprodutibilidade histórica;
testar a fórmula exige simular repositórios e contexto de execução;
reutilizar em contextos downstream, como aplicativos móveis ou pipelines de IA, exige duplicação ou uma extração tardia e cara.
Rejeitada porque cria dívida de auditabilidade desde o primeiro deploy, sem caminho de recuperação que não passe por uma reescrita completa da camada de computação.
A2 — cálculo orientado a eventos
Cálculos emitidos como eventos assíncronos e processados por um worker dedicado. Isso desacopla o momento de execução. Mas:
não resolve a reprodutibilidade — a fórmula ainda pode mudar entre a emissão do evento e o momento do reprocessamento;
adiciona complexidade de infraestrutura para resolver um problema de modelagem, não de throughput;
a auditabilidade da versão da fórmula continua ausente sem disciplina explícita;
o worker ainda é um serviço, então as mesmas falhas estruturais se aplicam.
Rejeitada porque ataca o problema errado, throughput, sem tocar no problema real: reprodutibilidade e versionamento.
A3 — registro de fórmulas configurável
Fórmulas registradas em um dicionário central, selecionadas por chave em tempo de execução. Flexibilidade máxima nos pontos de chamada. Mas:
a versão da fórmula passa a ser configuração de tempo de execução, não um contrato de tipo;
erros de chave são detectados em produção, não em tempo de compilação;
a relação entre entradas e saídas perde verificação estática;
"qual fórmula foi executada" se torna uma questão operacional, não uma propriedade do código.
Rejeitada porque troca segurança de tipos por flexibilidade sem benefício real para o domínio, ao mesmo tempo que torna o problema de proveniência mais difícil, não mais fácil.
Trade-offs
quadrantChart
title Mapa de trade-offs da decisao
x-axis Baixo acoplamento --> Alto acoplamento
y-axis Baixo custo de disciplina --> Alto custo de disciplina
quadrant-1 Evitar
quadrant-2 Aceitavel
quadrant-3 Zona de risco
quadrant-4 Alvo
Embutido nos servicos: [0.85, 0.15]
Worker orientado a eventos: [0.55, 0.45]
Registro configuravel: [0.40, 0.40]
Core Engine: [0.10, 0.70]Custos reais da decisão:
Crescimento permanente de linhas de código (LOC). Múltiplas versões imutáveis de cada fórmula aumentam o volume de código permanentemente. Um "ajuste simples" se torna criação de um novo arquivo. Não há atalho.
Extração explícita de entradas é verbosa. O serviço de aplicação precisa extrair valores primitivos das entidades de domínio antes de cada invocação do Core Engine. Isso introduz verbosidade perceptível nos pontos de chamada. DTOs de cálculo dedicados ou mappers bem definidos absorvem esse custo e tornam a extração reutilizável. A disciplina subjacente permanece, mas a repetição não.
A aplicação de fronteiras exige ferramentas. É tecnicamente possível importar o Core Engine a partir de um controller. Regras de linter ou verificações de fronteira de dependência em CI são necessárias para que a fronteira seja efetivamente aplicada, não apenas convencional. Sem essa automação, a fronteira se degrada gradualmente, um "ajuste rápido" de cada vez. Em TypeScript/Node,
dependency-cruiseroueslint-plugin-boundariescobrem bem esse caso.
Benefícios reais da decisão:
Testabilidade trivial. Funções puras são testadas sem mocks.
expect(calculateMonthlyPayment_v1(100_000, 0.045, 360)).toBeCloseTo(506.69)é o teste completo. Sem configuração. Sem desmontagem.Reprodutibilidade permanente. Um resultado gerado com
monthlyPayment: 'v1'em 2024 é recalculado com código idêntico em 2034. Sem suposições.Reuso de custo zero. Backend, frontend, aplicativos móveis e pipelines de ML consomem a mesma biblioteca sem custo de adaptação.
Auditabilidade rastreável. A versão da fórmula está no artefato. A fonte está no repositório. Isso basta.
Consequências
A separação do Core Engine produz uma propriedade sistêmica que não pode ser incorporada retroativamente: a proveniência algorítmica de qualquer número no sistema é sempre recuperável.
Não como melhor esforço. Como um invariante de projeto.
Isso viabiliza:
auditorias técnicas e regulatórias nas quais a computação exata utilizada pode ser demonstrada a partir apenas do artefato persistido;
depuração de divergências entre resultados históricos;
evolução científica controlada — quando a literatura atualiza uma fórmula padrão, a transição é gerenciável sem impacto retroativo sobre resultados históricos;
consumo por agentes de IA que precisam do mesmo Core Engine determinístico utilizado no backend, sem risco de divergência entre o entendimento do modelo e o que o sistema realmente calculou.
A consequência negativa é disciplina permanente: qualquer desenvolvedor que queira "apenas ajustar uma constante" em uma fórmula deve criar uma nova versão. Essa fricção é intencional. A fricção é o mecanismo de proteção, mas só funciona se for ergonômica. Um comando que cria uma nova versão da fórmula em segundos torna o caminho correto o caminho fácil. Sem isso, equipes sob pressão encontram atalhos criativos, e o padrão degrada silenciosamente de dentro para fora.
Insight estrutural
A decisão central não é sobre funções puras. Funções puras são o mecanismo. A decisão é sobre separar responsabilidades epistêmicas: quem sabe como calcular versus quem sabe quando calcular, com quais dados de contexto e o que fazer com o resultado.
Esses são tipos fundamentalmente diferentes de conhecimento. Um é matemático: determinístico e livre de contexto. O outro é operacional: contextual, com estado, preso a ciclo de vida e regras de domínio.
graph TB
subgraph "Responsabilidade matemática"
F["Fórmula
como calcular
—
determinística e livre de contexto por design"]
end
subgraph "Responsabilidade operacional"
O["Serviço de aplicação
quando · com o que · o que fazer com o resultado
—
contextual · com estado · preso ao ciclo de vida"]
end
O -->|"chama com primitivas"| F
F -->|"retorna valor bruto"| O
X1["org_id · user_id · feature flags
plano de assinatura · contexto da requisição"] -.->|"proibido em"| F
X2["constantes de fórmula · modelos científicos
invariantes matemáticos"] -.->|"proibido em"| OQuando você mistura os dois na mesma unidade de código, um contamina o outro. É aqui que a maioria dos sistemas falha.
a fórmula passa a incorporar conhecimento de contexto que nunca deveria ter, como
org_id, flags por plano ou configuração de tenant;o serviço não consegue evoluir seu ciclo de vida sem tocar na fórmula;
os testes de um passam a exigir a infraestrutura do outro.
A separação não é uma otimização de performance. É um alinhamento com a natureza distinta das duas responsabilidades. Sistemas que respeitam essa distinção produzem um tipo de garantia de correção que sistemas que a ignoram nunca conseguem reconstruir posteriormente, apenas aproximar.
Sinais para cenários semelhantes
Este padrão é aplicável sempre que você identificar qualquer um dos sinais abaixo:
Auditabilidade futura é requisito. Se em algum momento você precisar provar que um número foi calculado de uma forma específica, a fórmula precisa ser versionada e isolada hoje. O custo de extraí-la depois não é linear. Ele cresce com o número de pontos de contato que se acumularam em torno do cálculo acoplado.
Múltiplos contextos de execução. Se o mesmo cálculo precisa rodar no backend, frontend e em contextos ainda não antecipados, uma biblioteca é a única opção que não gera duplicação. Um serviço não pode ser biblioteca.
Evolução científica é esperada. Se fórmulas derivam de literatura técnica e vão ser corrigidas conforme o entendimento evolui, o versionamento explícito é o único mecanismo que mantém resultados históricos consistentes enquanto permite a evolução do sistema.
Equipe em crescimento. Quanto maior a equipe, mais rápido uma fronteira implícita é violada. A aplicação estrutural dessas fronteiras por meio de regras de linter ou verificações de dependência em CI é necessária antes do crescimento da equipe, não depois da primeira violação em produção.
Integração downstream com sistemas analíticos. Qualquer modelo de ML, agente de IA, relatório regulatório ou pipeline analítico que consome saídas calculadas e exige garantias sobre como elas foram geradas depende implicitamente da disciplina de versionamento do Core Engine. Essa garantia não pode ser adicionada retroativamente.
O sinal de que esse padrão é necessário não é o tamanho do sistema. É a presença de qualquer requisito de reprodutibilidade. Um sistema pequeno com cálculos auditáveis precisa desse padrão com a mesma urgência que um sistema grande.