Designing out impossible states
Earlier this week, another software engineer asked for advice in designing a feature. The feature added a random alphanumeric token to a Product in our system, along with a flag (token_navigation_enabled
) that controls whether users can navigate to a Product via its token. The engineer's specific question was around when to generate those tokens. The two options posed were:
- Option 1: Generate the token the first time
token_navigation_enabled
is enabled, lettingtoken
be nullable. - Option 2: Generate tokens for each existing Product, backfilling as necessary and making
token
non-nullable.
Option 1 scared me. It scared me because that design allowed for an invalid state: a null token with token_navigation_enabled
. Sure, the application might disallow that state for now, but who's to say a manual database change wouldn't circumvent application-level protections, or a bug enters the system that leaves us with null tokens in certain circumstances. Every time code relies on token_navigation_enabled
and needs the token, we must add the unfortunate non-null check. Additionally, the existence of a token is now indirectly coupled to token_navigation_enabled
, making the model harder to extend. What if we want to add a new feature that uses token? We must revisit how token is generated.
Option 2 is much more appealing to me. Why? Because it makes the invalid state impossible. Consuming code doesn't need to check for token because it's guaranteed to be there.
In the React world, you may have seen advice to avoid multiple boolean props expressing mutually exclusive states, and prefer a single enum-like field that indicates which state a component should be in. This advice is built on the same principle that makes option 2 "better." Put into words:
Strive to model your data such that it is impossible to express invalid states. You'll eliminate entire classes of bugs and avoid code checking for states the application shouldn't allow in the first place.