Design by Contract — Optimize & Reconcile¶
Contracts make code correct. Naively checked, they also make it slow. This file reconciles the two: 12 scenarios where a precondition, postcondition, or invariant check sits on a hot path or costs more than the work it guards. For each: the scenario, a measurement or back-of-envelope cost, and a principled resolution that keeps the contract checked where it matters and free where it doesn't. The governing idea — a contract is a statement about what must be true for the program to be correct, so it can be verified offline, at a boundary, or by the type system, instead of re-verified on every call in production.
The non-negotiable line, repeated because it is the one mistake that turns a performance win into a CVE: input validation is never a contract you may strip. A contract guards against programmer error (a bug in your own code that should never ship); validation guards against attacker/user error (data crossing a trust boundary that absolutely will be malicious). Strip the former in production; keep the latter always. Confusing the two is how assert authenticated disappears under python -O.
Table of Contents¶
- Precondition check on a 50M-calls/sec hot path (Java
-ea) - O(n log n) postcondition: "the array is sorted" (Python
-O) - Stripping a check that is actually validation — the security trap
- Caller and callee both check the same precondition (Go)
- Class invariant re-verified on every mutator (Java)
- Assertion levels: cheap always, expensive only in
debug(Go build tags) - Postcondition allocates to snapshot "old" state (Java)
- Property-based tests verify the contract offline at zero prod cost (Python)
- Compile-time contracts: types as zero-runtime-cost preconditions (Go)
- Boundary check once vs per-method invariant in a loop (Java)
- Expensive precondition lifted out of a tight loop (Go)
assertwith a side effect that vanishes under-O(Python)
Scenario 1 — Precondition check on a 50M-calls/sec hot path (Java)¶
public final class RingBuffer {
private final long[] slots;
public long get(int index) {
if (index < 0 || index >= slots.length) // precondition
throw new IndexOutOfBoundsException(index);
return slots[index];
}
}
Scenario: get is called 50M times/sec inside a market-data decoder. index is always produced by an internal mask (pos & (size - 1)) that mathematically cannot go out of range — the bound is an internal contract, not external input.
Measurement: the explicit branch costs ~0.3–1 ns per call when well-predicted. At 50M calls/sec that is 15–50 ms/sec of pure check overhead — 1.5–5% of a core, burned re-proving something the caller already guarantees. Worse, the redundant branch is also checked by the JVM's own array-access bounds check, so you pay twice; the manual check can defeat the JIT's bounds-check elimination.
Resolution
Express the bound as a Java `assert`, which is stripped unless the JVM runs with `-ea`: - **Test/staging:** run with `-ea`. The assertion fires with a precise message the instant the masking logic is wrong. - **Production:** run without `-ea`. The check compiles to nothing; the JIT is free to eliminate the *array* bounds check via loop-range analysis. Zero overhead, and a genuine bug still throws `ArrayIndexOutOfBoundsException` rather than corrupting memory (the JVM guarantees memory safety regardless). This is the canonical DbC/performance reconciliation: the contract is *documented and enforced in test*, *free in prod*, and the language's own safety net catches the unthinkable. Do **not** do this if `index` comes from a network frame — then it is validation, see Scenario 3.Scenario 2 — An O(n log n) postcondition on an O(n) routine (Python)¶
def merge_sorted(a: list[int], b: list[int]) -> list[int]:
result = _two_pointer_merge(a, b) # O(n) — the actual work
assert result == sorted(result), "postcondition: output is sorted"
return result
Scenario: merge_sorted is the inner step of a streaming merge over millions of small lists. The routine itself is O(n). The postcondition result == sorted(result) is O(n log n) — it does more work than the function it checks, and it allocates a full second copy.
Measurement: for n = 1000, the merge is ~1000 ops; sorted(result) is ~10,000 ops plus a 1000-element allocation. The check is ~10× the cost of the work, run on every call. A 30s job becomes a 5-minute job.
Resolution
Two moves. First, make the postcondition O(n) — *checking* sortedness is linear, even though *producing* a sort is not: Second, gate it so it vanishes in production. Python's `assert` is removed when the interpreter runs with `-O`: Now the contract is checked on every call in CI (where correctness matters and throughput doesn't), and costs nothing in prod. The general rule: **a checking algorithm is usually cheaper than the producing algorithm** (verify-sorted is O(n) vs sort O(n log n); verify-a-solution is often O(n) vs solve O(2ⁿ)). Reach for the cheap verifier, then gate even that.Scenario 3 — Stripping a check that is actually validation (the security trap)¶
def transfer(from_acct, to_acct, amount_cents: int):
assert amount_cents > 0, "amount must be positive"
assert from_acct.balance_cents >= amount_cents, "insufficient funds"
from_acct.balance_cents -= amount_cents
to_acct.balance_cents += amount_cents
Scenario: amount_cents arrives from an HTTP request body. A profiler shows the function is hot, so an engineer "optimizes" by deploying with python -O to strip the asserts. Both checks disappear. An attacker posts amount_cents = -1_000_000 and credits their own account.
Measurement: the checks cost ~50 ns. The cost of removing them is unbounded financial loss. This is not a performance trade-off; it is a vulnerability introduced under the banner of performance.
Resolution
Draw the bright line. **Validation of data crossing a trust boundary must be a real, unconditional statement — never `assert`, never behind a build tag, never `-ea`/`-O`-gated.**def transfer(from_acct, to_acct, amount_cents: int):
if amount_cents <= 0:
raise ValueError("amount must be positive") # validation — always runs
if from_acct.balance_cents < amount_cents:
raise InsufficientFunds() # validation — always runs
# Now an internal contract is legitimately assertable:
assert from_acct.balance_cents >= amount_cents # re-stated invariant, debug-only
from_acct.balance_cents -= amount_cents
to_acct.balance_cents += amount_cents
Scenario 4 — Caller and callee both check the same precondition (Go)¶
func ProcessBatch(items []Item) {
if items == nil {
panic("items must not be nil")
}
for _, it := range items {
normalize(&it)
}
}
func normalize(it *Item) {
if it == nil { // redundant: ProcessBatch already ruled out nil items
panic("item must not be nil")
}
it.Name = strings.TrimSpace(it.Name)
}
Scenario: normalize re-checks it != nil on every iteration, but its only caller (ProcessBatch) iterates a slice it already proved non-nil, and slice elements are never nil pointers here. The check is pure redundancy multiplied by batch size.
Measurement: for a 10K-item batch the inner nil check runs 10K times per batch, all guaranteed to pass. Beyond the cycles, it muddies the contract: a reader can't tell whether normalize is meant to be called with nil (defensive) or whether nil is a bug (contract). Double-checking obscures ownership.
Resolution
Assign the precondition to **one** party and document the division. In DbC the *caller* satisfies the precondition; the callee may assume it. Make that explicit and remove the redundant guard:// normalize trims the item's name in place.
// Precondition: it != nil (caller's responsibility).
func normalize(it *Item) {
it.Name = strings.TrimSpace(it.Name)
}
Scenario 5 — Class invariant re-verified on every mutator (Java)¶
final class Portfolio {
private final Map<String, Long> shares = new HashMap<>();
private long cashCents;
void buy(String sym, long qty, long priceCents) {
cashCents -= qty * priceCents;
shares.merge(sym, qty, Long::sum);
checkInvariant(); // runs after every mutation
}
private void checkInvariant() {
long marketValue = shares.entrySet().stream()
.mapToLong(e -> e.getValue() * priceOf(e.getKey())) // priceOf hits a cache/feed
.sum();
if (cashCents + marketValue < 0)
throw new IllegalStateException("portfolio underwater");
}
}
Scenario: checkInvariant walks every holding and looks up a price for each — O(positions) with a (cached) feed call per symbol. It runs after every buy/sell. During a bulk rebalance of 500 trades over a 200-position portfolio that is 500 × 200 = 100K price lookups, almost all redundant.
Measurement: at ~200 ns per cached priceOf, one checkInvariant is ~40 µs; 500 of them is ~20 ms of pure invariant-checking on an operation whose useful work is microseconds. The check is O(n) and runs O(m) times → O(n·m).
Resolution
An invariant must hold at every *observable* boundary — not after every *internal* step. Check it **once** when control returns to the client, and gate it as an assertion:void rebalance(List<Trade> trades) {
for (Trade t : trades) {
applyRaw(t); // mutates without per-step invariant check
}
assert invariantHolds() : "portfolio underwater after rebalance"; // once, -ea only
}
private void applyRaw(Trade t) {
cashCents -= t.qty() * t.priceCents();
shares.merge(t.sym(), t.qty(), Long::sum);
}
Scenario 6 — Assertion levels: cheap always, expensive only in debug (Go)¶
func (t *BTree) Insert(key, val []byte) {
t.insertInternal(key, val)
if err := t.verifyBalanced(); err != nil { // O(n) full-tree walk, every insert
panic(err)
}
}
Scenario: verifyBalanced walks the entire B-tree checking node fill factors and key ordering — a beautiful, thorough invariant. Run on every insert it makes insertion O(n) instead of O(log n), turning a 1M-row bulk load from seconds into minutes.
Measurement: insert work is ~log₂(1M) ≈ 20 comparisons; verifyBalanced is ~1M node visits. The check is ~50,000× the cost of the operation it guards. Even in CI a full-walk-per-insert can make the test suite unusably slow.
Resolution
Introduce **assertion levels** — cheap checks always on, expensive checks behind a tunable. Some checks are too costly even for normal test runs and belong to a "paranoid" tier exercised only by dedicated invariant tests or fuzzing.// AssertLevel: 0 = off (prod), 1 = cheap contracts (test), 2 = expensive invariants (fuzz/CI-nightly)
var AssertLevel = 0
func (t *BTree) Insert(key, val []byte) {
if AssertLevel >= 1 && key == nil {
panic("contract: key must not be nil") // O(1) — affordable in every test
}
t.insertInternal(key, val)
if AssertLevel >= 2 {
if err := t.verifyBalanced(); err != nil { // O(n) — only paranoid tier
panic(err)
}
}
}
Scenario 7 — Postcondition allocates to snapshot "old" state (Java)¶
void deposit(long amountCents) {
long oldBalance = balanceCents; // snapshot for postcondition
List<Txn> oldLog = new ArrayList<>(this.log); // deep-ish copy, every call
balanceCents += amountCents;
log.add(new Txn(amountCents));
assert balanceCents == oldBalance + amountCents;
assert log.size() == oldLog.size() + 1;
}
Scenario: the postcondition compares post-state to pre-state, so the code snapshots old values up front — including copying the entire transaction log. The copy happens unconditionally, even in production where the assert is stripped, because the snapshot is plain code, not part of the assert.
Measurement: copying a 10K-entry log on every deposit is ~10K-element allocation + copy per call. With -da (assertions disabled) the comparison is gone but the expensive snapshot still runs — you pay the full setup cost for a check that no longer executes. This is the subtle trap: only the assert expression is stripped; surrounding setup is not.
Resolution
Push the entire `old`-state capture inside an `assert` expression (or a method called only from one) so the JVM elides it together with the check when assertions are off:void deposit(long amountCents) {
assert checkDeposit(amountCents); // whole thing vanishes under -da
balanceCents += amountCents;
log.add(new Txn(amountCents));
assert balanceCents >= 0; // cheap O(1) post-check stays
}
// Returns true or throws; only invoked when -ea. The expensive snapshot
// lives here, so it is never allocated in production.
private boolean checkDeposit(long amountCents) {
int oldSize = log.size(); // capture cheap derived facts, not deep copies
// ... record what the postcondition needs ...
return true;
}
Scenario 8 — Move contract verification offline into property-based tests (Python)¶
def encode(frame: Frame) -> bytes: ...
def decode(data: bytes) -> Frame: ...
# Tempting "contract": verify the round-trip on every encode in prod.
def encode_checked(frame: Frame) -> bytes:
data = encode(frame)
assert decode(data) == frame, "round-trip postcondition" # decode on every encode!
return data
Scenario: the real contract is "decode is the inverse of encode." Verifying it inline means decoding every frame you encode — doubling work on the wire-serialization hot path even when the assert is -O-stripped, if the decode call sits outside the assert.
Measurement: inline round-trip verification at minimum doubles serialization cost (encode + decode per frame) and allocates the decoded frame. On a 100K-frames/sec link that is 100K wasted decodes/sec.
Resolution
The round-trip is a *universal property*, not a per-call obligation. Verify it **once, offline, over thousands of generated inputs** with property-based testing — then the production path carries zero verification cost: Production `encode` stays a bare, fast `encode(frame)` with no inline check. The contract is *still rigorously enforced* — just at CI time, against a far wider range of inputs than any single production call would exercise, at **zero production cost**. Property-based testing is the natural home for postconditions and invariants that are too expensive to check per call: round-trips (`decode∘encode == id`), idempotence (`f(f(x)) == f(x)`), commutativity, and "the output is sorted/balanced/conserved." See [`../../testing` property-based testing](../../refactoring/README.md) patterns and the `property-based-testing` discipline. The mental shift: **a contract is a specification; a property test is that specification executed against a generator — same statement, paid offline.**Scenario 9 — Compile-time contracts: types as zero-runtime-cost preconditions (Go)¶
// Runtime precondition, checked on every call.
func Withdraw(amountCents int64) error {
if amountCents < 0 {
return errors.New("amount must be non-negative") // runtime check, every call
}
...
}
Scenario: every numeric argument that "must be non-negative," "must be one of {OPEN, CLOSED}," or "must be a valid currency code" is re-checked at runtime on every call across the codebase. Thousands of such guards, each a branch, each a place the contract can be forgotten.
Measurement: each guard is cheap individually (~1 ns) but they are everywhere and, more importantly, they are checked at the wrong time — at call time, possibly in production, repeatedly — when the fact could be guaranteed once, at compile time, for free.
Resolution
Encode the precondition in a **type**, so the compiler proves it and the runtime never checks it. A value that cannot be constructed in an invalid state needs no per-call guard.// A constrained constructor is the single validation point.
type Amount struct{ cents int64 } // unexported field — cannot be built directly
func NewAmount(cents int64) (Amount, error) {
if cents < 0 {
return Amount{}, errors.New("amount must be non-negative") // validated ONCE, at the boundary
}
return Amount{cents}, nil
}
// Now Withdraw needs no check — the type IS the precondition.
func Withdraw(a Amount) { ... } // a.cents is non-negative by construction
Scenario 10 — Boundary check once vs per-method invariant in a loop (Java)¶
class Matrix {
private final double[][] rows;
boolean isSquare() { // O(rows) check
for (double[] r : rows)
if (r.length != rows.length) return false;
return true;
}
double determinant() {
if (!isSquare()) throw new IllegalStateException(); // re-checked here
...
}
Matrix multiply(Matrix o) {
if (!isSquare() || !o.isSquare()) throw new IllegalStateException(); // and here
...
}
}
Scenario: "the matrix is square" is a class invariant, yet every operation re-derives it with an O(rows) scan. In a numerical loop calling multiply 1M times, the squareness scan dominates.
Measurement: for a 1000×1000 matrix, isSquare() is a 1000-iteration scan. Called twice per multiply, over 1M multiplies, that is 2 billion redundant length comparisons — checking a property that cannot change after construction.
Resolution
If a property is established at construction and the object is immutable, it is an **invariant to verify once at the boundary**, not a precondition to re-check per method:final class Matrix {
private final double[][] rows;
private final boolean square; // computed once
Matrix(double[][] rows) {
this.rows = deepCopy(rows);
this.square = computeSquare(this.rows); // O(rows) — ONCE
}
double determinant() {
assert square : "invariant: matrix is square"; // O(1), -ea only
...
}
}
Scenario 11 — Expensive precondition lifted out of a tight loop (Go)¶
func RenderAll(tmpl *Template, rows []Row) []string {
out := make([]string, 0, len(rows))
for _, r := range rows {
if !tmpl.IsValid() { // O(template size) parse-check, EVERY iteration
panic("contract: template must be valid")
}
out = append(out, tmpl.Render(r))
}
return out
}
Scenario: tmpl.IsValid() re-parses and validates the template (an expensive O(template) precondition of Render). It is invariant across the loop — the template doesn't change — yet it runs once per row.
Measurement: for a 5KB template validated in ~20 µs, over 1M rows, that's 20 seconds of pure re-validation guarding a render that takes 2 µs. The check is 10× the work and 100% redundant after the first iteration.
Resolution
Hoist the loop-invariant precondition above the loop — check it **once** before iterating, exactly as you'd hoist any invariant computation:func RenderAll(tmpl *Template, rows []Row) []string {
if !tmpl.IsValid() { // ONCE — caller's precondition, verified at entry
panic("contract: template must be valid")
}
out := make([]string, 0, len(rows))
for _, r := range rows {
out = append(out, tmpl.Render(r)) // tmpl proven valid; no per-row check
}
return out
}
Scenario 12 — assert with a side effect that vanishes under -O (Python)¶
def commit(self, record):
assert self._log.append(record) is None # "always true" — but the append is the work!
self._flush()
Scenario: an engineer folded a real mutation (self._log.append(record)) into an assert expression — perhaps to "save a line." list.append returns None, so ... is None is always true and the assert passes in CI. But python -O strips the entire assert statement, including the append. In production, records are silently never logged.
Measurement: in CI (asserts on) everything works; tests pass. In production (-O) the append never runs — a 100% data-loss bug that no test catches because the test runs with assertions enabled. This is the dual of Scenario 7: there, useful work leaked outside the assert and ran needlessly; here, useful work hid inside the assert and vanished entirely.
Resolution
**Never put a side effect, mutation, or required computation inside an `assert`.** An assert must be a pure boolean predicate over existing state; stripping it must change *nothing* but the check itself. This is the contract programmer's prime directive for assertions, and it dovetails with Scenario 3: an `assert` is only safe to strip if removing it leaves behavior identical in every respect except verification. To enforce it mechanically: - Lint with rules that flag mutating calls inside `assert` (pylint `assert-on-tuple` and custom AST checks; Go vet has no `assert` so the build-tag pattern sidesteps this). - In code review, treat any `assert someCall()` where `someCall` is not obviously pure as a defect. - Test the production configuration in CI too: run a smoke suite under `python -O` so a stripped-assert behavior change surfaces before release. The reconciliation: the performance win of strippable assertions is real and large, but it is *only* sound when assertions are pure. Side effects, validation (Scenario 3), and required work must live in unconditional code; assertions carry verification and nothing else.Rules of Thumb¶
- Distinguish contract from validation before you optimize anything. A contract guards against your bugs (strippable:
assert,-ea,-O, build tags). Validation guards against foreign data crossing a trust boundary (never strippable). Identical-looking checks (x > 0) differ only in who can violate them. Audit every strip against this line. - Check at the outermost scope where the truth is established. Loop-invariant precondition → hoist above the loop (once). Object-lifetime invariant → verify in the constructor (once). Type-expressible precondition → encode in the type (compile time, zero runtime cost). Re-checking an unchanging fact per call is a loop-invariant smell.
- A checking algorithm is usually cheaper than the producing algorithm. Verifying "sorted" is O(n) though sorting is O(n log n); verifying a solution is often O(n) though finding it is exponential. Reach for the verifier — then gate even that.
- Gate expensive contracts behind assertion levels / build flags. Cheap O(1) checks can stay always-on; O(n) and O(n log n) checks belong behind
-ea(Java),-O/__debug__(Python), or build tags (Go) — enforced in test/staging/fuzz, free in prod. - Only the assert expression is stripped — surrounding setup is not. Push expensive "old-state" snapshots inside the assert so they vanish with it; snapshot derived scalars (a count, a checksum), never deep copies.
- Never put side effects, mutations, or required work inside an
assert. It must be a pure predicate; stripping it must change nothing but verification. Run a CI smoke suite under the production strip config (python -O) to catch violations. - Assign each precondition one owner. Caller-satisfies / callee-assumes. Remove double-checks: fewer instructions and a clear answer to "who's to blame when it breaks."
- Move universal properties offline. Round-trips, idempotence, conservation, "output is sorted/balanced" → property-based tests over thousands of generated inputs at zero prod cost, instead of per-call inline verification.
- Prefer compile-time contracts. A type that cannot represent an invalid state is the cheapest contract: validated once at construction, trusted everywhere, zero runtime checks. "Parse, don't validate."
- Measure both the check and its setup. The cost of a contract includes snapshot allocation, redundant lookups, and defeated JIT optimizations — not just the comparison. Profile before stripping; profile after, to confirm the win.
Related Topics¶
- README.md — the positive rules: stating pre/postconditions and invariants, the caller/callee division, contracts as executable specification, and the Liskov rule under inheritance.
- find-bug.md — broken contracts in the wild: silent invariant drift, subtype contract violations, stripped validation.
- professional.md — applying contracts on a team: where to draw the validation boundary, code-review checklists for assertions.
- Defensive vs Offensive — the stance toward bad input; this file's validation-vs-contract line is the performance-facing edge of that distinction.
- Refactoring — "parse, don't validate" and Move Method patterns that turn runtime checks into type-level guarantees.
In this topic