Enums Over Booleans
Enums over booleans for modeling your applications
Stop Using Booleans, Use Enums Instead!
You should use enums over booleans to represent application states because enums make your code more readable, prevent invalid state combinations, and are much easier to extend in the future. Booleans, on the other hand, create ambiguity (e.g., process(true, false)), allow for impossible states (e.g., isShipped = true and isCancelled = true), and require complex conditional logic that is difficult to maintain, especially when mapping the API response to a user interface.
The Problem with a REST API Full of Booleans 👎
Let's look at a common scenario: a REST API endpoint that returns the status of a customer's order. A backend developer leaning on booleans might design a JSON response like this:
{
"isStateCheckNeeded": true,
"selectedState": "RI",
"idMeURL": null,
"userEligible": false
}
This design introduces several risks and problems:
- The Risk of Invalid States The current structure allows for logically impossible combinations of flags. What if a bug causes the API to return:
{
"isStateCheckNeeded": true, // the user must select a state to determine eligibility
"userEligible": true //the user is already eligible
}
This state is a direct contradiction, but the data structure allows it. The frontend code is then forced to handle this nonsensical combination, likely by prioritizing one flag over the other, which can hide the underlying bug and lead to an unpredictable user experience.
-
The Problem of Implicit Meaning The actual UI state isn't explicit. The state "Verify Your Identity" is implied when isStateCheckNeeded is false and idMeURL has a value. The API doesn't tell the client what to do; it provides clues. Our frontend code has to act as a detective and derive this meaning every single time, creating a tight and brittle coupling between the frontend's interpretation and the backend's implementation details.
-
The Scalability Nightmare What happens when Product asks for a new step in the flow, like "Awaiting Manual Review"? With the current design, the backend team would likely add a new boolean flag, isAwaitingReview: boolean.
Immediately, every client (Web, iOS, Android) must update its complex if/else logic to account for this new flag and all its potential interactions with isStateCheckNeeded, userEligible, and idMeURL. The risk of one client implementing the new logic incorrectly is high, and the development effort across multiple teams is unnecessarily duplicated.
A single uiState enum elegantly solves all three of these problems by making the user's status explicit, unambiguous, and easily extensible.
Why Booleans Map Horribly to the UI
When the frontend receives the boolean-based JSON above, the UI code becomes a tangled mess of if/else if/else statements. The developer is forced to write defensive code to interpret the boolean flags and prevent the display of impossible states.
// ❌ Still a nightmare: the logic is brittle and order-dependent.
function UserVerificationFlow({ isStateCheckNeeded, isUserEligible, idMeUrl }) {
// First, we must defensively check for impossible states.
// This state is not possible, but as a consumer of the API, one needs to consider this and all permutations of the flags in order to safeguard the UI.
// Even if the check is not needed, it is cognitive overload to consider.
if (isStateCheckNeeded && isUserEligible) {
}
// The order of this entire block is critical. If a developer moves this
// 'isUserEligible' check to the top, eligible users who still need a
// state check would see the wrong UI.
if (isStateCheckNeeded) {
return <StateSelector />;
}
// This condition is complex and has to check other flags to be sure.
else if (!isStateCheckNeeded && !!idMeUrl && !isUserEligible) {
return <VerifyIdentityButton url={idMeUrl} />;
}
// This check for eligibility must come *after* the other checks.
else if (isUserEligible) {
return <ApprovedMessage />;
}
// This "denied" state is defined by what it's NOT. It only triggers if all
// the previous checks fail. This is an incredibly fragile way to define a state.
else if (!isStateCheckNeeded && !idMeUrl && !isUserEligible) {
return <DeniedMessage />;
}
// If the data doesn't perfectly match one of the conditions above,
// we fall through to an error state.
else {
return <ErrorMessage>An unexpected error occurred.</ErrorMessage>;
}
}
As you may be able to tell, the code is difficult to read and maintain. The reasons can be summarized as follows:
-
Order Dependency: The entire component's logic relies on the specific sequence of the if/else if blocks. Reordering them would completely break the user experience.
-
Redundant Logic: The same variables are checked repeatedly in different combinations throughout the chain, making it hard to refactor and easy to get wrong.
-
High Maintenance Cost: To add a new state (e.g., "Awaiting Review"), a developer must carefully find the correct place to insert a new else if and potentially update the conditions in all the other blocks to prevent unintended overlaps.
The Enum Solution: Clean, Clear, and Concise ✨
A better approach is to treat the order status as a single, explicit state. This is conceptually an enum. In a JSON API, this is typically represented as a string.
Here's the refactored, enum-style API response:
{
"data": {
"checkPSEEligibilityGuest": {
"uiState": "VERIFY_IDENTITY",
"context": {
"idMeUrl": "https://some-verification-url.com/xyz"
}
}
}
}
The possible values for uiState are a fixed set, defined and documented by the API: CHECK_STATE, VERIFY_IDENTITY, ID_APPROVED, ID_DENIED, BUY_LIMIT_REACHED, etc.
Now, look how clean and robust the UI code becomes:
function UserVerificationFlow({ uiState, context }) {
switch (uiState) {
case "CHECK_STATE":
return <StateSelector />;
case "VERIFY_IDENTITY":
return <VerifyIdentityButton url={context.idMeUrl} />;
case "ID_APPROVED":
return <ApprovedMessage />;
case "ID_DENIED":
return <DeniedMessage />;
default:
// Gracefully handle any states the UI doesn't know about yet
return null;
}
}
The advantages are immediate:
Clarity and Readability: The code is self-documenting. The switch statement clearly handles each defined state without any complex, order-dependent logic.
Safety & Simplicity: The API guarantees a single, valid state from the list of possibilities. The frontend code is no longer responsible for interpreting boolean combinations or handling impossible contradictions. It simply reacts to the state it's given.
Future-Proofing: When the business decides to add a new "Awaiting Manual Review" step, the API adds AWAITING_REVIEW to the list of possible values. The switch statement's default case can handle it gracefully until the UI is updated, and it's obvious where to add the new case.
When is a Boolean Okay?
Booleans aren't evil—they are essential. The key is using them for what they're designed for: representing true dichotomies, not descriptive states. A boolean should represent a simple true/false fact that has no other possibilities.
To decide, ask yourself these three questions:
Does this property describe what something is (its status or condition), or does it represent a simple on/off fact about it?
If this property is false, does that imply one single alternative, or could false represent multiple different conditions?
Do you feel the need to add null to represent a third state like "unknown" or "pending"?
A "yes" to that last question is an immediate red flag 🚩. The moment you see a property that can be true | false | null, you've discovered a state that was never truly binary. This "tri-state boolean" is a confession that the data model is fighting its own limitations. The null is a workaround to represent a third state, and it introduces ambiguity: does null mean the check hasn't run, or that it doesn't apply? The answer to "what does null really mean?" is almost always the name of your missing enum state.
Let's apply this test directly to the booleans from our checkPSEEligibilityGuest example to see why they are problematic.
Use Case | Property | Analysis | Verdict |
---|---|---|---|
The Trap ❌ | userEligible: false | This looks like a simple fact, but it fails our test. The value
| Should be an enum like
|
The Trap ❌ | isStateCheckNeeded: false | Similarly, | This is a sign that the entire flow is a state machine, not a collection of binary facts. |
Good ✅ | inventoryAvailable: true | Let's imagine a boolean from our example. This is a
true dichotomy. For the selected item and store,
inventory is either available for promise or it's not.
| Perfect for a boolean. |
Final Thoughts
Choosing the right data structure is crucial for writing clean, maintainable, and robust code. While a boolean might seem like the quickest solution for an API flag, it often introduces ambiguity and fragility that cascades into complex UI logic.
By representing distinct states with an enum (or a descriptive string in a JSON payload), you make your system more expressive, safer, and easier for everyone to work with.