Your status column is a state machine in disguise
A KYC flow tracked applicants through a varchar of freeform strings, so every typo was a silent dead-end state nobody could explain.
An account was stuck. Not rejected, not approved, not waiting on the customer: stuck, in a status nobody on the team could explain or get it out of. We traced it back and the culprit was a single character. Somewhere in the code, a status had been written as awaiting_resubmision, one s short of the value the rest of the system was checking for, and the database had swallowed it without a word of complaint.
That’s the whole problem with a status column, and it took me an embarrassingly long time to name it. A varchar that holds your lifecycle is a state machine you’ve decided not to admit you’re building.
A varchar is a state machine with the rules deleted
The flow tracked applicants through a column that held freeform strings: pending_application, awaiting_resubmission, approved_unconditional, rejected_recoverable, and a dozen more that accreted over time. No enum, no constraint, no transition table. Just a string, set in a handful of places scattered across the codebase, each one trusting that the literal it typed matched the literal everyone else was reading.
Every one of those decisions is, secretly, a state machine. There is a set of valid states. There are rules about which state can follow which. An applicant who was rejected_recoverable can be nudged back to awaiting_resubmission, but should never silently become approved_unconditional without passing through review. The machine exists whether or not you write it down. The only question is whether the database enforces it or whether you do, by hand, in your head, across forty call sites, hoping nobody fat-fingers a string.
The freeform version fails in the quietest possible way. A typo doesn’t throw. It writes. The row now holds a value that no if branch matches, so the account falls through every check and lands nowhere. No error, no log, no alert. Just a customer in limbo and a support ticket three weeks later that starts with “this is weird.”
Make the illegal states unrepresentable
The fix is to stop treating the status as text and start treating it as what it is.
- A closed set of states, enforced at the boundary. An enum in the database, or at minimum a check constraint, so writing
awaiting_resubmisionfails loudly at insert time instead of succeeding into the void. The database is the one place every path funnels through. Guard it there. - Named transitions, not raw assignments. Code shouldn’t be allowed to set status to anything it likes. It calls
submit(),requestResubmission(),approve(), and the transition function decides whether the move is legal from where you are now. The set of strings stops being scattered literals and becomes one table you can read in a single sitting. - A rejection of any state that isn’t reachable. If there’s no transition that leads to a state, that state can’t be entered, full stop. The limbo account I started with was only possible because nothing checked that the new value was a place you were allowed to go.
A status column with no transition rules isn’t data, it’s a promise you’re making forty times a day that you typed the same string everywhere. You won’t. Nobody does.
The tell, before it bites you
You can spot this trap without waiting for the stuck account. The tell is a status field that gets set with a bare string assignment anywhere outside one dedicated function. The moment two different files both write status = "approved" as a literal, you’ve already lost: those two strings are a refactor or a typo away from disagreeing, and the day they do, nothing will tell you. The compiler is happy. The tests, if they assert on the same literal, are happy. Production is quietly wrong.
So now, when I inherit a system, the status column is the first place I look, and “it’s just a string column” is the sentence that makes me nervous. It’s never just a string column. It’s a state machine someone declined to draw, running in production, held together by the hope that everyone types carefully. Draw it. Name the transitions. Let the database refuse the states that shouldn’t exist. The account stuck in a status nobody can explain isn’t a freak event. It’s an unconstrained string column doing exactly what unconstrained string columns do.