Modeling Annual Commitments with Overage Drawdown
A committed-spend contract says the customer will spend $240,000 over twelve months, consumption is rated against a negotiated rate card, and each rated amount reduces the remaining commitment. It sounds like an account balance and is usually implemented as one — a single mutable number that gets decremented — which is precisely the design that cannot answer “why is the remaining balance $61,400?” six months later. This guide sits under Enterprise Contracts & Quote-to-Cash and covers the movement-log implementation that stays auditable.
You reach this problem the first time a customer’s finance team reconciles their own consumption records against your remaining balance and gets a different number. Without a movement log there is no way to find where the two diverged; with one, the answer is a query that lists every draw with its period, amount, and originating invoice.
Trade-offs
| Representation | Auditability | Idempotency | Concurrency | Reconstructable |
|---|---|---|---|---|
| Mutable balance column | None — only the current value | Requires external guard | Row lock per update | No |
| Movement log, derived sum | Full history per movement | Natural, via a unique key | Append-only, no contention | Yes, from any point |
| Movement log + cached balance | Full, with a fast read | Natural | Append plus a cache write | Yes, cache is disposable |
| Provider-side balance object | Whatever the provider exposes | Provider-defined | Provider-defined | Only via their API |
The cached-balance row is worth taking once the log grows large. Summing a few hundred movements per contract is trivial; summing them on every request across thousands of contracts is not. Keep the cache derived and disposable — recomputable at any moment from the log — so a cache bug is a performance incident rather than a financial one.
Step-by-Step Implementation
1. Fund the commitment
The first movement records what was committed, with a reference to the signed document. Positive amounts add funds; negative amounts consume them.
INSERT INTO commitment_movements
(order_form_id, kind, amount_minor, occurred_at, idempotency_key)
VALUES
($1, 'fund', 24000000, $2, 'fund:' || $1); -- 240,000.00 in minor units
Keying the funding movement on the order form alone means a retried contract-activation job cannot fund twice. Every subsequent movement gets a key built from the tuple that must be unique in business terms.
2. Draw down once per period
The drawdown runs at cycle close, after usage has been rated. Its amount is the rated consumption for the period, capped at the remaining balance so the log never goes negative.
def apply_drawdown(db, order_form_id: str, period_start: date, rated_minor: int) -> dict:
remaining = db.commitment_remaining(order_form_id) # SUM over the log
draw = min(rated_minor, max(remaining, 0))
overage = rated_minor - draw
if draw > 0:
db.insert_movement(
order_form_id=order_form_id, kind="drawdown",
amount_minor=-draw, occurred_at=period_start,
idempotency_key=f"drawdown:{order_form_id}:{period_start.isoformat()}",
)
return {"drawn_minor": draw, "overage_minor": overage}
The min against the remaining balance is what separates drawdown from overage cleanly: the log only ever consumes what exists, and everything beyond that is a separately rated invoice line.
3. Rate overage after exhaustion
The overage rate is a distinct field on the order form. Defaulting it to the committed rate gives away the volume discount on unplanned consumption; defaulting it to list surprises the customer. Whichever the contract says, store it explicitly rather than deriving it.
4. Notify before exhaustion
Consumption thresholds at 50%, 80%, and 100% of the commitment give the account team time to open an expansion conversation before the customer receives an overage invoice. Fire on the crossing edge, and include the projected exhaustion date computed from the trailing three periods — the projection is what makes the notification actionable rather than informational.
def projected_exhaustion(remaining_minor: int, recent_draws: list[int]) -> int | None:
"""Periods until the balance is exhausted, from the trailing average draw."""
if not recent_draws:
return None
avg = sum(recent_draws) / len(recent_draws)
return None if avg <= 0 else int(remaining_minor // avg)
5. Settle at term end
On the term-end date, the remaining balance must be resolved in exactly one of three ways, recorded as a final movement so the log balances to zero or carries forward explicitly.
-- Expiry: the unconsumed balance is forfeited per the contract.
INSERT INTO commitment_movements (order_form_id, kind, amount_minor, occurred_at, idempotency_key)
SELECT $1, 'expiry', -SUM(amount_minor), $2, 'expiry:' || $1
FROM commitment_movements WHERE order_form_id = $1
HAVING SUM(amount_minor) > 0;
Rollover instead funds the successor order form with the remaining amount; a true-up instead raises an invoice for the shortfall between committed and consumed. All three are a single movement plus, in two cases, a document — never a manual adjustment.
Reconciling Against the Customer’s Own Records
Enterprise customers track their consumption independently, and the reconciliation conversation happens at least annually. Three practices make it short.
Publish the movement log, not the balance. A customer-facing view listing each period, the usage that drove it, and the resulting draw lets their team match line for line. A single “remaining” figure invites a dispute with no shared evidence.
Use the same period boundaries the invoice uses. If drawdowns are dated to the period start but usage is aggregated to a period ending three days later because of a late-event grace window, the two will not tie out and the difference will look like an error. Date the movement to the same instant the invoice covers, and record the grace window’s effect as its own adjustment when it matters — the same discipline described in handling late-arriving usage events applied to the commitment.
Reconcile in the contract currency only. Converting a commitment for reporting is fine; converting it for the customer-facing balance introduces a rate question into a conversation that should be about consumption.
Which branch applies should be a stored field rather than an operational judgement, because the person running the term-end job in eighteen months will not have read the contract. Store it as an enumerated column on the order form, default it to whichever your standard terms specify, and require an explicit value whenever a negotiated contract deviates. A commitment that reaches its term with an unset settlement rule stalls the job, which is the correct outcome — far better than a default that quietly forfeits a customer’s money or quietly gives it away.
Verification & Testing
The idempotency test is the one that protects real money: apply the same period’s drawdown twice and assert exactly one movement exists and the balance moved once. Then apply drawdowns out of order — period three before period two — and assert the final balance is identical, because a sum over a set must be order-independent. If it is not, someone has introduced a running total.
Test the exhaustion boundary precisely. With a remaining balance of exactly the period’s rated amount, assert the draw consumes it entirely and produces zero overage; with one minor unit less, assert one minor unit of overage. Off-by-one errors here produce invoices that are almost right, which are harder to spot than invoices that are obviously wrong.
Finally, test each term-end path against a fixture contract: expiry zeroes the balance, rollover funds the successor with the exact remaining amount, and true-up raises an invoice for the difference between committed and consumed. Assert in all three cases that re-running the term-end job changes nothing.
Where the Commitment Sits in Accounting
The movement log is a billing artefact; the accounting treatment sits alongside it and follows different rules, and conflating the two produces month-end surprises.
A prepaid commitment is cash received for services not yet delivered, so it is a liability — deferred revenue — from the moment payment lands. Each drawdown releases a portion of that liability into recognised revenue, in the period the consumption occurred. This means the drawdown movement has an accounting counterpart, and the two must agree: the sum of drawdowns in a period should equal the deferred-revenue release for that contract in the same period. When they diverge, the cause is almost always a drawdown dated to a different period than the usage it represents.
A non-prepaid commitment is only a promise to spend. No cash has moved, nothing is deferred, and nothing is recognised until an invoice is issued for actual consumption. The balance is still worth tracking — it drives the overage boundary and the renewal conversation — but it must not appear on the balance sheet, and a system that funds the movement log identically for both types needs a flag that keeps the accounting integration honest.
Forfeited commitment at term end is the case worth getting explicitly right. When an unconsumed prepaid balance expires, the liability has been extinguished without the service being delivered, and the amount is generally recognised as revenue (“breakage”) at that point. It is real revenue with an unusual shape, and it typically requires a separate account so the finance team can see it distinctly rather than having it blend into ordinary consumption. Recording the expiry as its own movement kind is what makes that separation possible without a manual journal entry.
The practical instruction is to give each movement kind a defined accounting posting and to reconcile the log against the ledger monthly, exactly as described in reconciling MRR against recognised revenue. Any period where the two disagree points at a dating problem in the drawdown, which is far easier to fix in the month it happens than during an audit a year later.
Gotchas & Production Pitfalls
- Drawdown applied before rating completes. Closing the commitment period before the usage pipeline has drained produces a draw that is too small and an adjustment later. Gate the drawdown on the same watermark the invoice uses.
- Negative balances from concurrent draws. Two jobs drawing simultaneously can both read the same remaining amount. Serialise per contract, or enforce non-negativity with a constraint that recomputes the sum.
- Overage rate missing from the contract. It will default to something, and the default will be wrong. Make it a required field on the order form.
- Mid-term commitment increases modelled as a new contract. An expansion is another
fundmovement on the same order form, not a second contract; splitting it breaks the drawdown and the customer’s view of remaining balance. - Credits applied to overage but not to the commitment. A goodwill credit against an overage invoice must not also restore commitment balance, or the customer is compensated twice. Decide which side the credit lands on and record it as a movement only if it truly restores commitment.
Frequently Asked Questions
Should the commitment be prepaid or invoiced as consumed? Both exist. A prepaid commitment funds the balance on payment and sits in deferred revenue; a non-prepaid commitment funds the balance at signing and invoices as consumption occurs. The movement log is identical; only the accounting entry differs.
What happens if usage in a period exceeds the entire remaining commitment? The draw takes the balance to zero and the remainder is overage on that period’s invoice. This is the normal terminal case, not an error, and the notification ladder should have warned the account team well before it.
Can a commitment cover multiple products? Yes, and that is usually the point. Rate each product at its contracted rate and draw the total from one balance. Keep per-product detail in the usage records so the customer can see what consumed the commitment.
How do refunds interact with drawdown? A refund of an overage invoice is an ordinary credit note. A refund that unwinds consumption should restore the commitment with a positive movement referencing the original drawdown, so the log shows the reversal rather than a balance that inexplicably increases.