skip to content

When a domain operation can't be made fully side-effect-free -- for example, Account.debit(amount) must mutate the balance -- how can assertions be used to keep that side effect's contract explicit, and what's the practical failure mode if a team relies on Java's assert keyword for this in production?

level: middleimportance: should knowfreq 30%

answer

  1. pre/postconditions + invariants document side effects
  2. Java assert is OFF by default (-ea flag)
  3. always-on validation vs cheap internal assert
  4. debit(amount): balance can't go negative
  5. assertions substitute for reading every caller

basics

~20 s

You write down, in the code, exactly what must be true before and after the change (e.g. 'balance can't go negative') so anyone reading it knows the rule without guessing. But Java's built-in assert is switched off by default in production, so if that's your only check, the rule silently stops being enforced.

solid answer

~40 s

When a method must mutate state, Evans recommends documenting the change with explicit pre/postconditions and invariants, either as literal assertion statements or as always-executing validation, so the reader doesn't have to infer the contract by tracing every caller. For debit(amount), that means stating the precondition (amount > 0, balance >= amount) and postcondition (balance_after == balance_before - amount, balance_after >= 0). The practical trap is conflating this documentation technique with Java's assert keyword, which is disabled unless the JVM is launched with -ea, so most production deployments silently skip every assertion. Robust teams instead express these contracts as explicit validation that always executes (throwing exceptions), invariant-checking after every mutation, or as tests, reserving the assert keyword (if used at all) for cheap internal sanity checks that are fine to lose in production.

go deeper

for a junior

Understands that assertions state what must be true before and after an operation, and knows the -ea flag concept exists even if not deeply familiar with its implications.

for a middle

Writes precondition checks (e.g. require()/throw) on mutating methods and knows not to depend on Java's assert keyword for anything production-critical.

for a senior

Designs invariant enforcement at the right layer (constructor validation, value objects, or explicit checks) rather than scattering ad hoc assertions, and can explain why a debit operation's postcondition matters for correctness beyond the precondition.

for a principal

Sets team-wide policy on where contracts are enforced (types vs runtime checks vs tests), decides when the cost of full formal assertions outweighs simpler test coverage, and recognizes assertions as one tool among several for keeping a large, evolving model trustworthy.

## What an assertion states Assertions, in the Supple Design sense, are explicit statements of: - what must be true before an operation runs (**preconditions**), - what must be true after it completes (**postconditions**), - what must remain true throughout an object's life (**invariants**). For a mutating method like `Account.debit(amount)`, that means writing down -- as executable checks, structured comments, or both -- that the precondition is `amount > 0` and `balance >= amount`, and the postcondition is `balance_after == balance_before - amount`, with the invariant that `balance >= 0` always holds after any operation completes. The mechanism is deliberately lightweight: it doesn't require a formal proof system, just making the contract explicit enough that a reader, or a test, can verify it without tracing every call site that might invoke the method. ## Why the technique exists This exists because not every operation can be made side-effect-free; something in the system has to change state eventually. When a method must mutate, the interface's name alone, however intention-revealing, can't fully convey the exact contract of that mutation -- 'debit reduces the balance' doesn't tell a reader whether a debit that would overdraw the account throws, clamps to zero, or silently goes negative. Assertions close that gap: they narrow the black-box nature of a necessary side effect down to a precise, checkable statement, which keeps the interface trustworthy even for the minority of operations that Supple Design's other tools, like side-effect-free functions and closure of operations, can't eliminate. This is what keeps a large, evolving model safe to refactor -- you can change debit's internals freely as long as its asserted contract still holds. ## The assert-keyword trap The most important practical trade-off is the gap between assertions as a design or documentation technique and Java's, and Kotlin's when compiled to the JVM, built-in `assert` keyword, which is **disabled by default** -- the JVM ignores assert statements entirely unless started with the `-ea` flag. Teams that write `assert(amount > 0)` and consider the contract 'enforced' are frequently wrong: in most production deployments, that line executes as a no-op, and the precondition it was meant to guard silently stops being checked. The correct trade-off: - reserve the language `assert` keyword, if used at all, for cheap internal sanity checks that are acceptable to lose in production; - express anything that must actually be enforced as explicit validation: `require()`/`check()` in Kotlin, or an if-then-throw in Java, both of which always execute regardless of JVM flags. ## How it fails in practice The failure mode plays out concretely: 1. A service passes automated tests in CI, often run with assertions enabled via a test runner flag, but ships to production where `-ea` was never set, so a negative debit amount that should have been rejected instead silently corrupts the balance, and the bug isn't caught until reconciliation weeks later. 2. A related failure is checking an invariant only in the constructor, so a freshly created `Account` can't start negative, but not after every mutating method, letting a bug in a later-added `applyPenaltyFee()` push the balance negative without anything catching it. 3. A third failure is documenting a precondition in a comment that drifts out of date as the method evolves, because a comment, unlike an executable check, isn't caught by a test failure when it becomes wrong. ## A contract that always executes A robust version of `Account.debit(amount: Int)` in Kotlin would open with `require(amount > 0)` and `check(balance >= amount)`, both of which always execute, perform the mutation, and could optionally close with a postcondition check like `check(balance == balanceBefore - amount)` to catch a bug in the arithmetic itself, useful especially while the method is still evolving or under test. Framing the contract this way means a code reviewer, a new team member, or a future refactor of the internal storage, say moving from an `Int` field to an event-sourced ledger, all have one place to check what 'correct' means for this operation, without depending on a JVM launch flag that most teams forget is off by default.

  • If Java's assert keyword is off by default in production, why would a team use it at all?
    It's still valuable during development and testing, run with -ea, to catch bugs early and cheaply, and as living documentation of invariants that a reader can trust reflects the current contract, since assertions are executable and go stale visibly. The key discipline is never relying on assert for anything that must be enforced in production; for that, you throw a real exception or validate explicitly.
  • How do assertions relate to side-effect-free functions if a method DOES have to mutate state?
    When a function can't be made pure, assertions are the fallback: they narrow the black box of the side effect down to a precise, checkable contract, so callers can trust the mutation without reading its implementation. It's a way of keeping the interface intention-revealing even for the minority of operations that must change state.
  • What's a lightweight way to get assertion-like guarantees that don't depend on a JVM flag?
    Use require()/check() in Kotlin or explicit if-then-throw in Java, which always execute regardless of flags, or push the invariant into the type system with a value type that can't be constructed invalid, or cover the contract with unit tests that run in CI on every build.

Like a posted sign on a chemical tank listing the safe pressure range before and after venting -- the sign doesn't stop the leak by itself, but it lets an operator, or a new hire, know instantly what 'correct' looks like without opening the tank and tracing the pipework.

saying these in an interview costs you the question

  • treating Java's assert keyword as a production safety net
  • no documented pre/postcondition on a mutating method, forcing readers to trace every caller
  • confusing 'assertion' as a documentation/verification technique with the general term 'test'
  • invariants checked only in a constructor but not after every mutating method
  • assuming -ea is the default JVM flag

context