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

State Machines in Production: 16 States of Accounting

Topo Contábil has a bank reconciliation workflow. When I first mapped it out, I counted 16 distinct states, from "raw_import" all the way through "matched," "partially_matched," "disputed," "manually_reviewed," "approved," "posted," and about nine others that exist because real reconciliation has many edge cases.

The first version used isReviewed, isApproved, isPosted, isDisputed and isManuallyMatched. Five booleans means 32 possible combinations, of which maybe 16 are valid. The rest is corrupted data waiting to blow up. We caught a transaction that was "disputed" and "approved" at the same time because two people clicked the button together. The flags toggled independently.

With a state machine, a transaction in "disputed" can only move to "under_review" or "rejected." It cannot become "approved" because that edge does not exist in the graph. The impossible boolean combinations can no longer be represented.

Each state is an object with allowed transitions, guard conditions, and side effects. The guard checks the business rule before allowing the transition: can this user approve a transaction over R$10K? Is the supporting documentation attached? After a successful transition, the side effects send the notification, write the audit log entry and 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 way out is 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 under_review 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, compared with 2000 lines of business rules.

State Machines in Production: 16 States of Accounting | The Broad Way