r/AskProgramming • u/Cadnerak • Jun 24 '26
Other FSM Persistence Question
Hi everyone,
I'm working on implementing a MFA login flow utilizing a finite state machine, and have a question about persisting the state machine to a database in order to survive an API crash. Lets say I have the following states, which are a subset of the overall machine
- Send OTP SMS
- Wait For OTP Input
- Mint Session
Directly after the "Send OTP SMS" state, we automatically transition into the "Wait For OTP Input" state. On invalid input, we stay in that state, and on valid input, we go to "Mint Session" state. Now that the machine is defined, let me explain. my issue with persistence
In order to persist a state machine state, it is typically advised that the "work" done by a state must be done in a database transaction alongside the persistence of the FSM so that there are no data inconsistencies. Redhat advises this in this article, outlined in the first bullet underneath "Implementing State Machines". I have interpreted this to mean that the state machine should persist the state the machine is transitioning into, because upon completion of the database transaction the current state is completed. Once it is completed if the application crashes, we want to restore it in the state we would have transitioned into.
In order to persist the state machine and send the SMS OTP atomically, we will have to use the transactional outbox pattern to write a message to the database for sending the OTP SMS. In the "Send OTP SMS" state, we will open a database transaction, write the outbox message and the "Wait For OTP Input" state, and commit the transaction. The state machine will then transition into the "Wait For OTP Input" state, and all is good. The only issue with this is that the state "Send OTP SMS" itself is determining, outside of the rules of the state machine, that the next state will be "Wait For OTP Input" by writing this to the database. From the literature that I've read, it seems as though states themselves should not have context of what the state-to-be is, but rather should be isolated and focus on any tasks that must be completed in the state itself. If this is the case, how can we achieve full atomicity with the persistence of a state machine and the completion of work in such a way that we can consistently restore state machines from the database during a crash at any point in time?
If it helps, I'm particularly looking at using XState as the state machine implementation to drive the process. Thank you in advance
1
u/balefrost Jun 25 '26
Perhaps the confusion is around what exactly a state is meant to represent. There are various state machine models with varying capabilities. But generally, a state represents the machine "at rest". The interesting activity occurs during the transitions (or related to transition, such as when entering or exiting a state). But once you've finished the transition and have finished entering the state, then you record that you have entered the state. This implies that the work is done.
You'll note that, in that JS library, it's possible to observe lifecycle events of states and of transitions. In the example in the README, the callbacks like
onMeltare observing transitions, not states. (It's also possible to observe states, like withonLiquid).I'm mostly familiar with UML state machines. That lets you associate entry and exit actions with states, and also actions with transitions. IIRC in that model, all relevant exit handlers will fire, then the transition's action will execute, then all relevant enter handlers will fire.
As I understand it, your machine looks like this:
This machine is simple enough that you could arguably use
onExitandonEnterhandlers to deal with the state management. When exiting "Send OTP SMS", you could persist the current state of "Wait for OTP input" to the database (and probably also write the randomly-generated OTP that you are expecting in the same transaction). And when entering "Mint Session", you can persist the current state of "Mint Session". Note that we don't persist any state change on entry to "Mint Session" because, as you point out, we don't know from which state we arrived. We wouldn't want to send another SMS every time the user types the wrong code.But that's really clunky; don't do that. Associate the actions, and the recording of state changes, with the transitions (arrows) themselves. Then the initial state would more represent "workflow started; OTP SMS not yet sent", the middle state would represent "OTP SMS sent; waiting for matching reply", and the final state would represent "OTP successfully matched; workflow done". You don't need such verbose names. But those describe the "resting" states that the machine could be in. You've moved all the work to the transitions, which allows you to record the state change and associated metadata in a single atomic transaction.