The Broad Way

[ Sharp Mind · Sharp Blade · Sharp Spirit ]

root@construct:~
/state-machines-in-production-16-states-of-accounting
$_
<-- back to /logs
2026-02-06//LOG

State Machines in Production: 16 States of Accounting

TOPO Contabil has a bank reconciliation workflow. When I first mapped it out, I counted 16 distinct states. SIXTEEN. From "raw_import" all the way through "matched," "partially_matched," "disputed," "manually_reviewed," "approved," "posted," and about nine others that exist because accounting is a nightmare dressed as a spreadsheet. The first version used boolean flags. You know the pattern. isReviewed, isApproved, isPosted, isDisputed, isManuallyMatched. Five booleans means 32 possible combinations, of which maybe 16 are valid and the rest are corrupted data waiting to happen. We had a bug where a transaction was simultaneously "disputed" AND "approved" because two people clicked buttons at the same time and the booleans just did what booleans do. They toggled independently. State machines fix this COMPLETELY. A transaction in "disputed" state can ONLY transition to "under_review" or "rejected." It cannot magically become "approved" because there is no edge in the graph that allows it. The state machine is the law, and the law is not negotiable. Here is the architecture. Each state is an object with three things: allowed transitions, guard conditions, and side effects. The guard conditions check business rules before allowing a transition. Can this user approve transactions over 10K? Is the supporting documentation attached? The side effects fire AFTER a successful transition. Send notification. Create audit log entry. Update the ledger. The state explosion problem is real though. 16 states with an average of 3 transitions each means 48 edges in the graph. Each edge has guards and side effects. That is a LOT of code if you are not careful. The solution: convention over configuration. Most guards are composable. "hasPermission AND hasDocumentation AND amountBelowThreshold." You build a library of small guard functions and compose them. Same for side effects. We store the current state and the full transition history. Every state change is an append-only log entry with timestamp, user, previous state, new state, and the guard results. This is not optional for accounting software. Auditors WILL ask you "who moved this from disputed to approved and when." If your answer is "uh, let me check the boolean columns," you are going to have a bad time. XState is great for the frontend. For the backend, I rolled a lightweight engine because I needed it to integrate with our event sourcing setup. The state machine definition is a plain object. The engine is maybe 200 lines. The business rules are 2000 lines. That ratio tells you something important about where the real complexity lives. Sixteen states. Zero impossible combinations. That is the pitch.
The Broad Way | Kinho.dev