DEV Community

Kingsley Onoh
Kingsley Onoh

Posted on Originally published at kingsleyonoh.com

Zero Is Not an Empty Value in Financial Software

What does zero downside mean in a cloud commitment recommendation?

It should mean that the modeled draws produced no loss after commitment cost, unused capacity, upfront amortization, and liquidity penalty. In one optimizer run, it meant something else: the helper that summarized the frontier had no idea how to represent an empty array.

The candidate carried a p95 downside loss of 6,300 cents. The frontier summary reported zero. The ranked policy relaxation also suggested zero. Nothing crashed. The run completed, the JSON was valid, and the wrong value looked unusually good.

That is the kind of defect I worry about in financial software. A loud exception stops a decision. A plausible zero can approve one.

The Value Had Already Changed Meaning

The Cloud Commitment Portfolio Optimizer compares commitment candidates across AWS, Azure, and GCP. Each candidate has expected savings, a commitment amount, utilization percentiles, a p95 downside loss, and a set of binding policy constraints. The worker sorts feasible candidates by expected savings, then by downside.

The same candidate list also feeds a frontier summary. That summary reports the best expected savings and the lowest p95 downside among all candidates. If no candidate satisfies the active policy, the worker persists an infeasible run and suggests which policy limit would need to move.

Those are three different uses of the same number.

For a candidate, zero downside describes an economic result. In a frontier, zero can be the best observed value. In a reducer, zero was being used as “there was no first value.” The type was bigint in all three places, so TypeScript could not tell them apart.

The first implementation reduced the list from 0n. That is a common instinct because it keeps a helper total. It is also correct for sums. For a minimum over positive values, it is a trap:

min(6300, 0) = 0
min(4100, 0) = 0
min(1, 0) = 0
Enter fullscreen mode Exit fullscreen mode

Every positive downside loses to the sentinel. The larger the actual risk, the more confidently the summary still says zero.

Why This Was Easy to Miss

The economic calculation itself was not wrong. In the Zig kernel, downside is derived from net savings and clamped at zero. A no-action case can legitimately produce zero across every economic field. In the TypeScript worker, each candidate calculates its own downside distribution, selects the 95th percentile, checks it against the policy budget, and assigns feasibility.

The defect lived after all of that, inside an aggregate used for presentation and infeasibility guidance.

That location matters. Most tests were naturally aimed at the main path: claim a queued run, read the frozen forecast, load versioned price items, produce a recommendation, store the frontier, and mark the run complete. A successful candidate fixture had its expected saving and p95 downside asserted directly. It passed.

The bug became visible only when the test forced an infeasible objective and then inspected two secondary outputs: lowest_p95_downside_loss_cents in the frontier and max_downside_loss_cents in the ranked relaxation. Both should have carried 6300. Both carried the reducer's sentinel.

I was wrong about where the risky code was. I expected the difficult defects to sit in percentile selection, amortization, or provider-specific eligibility. The failure came from a utility function small enough to read without stopping.

There was another reason it survived ordinary review. Zero looked defensive. An empty array would not throw. The API could still return a stable shape. That defensive default removed a runtime failure by creating a business statement.

The Constraint Was Bigger Than a Helper

I could not replace the value with JavaScript Infinity. The worker uses bigint because money crosses the system as canonical decimal strings and must not pass through floating-point numbers. bigint has no infinity value, and introducing a number sentinel would break the numeric contract.

Throwing on an empty list was possible, but emptiness is not always exceptional. A run can produce no candidates when price coverage does not match the forecast scope. That run should become infeasible with an explanation, not fail as an internal error.

Returning zero for an empty list kept the existing frontier schema stable, but only if the reducer handled non-empty lists from a real value. The current minBigInt() does that:

function minBigInt(left: bigint, right: bigint): bigint;
function minBigInt(values: readonly bigint[]): bigint;
function minBigInt(leftOrValues: bigint | readonly bigint[], right?: bigint): bigint {
  if (typeof leftOrValues === "bigint") return leftOrValues < right! ? leftOrValues : right!;
  const [first, ...rest] = leftOrValues;
  return rest.reduce((minimum, value) => (value < minimum ? value : minimum), first ?? 0n);
}
Enter fullscreen mode Exit fullscreen mode

For a non-empty list, the first candidate becomes the seed. A list containing 6300n now returns 6300n. Two values compare with each other, not with a value invented by the helper.

The empty fallback remains 0n, which is a tradeoff rather than a perfect model. The caller also records candidate_count, so a consumer can distinguish an empty frontier from a risk-free candidate. If I redesigned the contract now, I would make the aggregate nullable when candidate_count is zero. That would move the distinction into the schema instead of asking readers to infer it from two fields.

I considered splitting the overload into two named functions: one for comparing two values and one for aggregating a collection. That would reduce the chance that a caller accidentally reaches the array branch, but it would not settle the empty-state question. The return type still has to say whether no minimum exists. A cleaner contract would return bigint | null for the collection form and force buildFrontier() to serialize that absence deliberately. The current repair stayed smaller because the published summary already pairs its amount with candidate_count.

I kept that redesign out of the repair because it would have changed the API and report contract during a provider-expansion batch. The focused correction restored truth for every non-empty frontier without widening the release. A contract migration deserves its own tests and version decision.

The Test Had to Cross the Boundary

A unit test for minBigInt([6300n]) would prove the arithmetic. It would not prove that the number reached the places finance reads.

The integration fixture creates an active policy with a downside budget of 10 cents and a minimum expected saving of 500 cents. It feeds three forecast points of 1,000 cents against a monthly effective commitment cost of 7,300 cents. The resulting candidate has no positive expected saving and carries 6,300 cents of downside.

The test then runs the real worker against PostgreSQL and the object store. It checks four consequences:

  1. The run ends as infeasible.
  2. No recommendation row is inserted.
  3. The persisted relaxation suggests a downside budget of 6300, not zero.
  4. The frontier artifact reports its lowest p95 downside as 6300.

That path matters more than direct helper coverage. The same value passes through candidate evaluation, aggregation, artifact serialization, database persistence, and the API-facing summary. A regression in any one of those steps breaks the test.

The fixture also checks that the artifacts do not expose credentials, raw rows, stack traces, or internal candidate IDs. Financial correctness and disclosure boundaries belong in the same proof path. A correct risk number in an unsafe artifact is still a failed design.

Zero Has Several Jobs, So It Needs Context

There are legitimate zeros throughout this optimizer. No commitment means zero committed capacity. A profitable draw has zero downside loss. A policy can permit zero minimum expected savings. An empty collection can have a count of zero.

The mistake was letting one zero stand in for all of them.

This shows up beyond minimum functions. An absent price is not a zero price. A forecast with no history is not zero demand. An approval that has not been requested is not a rejection. A retry count of zero says no attempt has happened; it does not say delivery succeeded.

The safest representation follows the business state. Use a number for an amount. Use null for an unavailable aggregate when the contract permits it. Use a status for a workflow state. Use an empty collection when there are no members. Problems start when a convenient primitive is asked to carry two of those meanings at once.

What surprised me was not that a reducer could be wrong. It was how far a six-line helper reached. The wrong seed affected the frontier summary, the policy relaxation, the report a reviewer could quote, and any later analysis built from that artifact. The economic kernel had done its job. The summary layer changed the claim.

The Result

Before the repair, the infeasible fixture stored lowest_p95_downside_loss_cents: "0" and suggested a zero downside budget. After the repair, both fields preserve the candidate's "6300". The worker still returns a valid infeasible result, writes no purchase recommendation, and keeps the artifact free of internal or secret data.

The same worker dispatches five provider and instrument paths through the shared evaluation contract. The focused infeasible test now guards the aggregate that all five use. That is more valuable than five copies of the same helper test because it protects the common financial statement at its persistence boundary.

In risk software, empty and zero are different financial statements.

Top comments (0)