TOPO Contabil 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 accounting is a nightmare dressed as a spreadsheet.
The first version used boolean flags. The flags were 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 rule out those impossible combinations. 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 enforces those transitions.
Each state is an object with 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 run after a successful transition, sending notifications, creating audit log entries, or updating 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 can become a lot of code. I use convention over configuration to keep it manageable. 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, compared with 2000 lines of business rules. Most of the work is in those accounting rules. The small engine keeps the sixteen states from turning into impossible combinations.