Preconditions, Assertions & Validation Boundaries

Who is responsible for checking what — design by contract's split between preconditions, postconditions and invariants — why assertions and input validation must never be the same mechanism, and why parsing at the boundary beats validating everywhere.

"Validate your inputs" is advice that, applied uniformly, produces a codebase where every function re-checks what its caller already checked, and where nobody can say which check is load-bearing. The useful version assigns responsibility: a contract states what the caller must guarantee and what the callee will then deliver, so each condition is checked exactly once, by the party that can actually do something about a violation.

Design by contract #

The framing pays off because it answers "whose fault is this" mechanically, and because it makes not checking a legitimate choice: if the precondition is documented, a caller that violates it has a bug, and the method is not obliged to defend against it. The trouble starts when the precondition is neither documented nor checked — then the failure surfaces three layers deeper as something unrelated.

Contravariance shows up here again: a subclass may weaken a precondition and strengthen a postcondition, never the reverse (Liskov).

Assertions are not validation #

These are different mechanisms with different audiences, and merging them is a security bug waiting to happen.

Assertion Validation
Checks an internal assumption — a bug if false external input — expected to be false sometimes
Audience the developer the caller / the user
On failure crash, loudly a typed error the caller handles
May be compiled out yes never
Example assert index < size after an internal computation "this email address is malformed"

Assertions still earn their place for internal invariants — after a non-obvious computation, at the top of a private method with a subtle requirement, in a loop invariant. There they document the assumption and catch its violation in tests, at a cost of nothing in production.

Parse, don't validate #

The structural improvement over both. A validation function returns a boolean and throws the knowledge away; a parsing function returns a more precise type, so the knowledge is carried in the type system and cannot be forgotten.

typescript
// validate: the knowledge evaporates at the closing brace
function isValidEmail(s: string): boolean
if (isValidEmail(input)) { send(input) }        // send() still takes any string

// parse: the knowledge becomes a type
function parseEmail(s: string): Email | ParseError
function send(to: Email)                        // impossible to call with an unchecked string

The payoff compounds: once Email, NonEmptyList, PositiveAmount and AccountId exist, every function downstream stops re-checking, and a new function cannot forget to check, because it cannot obtain the value without going through the parser. This is the same argument as value objects and the reason primitive obsession is expensive.

rust
struct Email(String);
impl Email {
    fn parse(s: &str) -> Result<Email, ParseError> { … }   // the only constructor
}

The constructor being private is what makes it a proof rather than a suggestion.

Where the boundary is #

A trust boundary is any place data arrives that the program did not produce: HTTP requests, message queues, files, environment variables, database rows written by an older version, FFI, and — the one people forget — anything deserialized (serialization).

text
untrusted ──►│ parse once │──► domain types ──► business logic ──►│ encode │──► out
             │  reject or │      (total,                          │        │
             │  construct │       always valid)                   └────────┘

Two properties make this work: the parse happens once, at the edge, and the interior types are total — every value of the type is meaningful, so interior code has nothing to check. A codebase that instead validates defensively at every layer has no boundary at all; it has a diffuse suspicion, which is more code and less safety.

Be conservative in what you accept #

Postel's robustness principle — "be liberal in what you accept" — was standard advice for decades and is no longer. RFC 9413 (IAB, 2023) states the reversal directly: tolerating unexpected input "is no longer considered best practice in all scenarios", because silent acceptance creates a feedback loop where broken implementations proliferate, the de-facto protocol drifts from the spec, and eventually nobody can change anything. Intolerance of violations is what gives implementers the signal to fix them.

The application-level version is the same: accepting a request with unknown fields, a date in three formats, or a number-as-string quietly makes all of those part of your API forever (API evolution). Reject early, and make the rejection message say precisely what was wrong.

Sources

  1. articleAlexis King — Parse, don't validate (2019)
  2. docsRFC 9413 — Maintaining Robust Protocols (IAB, 2023)
  3. docsMeyer — Design by Contract (Eiffel documentation)
  4. docsJava — Programming with Assertions (assertions are disabled by default)
  5. bookEffective Java, 3rd Edition

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close