In domain-dense systems, where the core value of the software is the correctness of scientific, technical, or regulatory calculations, there is a quiet architectural trap: computation logic accumulates inside application services.
It starts small. An inline estimation function in a use case. A calculation helper imported by a controller. A service class that "also does the numbers." Each addition seems reasonable at the time. The problem only surfaces when someone asks a simple, inevitable question:
"Which formula produced this number, and can I reproduce it exactly right now?"
If the answer requires git blame, context tracing, or guesswork, the system is already broken.
This article explains why we isolate deterministic computation into a self-contained internal library, the Core Engine, and the architectural constraints that drove that decision.
The cost of scattered calculations
When calculations are scattered across application services, they inevitably produce four structural failures.
1. Formula duplication and drift. Over time, the same formula appears in multiple places with subtle variations: slightly different constants, divergent edge-case handling, or inconsistent rounding. Each version "looks right" to whoever wrote it, but there is no canonical source of truth.
2. Historical non-reproducibility. If a result calculated in 2026 needs to be audited in 2028, the question "what was the formula when this was generated?" often has no traceable answer. The service changed. The formula was "fixed." The original result is unrecoverable.
3. Structurally impossible auditability. Systems with regulatory or technical obligations must prove that a given output was produced by a known, approved formula. If that formula is entangled with I/O, lifecycle logic, feature flags, and business rules, isolating it for an audit is not a tooling problem. It is a structural one.
4. Degraded testability. You end up needing to boot up the whole service, complete with database mocks, just to validate a math equation. That alone is an architectural smell.
The constraints shaping our design
To solve this, we had to address several non-negotiable forces:
Reproducibility as a contract: A result generated today must be calculable using the exact same algorithm years later. Old formula versions must never be modified retroactively.
Mandatory provenance: The domain requires that any output carry explicit algorithmic provenance: not as optional metadata, but as a mandatory persisted field in every calculation artifact.
Multiple execution contexts: The same computations need to run in the backend API, frontend, mobile app, and AI/ML pipelines. Embedding them in a single service forces duplication in downstream consumers.
Separation of responsibilities: Applications decide when to calculate, with what inputs, and where to persist the result. The formula decides only how to calculate.
The solution: a deterministic Core Engine
We concluded that all deterministic computation logic must live in a self-contained internal library, the Core Engine, governed by strict, non-negotiable rules.
Rule 1: absolute purity
Every function in the Core Engine is pure. Given the same inputs, it always returns the same output. There are no side effects, no I/O, no database access, and no feature flag reads. The Core Engine has no concept of an organization, a user, a subscription plan, or its execution context.
graph LR
subgraph "Application Layer"
AS[Application Service]
REPO[(Repository)]
CONTEXT["org_id / user_id / flags"]
end
subgraph "Core Engine"
direction TB
FORMULA["calculateMonthlyPayment_v1(principal, rate, term)"]
NOTE["Pure function
No I/O · No context · No state"]
end
AS -->|primitives only| FORMULA
FORMULA -->|number| AS
AS --> REPO
CONTEXT -.->|forbidden| FORMULARule 2: primitive inputs only
The Core Engine accepts only primitives or flat value types. Rich domain entities like LoanApplicationEntity are strictly forbidden as inputs. Before invoking the Core Engine, the application service must extract the required primitive values. This ensures the Core Engine never acquires implicit dependencies on evolving domain contracts.
// Correct — primitives extracted before calling the Core Engine
const payment = calculateMonthlyPayment_v1(
draft.principalAmount,
draft.annualRate,
draft.termMonths
);
// Forbidden — passing a rich domain entity
const payment = calculateMonthlyPayment_v1(loanApplication);Rule 3: explicit, immutable versioning
Every formula has an explicit version encoded directly in its name, such as calculateMonthlyPayment_v1 and calculateMonthlyPayment_v2, each living in its own file.
Old versions are mathematically immutable. Fixing a bug that changes a calculation result means creating a new version, never overwriting the existing one.
Note: immutability applies to the mathematical logic. If a frozen version depends on an external library that receives a security CVE patch, updating that dependency is permitted, provided the formula's computational behavior remains byte-for-byte identical. Changing constants, coefficients, or algorithmic steps under an existing version identifier is strictly prohibited.
timeline
title Formula version lifecycle
section Monthly Payment
2024-01 : calculateMonthlyPayment_v1 released
2025-03 : calculateMonthlyPayment_v2 released (variable-rate support)
: v1 frozen — still executable, unchanged
section Risk Score
2024-01 : calculateRiskScore_v1 released
2024-09 : calculateRiskScore_v2 released (updated actuarial table)
: v1 frozen — parallel availability for historical replayRule 4: exceptions for mathematical invariants only
The Core Engine throws exceptions only for violations of mathematical invariants: negative principal, zero term, division by zero, or NaN inputs. It does not throw business exceptions. It does not return null or -1 as error signals. The distinction between mathematical failure and domain failure is explicit and typed:
class EngineInvariantError extends Error {
constructor(message: string) {
super(message);
this.name = 'EngineInvariantError';
}
}
// Usage — the caller knows exactly what kind of failure to expect
try {
const payment = calculateMonthlyPayment_v1(principal, rate, term);
} catch (e) {
if (e instanceof EngineInvariantError) {
// Math is broken — the input data itself is structurally impossible
}
// All other errors propagate normally
}Rule 5: provenance persistence is the service's job
The Core Engine computes and returns. The application service persists the result with the formula version used as a mandatory field in every persisted artifact:
// Application service — orchestrates, persists, owns provenance
async function priceLoan(application: LoanApplication): Promise<PricingSnapshot> {
// 1. Extract primitives from domain entities
const principal = application.requestedAmount;
const rate = application.offeredAnnualRate;
const term = application.termMonths;
// 2. Invoke Core Engine — pure computation, no context
const monthlyPayment = calculateMonthlyPayment_v1(principal, rate, term);
const riskScore = calculateRiskScore_v2(application.creditScore, application.debtRatio);
const effectiveRate = calculateEffectiveRate_v1(rate, application.fees, term);
// 3. Persist result with explicit formula provenance
return snapshotRepository.save({
application_id: application.id,
outputs: { monthlyPayment, riskScore, effectiveRate },
metadata: {
formula_versions: {
// mandatory — this is what makes replay possible
monthlyPayment: 'v1',
riskScore: 'v2',
effectiveRate: 'v1',
},
},
});
}Without this persistence, historical reproducibility does not exist. The formula version would be inferred implicitly, which invalidates any claim of auditability.
Alternatives considered
A1 — calculations embedded directly in use cases or services
The most natural alternative, and the most dangerous one. Computation lives alongside lifecycle logic, business validation, and persistence. Maximum initial convenience. But:
without explicit versioning, the first "fix" immediately breaks historical reproducibility;
unit testing the formula requires mocking repositories and execution context;
reuse in downstream contexts like mobile or AI pipelines requires duplication or a late, costly extraction.
Rejected because it creates auditability debt from the first deploy, with no path to recover it without a full rewrite of the computation layer.
A2 — event-driven calculation
Calculations emitted as asynchronous events and processed by a dedicated worker. It decouples execution timing. But:
it does not solve reproducibility — the formula can still change between the event being emitted and the moment of reprocessing;
it adds infrastructure complexity to solve a modeling problem, not a throughput problem;
formula version auditability is still absent without explicit discipline;
the worker is still a service, so the same structural failures apply.
Rejected because it addresses the wrong problem, throughput, without touching the actual problem: reproducibility and versioning.
A3 — configurable formula registry
Formulas registered in a central dictionary, selected by key at runtime. Maximum flexibility at call sites. But:
the formula version becomes runtime configuration, not a type contract;
key errors are discovered in production, not at compile time;
the relationship between inputs and outputs loses static verification;
"which formula ran" becomes an operational question, not a code property.
Rejected because it trades type safety for flexibility with no real benefit to the domain, while making the provenance problem harder, not easier.
Trade-offs
quadrantChart
title Decision trade-off map
x-axis Low coupling --> High coupling
y-axis Low discipline cost --> High discipline cost
quadrant-1 Avoid
quadrant-2 Acceptable
quadrant-3 Risk zone
quadrant-4 Target
Embedded in services: [0.85, 0.15]
Event-driven worker: [0.55, 0.45]
Configurable registry: [0.40, 0.40]
Core Engine: [0.10, 0.70]Real costs of the decision:
Permanent LOC growth. Multiple immutable versions of each formula increase the code volume permanently. A "simple fix" becomes creating a new file. There is no shortcut.
Explicit input extraction is verbose. The application service must pull primitive values out of domain entities before every Core Engine invocation. This is noticeable call-site verbosity. Dedicated calculation DTOs or well-defined mappers absorb this cost and make the extraction reusable. The underlying discipline remains, but the repetition does not.
Boundary enforcement requires tooling. It is technically possible to import the Core Engine from a controller. A linter rule or CI dependency boundary check is required to make the boundary enforced, not just conventional. Without that automation, the boundary degrades gradually, one "quick fix" at a time. In TypeScript/Node,
dependency-cruiseroreslint-plugin-boundariescover this well.
Real benefits of the decision:
Trivial testability. Pure functions are tested with zero mocks.
expect(calculateMonthlyPayment_v1(100_000, 0.045, 360)).toBeCloseTo(506.69)is the entire test. No setup. No teardown.Permanent reproducibility. A result generated with
monthlyPayment: 'v1'in 2024 is recalculated with identical code in 2034. No assumptions required.Zero-cost reuse. Backend, frontend, mobile, and ML pipelines consume the same library with zero adaptation cost.
Traceable auditability. The formula version is in the artifact. The source is in the repository. That is enough.
Consequences
The separation of the Core Engine produces a systemic property that cannot be retrofitted: the algorithmic provenance of any number in the system is always recoverable.
Not as best-effort. As a design invariant.
This enables:
technical and regulatory audits where the exact computation used is demonstrable from the persisted artifact alone;
debugging of divergences between historical results;
controlled scientific evolution — when the literature updates a standard formula, the transition is manageable without retroactive impact on historical records;
consumption by AI agents that need the same deterministic Core Engine the backend uses, with no risk of drift between the model's understanding and what the system actually computed.
The negative consequence is permanent discipline: any developer who wants to "just adjust a constant" in a formula must create a new version. This is intentional friction. The friction is the protection, but only if it is ergonomic. A command that scaffolds a new formula version in seconds makes the correct path the easy path. Without that, teams under pressure find creative workarounds, and the pattern degrades silently from the inside.
Structural insight
The core decision is not about pure functions. Pure functions are the mechanism. The decision is about separating epistemic responsibilities: who knows how to compute versus who knows when to compute, with what contextual data, and what to do with the result.
These are fundamentally different kinds of knowledge. One is mathematical: deterministic and context-free. The other is operational — contextual, stateful, bound to lifecycle and domain rules.
graph TB
subgraph "Mathematical responsibility"
F["Formula
how to compute
—
deterministic and context-free by design"]
end
subgraph "Operational responsibility"
O["Application Service
when · with what · what to do with result
—
contextual · stateful · lifecycle-bound"]
end
O -->|"calls with primitives"| F
F -->|"returns raw value"| O
X1["org_id · user_id · feature flags
subscription plan · request context"] -.->|"forbidden in"| F
X2["formula constants · scientific models
mathematical invariants"] -.->|"forbidden in"| OWhen you mix the two in the same code unit, each contaminates the other. This is where most systems fail.
the formula acquires context knowledge it should never have, such as
org_id, plan-level flags, or tenant configuration;the service cannot evolve its lifecycle without touching the formula;
tests for one require infrastructure setup of the other.
The separation is not a performance optimization. It is an alignment with the different nature of the two responsibilities. Systems that respect this distinction produce a kind of correctness guarantee that systems which ignore it can never reconstruct after the fact, only approximate.
Signals for similar situations
This pattern is applicable whenever you identify any of the following:
Future auditability is a requirement. If at any point you will need to prove that a number was calculated a specific way, the formula must be versioned and isolated today. The cost of extracting it later is not linear. It scales with the number of touch points that have grown around the entangled calculation.
Multiple execution contexts. If the same computation needs to run on the backend, frontend, and contexts not yet anticipated, a library is the only option that does not produce duplication. A service cannot be a library.
Scientific evolution is expected. If formulas are derived from technical literature and will be corrected as understanding evolves, explicit versioning is the only mechanism that keeps historical results consistent while allowing the system to move forward.
Growing team. The larger the team, the faster an implicit boundary gets violated. Structural enforcement through linter rules or CI dependency checks is necessary before the team grows, not after the first violation is discovered in a production incident.
Downstream integration with analytical systems. Any ML model, AI agent, regulatory report, or analytics pipeline that consumes computed outputs and needs guarantees about how they were generated depends implicitly on the Core Engine's versioning discipline. The guarantee cannot be bolted on retroactively.
The signal that this pattern is necessary is not system size. It is the presence of any reproducibility requirement. A small system with auditable calculations needs this pattern as urgently as a large one.