Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Make Illegal States Unrepresentable

Pattern

A reusable solution you can apply to your work.

“Making the wrong thing hard to express is better than checking for the wrong thing at runtime.” — Yaron Minsky

Understand This First

  • Boundary – constructors that enforce invariants define boundaries between valid and invalid state.

Context

At the heuristic level, this principle applies whenever you’re designing data structures, types, or configurations. It builds on Boundary and complements Local Reasoning. Where encapsulation hides implementation details, this pattern goes further: it arranges the design so that invalid combinations of state literally can’t be constructed.

In agentic coding, this principle is especially powerful. An AI agent generates code based on the structures you define. If your types permit invalid states, the agent will write code that handles those states (branching, validating, throwing exceptions) adding complexity that wouldn’t exist if the types were tighter. If your types make illegal states impossible, the agent produces simpler code because there are fewer cases to consider.

Problem

How do you prevent bugs that arise from data being in a state that should never exist?

Runtime validation catches some of these bugs, but only the ones you think to check for. Defensive programming (adding if statements and assertions throughout the code) is fragile, verbose, and easy to forget. The real danger is the invalid state you didn’t anticipate, which flows silently through the system until it causes a failure far from its origin.

Forces

  • Permissive types are easier to define initially but create a combinatorial explosion of states to validate.
  • Runtime checks catch some invalid states but add code, slow execution, and are only as good as the developer’s imagination.
  • Strict types require more upfront thought but eliminate entire categories of bugs at compile time.
  • Serialization boundaries (APIs, file formats, databases) often force permissive representations that must be validated on entry.

Solution

Design your types and data structures so that every value they can hold represents a valid state. If a state shouldn’t exist, make it impossible to construct — not just checked at runtime but structurally excluded.

Consider a traffic light. A permissive representation might use three booleans: red, yellow, green. This allows eight combinations, but only three are valid (one light on at a time). A tighter representation uses an enumeration with three values: Red, Yellow, Green. The six invalid states simply can’t be expressed.

In practice, this means:

Use enumerations instead of strings or integers for values drawn from a fixed set. A status field that’s a string can hold anything. A Status enum with Active, Suspended, and Closed can only hold valid values.

Use sum types (tagged unions) for values that vary by kind. A payment can be a credit card, a bank transfer, or a digital wallet, each with different required fields. Rather than one type with nullable fields for all three, define a type that is exactly one of the three, each with its own required fields.

Enforce invariants through constructors. If an email address must contain an @ symbol, validate that in the constructor and make it impossible to create an EmailAddress value that violates the rule.

Tip

When defining data structures for an agentic workflow, spend a few minutes tightening the types. An agent working with an enum generates match/switch statements that cover every case. An agent working with a raw string generates validation code, error handling, and defensive branches, all of which are opportunities for bugs.

How It Plays Out

A team models a user account with a role field stored as a string. Over time, code appears that checks if role == "admin" or if role == "Admin" or if role == "ADMIN". A bug ships because one check uses the wrong casing. Replacing the string with a Role enum eliminates the entire category of bug: the compiler ensures every comparison is against a valid value.

An agent is asked to handle order states: Pending, Paid, Shipped, Delivered, Cancelled. The developer defines these as an enum with associated data. A Shipped order carries a tracking number, a Cancelled order carries a reason, and a Pending order carries neither. The agent generates clean pattern-matching code with no null checks and no “this should never happen” branches.

Example Prompt

“Define the order status as an enum with associated data: Pending has no extra fields, Shipped carries a tracking number, and Cancelled carries a reason string. Use this enum throughout the order module instead of raw strings.”

Consequences

When illegal states are unrepresentable, entire categories of bugs are eliminated at design time rather than discovered at runtime. Code becomes shorter because validation logic and defensive branches disappear. Tests can focus on business logic rather than state validation. And code reviews become easier because reviewers don’t need to check whether every function correctly validates its input.

The cost is upfront design effort. Tight types require thinking carefully about your domain before writing code. They can also make serialization harder: you need explicit conversion between the permissive formats of JSON, databases, or APIs and the strict formats of your internal types. This conversion is worth doing; it creates a clear boundary between the messy outside world and the clean internal model.

  • Depends on: Boundary — constructors that enforce invariants define boundaries between valid and invalid state.
  • Enables: Local Reasoning — fewer possible states means less to consider when reading code.
  • Enables: KISS — eliminating invalid states reduces the complexity of the code that handles them.
  • Refined by: Smell (Code Smell) — excessive null checks and “impossible” branches are smells that suggest states should be made unrepresentable.
  • Uses: Source of Truth — tight types help ensure data validity at the source.