DevIQ
Make Invalid States Unrepresentable
Make Invalid States Unrepresentable
"Make Invalid States Unrepresentable" is a design principle that advocates for structuring your types, classes, and data models in such a way that invalid or illegal states simply cannot be expressed or constructed. By leveraging the type system and careful API design, you can eliminate entire categories of bugs at compile time rather than having to validate and handle invalid states at runtime.
The Problem
In many software systems, data structures are designed in ways that allow invalid combinations of values. For example, consider a user profile that can be in one of two states: a guest user or an authenticated user. A naive implementation might look like this:
public class UserProfile{ public bool IsAuthenticated { get; set; } public string? Username { get; set; } public string? Email { get; set; } public string? SessionToken { get; set; }}This design allows for invalid states such as:
IsAuthenticated = truebutUsername,Email, orSessionTokenare nullIsAuthenticated = falsebut authentication fields are populated- Any combination where the boolean flag doesn't match the presence of authentication data
These invalid states lead to defensive code throughout the application, with constant null checks and validation logic scattered everywhere. This principle is highly correlated with the primitive obsession code smell, since primitive types almost always are able to represent more possible values than a specific property or variable's set of valid states.
The Solution
Instead of allowing invalid states to exist, redesign the types to make them impossible:
public abstract class UserProfile{ private UserProfile() { } // Prevent external instantiation public sealed class Guest : UserProfile { } public sealed class Authenticated : UserProfile { public string Username { get; } public string Email { get; } public string SessionToken { get; } public Authenticated(string username, string email, string sessionToken) { Username = username ?? throw new ArgumentNullException(nameof(username)); Email = email ?? throw new ArgumentNullException(nameof(email)); SessionToken = sessionToken ?? throw new ArgumentNullException(nameof(sessionToken)); } }}Now it's impossible to have an authenticated user without the required fields, and there's no boolean flag that can be out of sync with the actual state.
Benefits
- Compile-Time Safety: Many bugs are caught at compile time rather than runtime
- Reduced Testing Burden: You don't need to test for invalid states that can't exist
- Clearer Intent: The code explicitly communicates what states are valid
- Less Defensive Programming: Eliminate scattered validation and null checks
- Better Refactoring: Changes to state machines become safer and more obvious
Real-World Examples
Payment Status
Instead of using flags (see Flags Over Objects Antipattern) that can conflict:
// Bad: Invalid states possiblepublic class Payment{ public bool IsPending { get; set; } public bool IsCompleted { get; set; } public bool IsFailed { get; set; } public string? TransactionId { get; set; } public string? FailureReason { get; set; }}Use a proper state representation:
// Good: Only valid states possiblepublic abstract class PaymentState{ private PaymentState() { } public sealed class Pending : PaymentState { } public sealed class Completed : PaymentState { public string TransactionId { get; } public Completed(string transactionId) => TransactionId = transactionId; } public sealed class Failed : PaymentState { public string Reason { get; } public Failed(string reason) => Reason = reason; }}Optional Values
Instead of using null with a separate "has value" flag:
// Bad: Flag and value can be inconsistentpublic class Configuration{ public bool HasCustomTimeout { get; set; } public int TimeoutSeconds { get; set; }}Use proper optional types:
// Good: Either has a value or doesn'tpublic class Configuration{ public int? TimeoutSeconds { get; set; } // Or use Option<T> from functional libraries}Related Principles and Patterns
- Encapsulation - Protecting object invariants
- Fail Fast - Detecting problems as early as possible
- Type Safety - Using the type system to prevent errors
- Parse, Don't Validate - Creating valid objects from input
- Guard Clause
Language Support
Some programming languages make this principle easier to apply than others:
- Algebraic Data Types (F#, Haskell, Rust, TypeScript): Native support for discriminated unions makes invalid states naturally unrepresentable
- Sealed Class Hierarchies (C#, Java, Kotlin): Can simulate discriminated unions through inheritance
- Generics and Type Parameters: Help create type-safe abstractions
- Immutability: Prevents objects from transitioning into invalid states after construction
References
This principle originates from the functional programming community, particularly from the work of Yaron Minsky, who popularized the phrase in the context of OCaml development. The principle has since been widely adopted across different programming paradigms.
Edit this page on GitHub