Fact-based state in Rails

Maybe you learned this from experience or you joined a Rich Hickey-esque immutability craze, but one way or another you know state machines are complicated. They couple together a variety of meanings and easily grow out of control as your application gets more and more interesting. What can be done?

Terse and syntax-sugery, but what’s completely missing from the code? A strategy to address:

Correcting/understanding unexpected states. How exactly did an object get into its state? And if it’s unexpected, what were its previous states? This situation is much much worse in production, where an inappropriate state goes by the name of data corruption and, as such, is one of the most costly failures with a slight chance of recovery.

Change. New business requirement makes an old state need to become two states, then two other states are grouped as one. What if you can now capture more data and introduce a ‘packaged’,’returned’,etc.? Or even simply rename a state? These are surpisingly nerve-wrecking changes to the system, necessitating at worst, a live update of entire database tables every time this happens.

State as a function of facts

To address both of the above issues with mutable state, one must simply turn the state machine inside-out and emphasize state’s complement – transitions. Transitions have a time-based nature in that they are ordered and have happened at a particular time. So they may also be known as events. Lets look at the above state machine to gather the states:

incomplete, open, cancelled, shipped

And emphasize the transition verbs:

purchase, cancel, resume, ship

Given a sequence of events, we can always produce a state. So lets gather events as nouns:

Verbose? Well, actually, this more verbose system with 3 major classes and inheritance is actually a much more flexible and error-proof solution to the state machine problem. Why does it work? Because it actually decouples incoming data(events) from our business interpretation of it. Events that happened – happened. It is the code of the system that makes sense of the past and guides the entry of new data. The concept of a state, is thus replayed from the history of past events:

No data migrations

You can introduce, remove and change states by changing only code. This is a significant improvement because each data migration is risky, difficult to debug and only gets worse with larger volumes of production data

Debugging unexpected states

There’s now full history of how an order got into a certain state and can thus be understood and debugged with a much larger hope of recovery.

And some would consider it a benefit to forego a DSL and meta-programming introduced by AASM or similar gems.

What do you think?

Concerned storing a little extra state will hurt runtime performance? Unconvinced of the simplicity of “state = f(past)” premise? Have taken this approach further? Let us know in the comments.