Why is tracking one operation with three independent boolean flags in component state bug-prone, and what shape replaces it?
answer
- count the combinations you can write
- eight expressible, four legal
- one write per transition
- make contradictions unrepresentable
- data travels with its state
basics
~20 sThree independent booleans describe eight combinations when the operation has about four real states, so contradictory ones are reachable and each write must set several flags. One status field with a closed set of named values makes the illegal combinations unrepresentable.
solid answer
~50 sIndependent flags let the state space exceed the real one. Three booleans for one operation give eight combinations while the operation has roughly four honest states, so "in progress and failed at once" is reachable — usually because a transition set two flags and forgot the third. Every reader then has to re-derive the precedence rules, and they will disagree about which branch wins. Replace the flags with one field whose value is a name from a closed set, so a transition is a single write and contradictions cannot be expressed. Then attach to each state only the data that state actually has: the payload with success, the message with failure, neither with the idle state. That kills the second family of bugs: reading a value that exists but is meaningless in the current state. Genuinely independent concerns still deserve separate fields — collapse a state, not two unrelated axes.
code
pseudocode · 11 linesstate request = { status: "idle" }
on start: request = { status: "loading" }
on success: request = { status: "succeeded", items: rows }
on failure: request = { status: "failed", message: text }
render:
when request.status is "idle" -> prompt
when request.status is "loading" -> progress
when request.status is "succeeded" -> list of request.items
when request.status is "failed" -> request.messagego deeper
Know the symptom to look for: a spinner and an error message on screen at the same time usually means several flags describe one operation and a transition forgot to clear one.
Be able to count the representable combinations against the real states, and rewrite the flags as one field whose values are named, with the data for each state attached to it.
Show that you use shape to remove bug classes rather than adding defensive checks, and that you know which component owns the field so two views cannot report different progress.
Argue the economics: unrepresentable states cost one modelling decision, while defensive checks cost every reader forever. Also say where the rule stops — independent axes stay independent.
## Count the states you can represent State shape is an ownership decision: the component that owns a value also owns the question of what values are even expressible. Three independent booleans express 2 x 2 x 2 = 8 combinations. An operation like "load the list" has about four states worth naming. The gap between eight and four is not theoretical — it is the set of combinations your rendering code must handle, or accidentally does not. | Combination of three flags | Meaning | Legal? | |---|---|---| | none set | Not started yet | Yes | | in-progress only | Running | Yes | | finished only | Completed with data | Yes | | failed only | Completed with an error | Yes | | in-progress and failed | Running and failed at once | No | | finished and failed | Both outcomes | No | | in-progress and finished | Running and done | No | | all three set | Nonsense | No | Half the space is illegal, and nothing in the shape prevents it. The illegal half is reached the ordinary way: a retry path sets the in-progress flag without clearing the failure flag, so the screen shows a spinner and an error together. ## What the flags cost in the code around them - **Every transition is several writes.** Starting a retry means set one flag, clear two. Miss one and the state is contradictory, with no error to point at it. - **Every reader invents precedence.** One place checks failure first, another checks progress first. Both are defensible; together they render two different screens for one state. - **A new outcome doubles the space.** Adding a fourth flag turns eight combinations into sixteen, of which perhaps five are legal. - **Tests cannot enumerate it.** You can write a case per named state; nobody writes eight cases per operation, so the illegal combinations are untested by definition. ## One field with a closed set of values The replacement is a single field owned by whoever owns the operation, holding one name from a fixed set — not-started, running, succeeded, failed. Three properties follow immediately: 1. **A transition is one write.** There is no partial update, so there is no contradictory intermediate state to leak into a render. 2. **Illegal combinations are unrepresentable**, not merely unhandled. "Running and failed" cannot be written down. 3. **Readers branch once**, on the same field, in the same order, so two parts of the screen cannot disagree about which state the operation is in. ## Carry only the data the state has The second half of the fix is about the values that travel with the status. Flags usually sit beside a payload field and an error field, both present in every state and both meaningless in most of them, so code reads an empty payload while the real state is "failed", or shows a leftover error after a successful retry. Model the state as a value that carries its own data: the succeeded state carries the payload, the failed state carries the message, the running and not-started states carry nothing. Now "read the payload" is only possible where a payload exists, and stale leftovers cannot survive a transition, because the transition replaces the whole value rather than editing one field beside it. ## Where the field lives This is still an ownership question. One operation has one status, so one component owns the field — the one that starts the operation. Components that merely display progress receive the current status as an input. Two components each keeping their own status for the same operation is the duplication bug wearing different clothes: they will disagree, and the disagreement will look like a rendering glitch. ## When several booleans are the right shape Do not over-apply this. Booleans are wrong when they encode *one* thing's progress; they are right when they encode genuinely independent axes. A row can be selected, expanded, and loading at the same time, and all combinations are meaningful — forcing those three into one field invents impossible names and loses information. The test is whether two flags can be true together in a state the product actually has. If yes, they are separate axes. If no, they are one field pretending to be two. The judgment to bring to an interview is that shaping state is cheaper than defending it. A shape that cannot express a contradiction removes a class of bugs outright, rather than adding a check that some caller will forget.
- How does this shape stop a stale error message from showing after a successful retry?Because the transition replaces the whole value rather than editing one field beside others. The succeeded state carries the payload and has no message field at all, so there is nowhere for the old message to survive. With separate flags the message field persists, and clearing it is a step a retry path can forget.
- The statuses are exclusive but the operation can be retried while old data is still shown. How do you model that?Let the running state carry what it needs — the previously loaded payload alongside the fact that a refresh is in flight. The point is not that the running state is empty, it is that each named state declares exactly the data it has. A "refreshing with previous data" state is a legitimate named state.
saying these in an interview costs you the question
- Believes separate boolean flags cannot contradict if you are careful
- Adds a fourth flag rather than replacing the flags with one field
- Says separate booleans are never acceptable, even for independent axes
- Keeps payload and error fields present in every state
- Lets two components each hold their own status for one operation
- Fixes contradictory flags by adding checks in each reader