loading...
Cover image for Kissing the state machine goodbye

Kissing the state machine goodbye

bertilmuth profile image Bertil Muth Updated on ・6 min read

Recently, I have written about simplifying an event sourced application.

The article starts with code from a talk by Jakub Pilimon and Kenny Bastani. And it ends with building a model for events in the code: how they are applied, and under which conditions.

The sample application is about Credit Card management. You can:

  • Assign a credit limit. But only once, otherwise the application throws an IllegalStateException.
  • Withdraw money. But you can't make more than 45 withdrawals in a certain cycle. Or you'll get an exception as well.
  • Repay money

I played around with the CreditCard class. I had a feeling that something might be wrong with the withdraw method. So I wrote a test that checks for the correct behavior.

@Test(expected = IllegalStateException.class)
public void withdrawWithoutLimitAssignedThrowsIllegalStateException() {
    CreditCard card = new CreditCard(UUID.randomUUID());
    card.withdraw(BigDecimal.ZERO);
}

The test attempts to withdraw an amount of zero. But no credit limit has been assigned before. The application should reject this, and throw an IllegalStateException.
Instead, the application threw a NullPointerException.

The application assumed that the limit had been assigned before.
Now: this is a sample application. If it covered all cases it probably wouldn't be as understandable as it is.

Let's pretend we're dealing with a real world application. What if the required order of commands/events depends on a multitude of conditions and states?

If you have ever tried to implement this with conditional statements only, you probably know it's easy to lose the overview. But there is a standard solution for managing complicated flows and changes in behavior.

State machine to the rescue

In computer science, state machines have been around for decades. They are well understood in theory. They are battle proven in practice. They are the de facto standard for dealing with state dependent behavior.

So I decided to create a UML state machine model for the sample application. I asked myself first: Do I want to deal with commands or events in the state machine?

Commands are about something the application should do in the future.
Events are about something that has happened in the past.

I wanted to prevent withdrawals without a credit limit assigned.
So the state machine model needed to deal with commands.

The syntax of a transition in the diagram is command[condition] / commandHandler(). It means: when a command object has been received, and the condition is fulfilled if present, handle the command and go to the next state.

State machine

The model fixes what is allowed to happen, and what not. For example: repaying is only possible after withdrawing.

But that precision has a price. If you want the state machine model to be executed and to control the behavior at runtime, you need to model every possible transition from every state. Including its condition, if there are two transitions with the same event.

That's why there is a lot more repetition in the state machine than in the original code with the if statements. A way to reduce the amount of repetition is to use super states and sub states:

State machine with sub states

It is easy to define state dependent behavior in a state machine model. But a state independent rule like in any state (when condition X holds), do Y leads to several transitions. For example, I needed to add requestToCloseCycle to every super state.

You need people with the right skills to create the models. And it's not easy to communicate about the models with non-technical stakeholders. It's not the way they normally speak about user journeys.

Saying goodbye

It seems there are two options so far.

In the left corner: the if statement. Easy to start with. Low overhead. Best fit for applications that have no complicated flows of behavior. But it's easy to lose the overview when the behavior gets complicated.

In the right corner: the executable state machine model. Powerful. Proven. Precise. Gives you an overview of the behavior. But it's hard to define state independent rules. And state machine models are difficult to communicate about with non-technical stakeholders.

I stand in the third corner. I have found an alternative to state machines.
A solution that

  • enables you to define conditions. But you don't have to in most cases.
  • makes state dependent and independent rules equally easy to specify.
  • uses language that all stakeholders can relate to.

Before I dig into the details, here's the sample state machine model rewritten using that solution:

Model model = Model.builder()
  .useCase(useCreditCard)
    .basicFlow()
        .step(assigningLimit).user(requestsToAssignLimit).systemPublish(assignedLimit)
        .step(withdrawingCard).user(requestsWithdrawingCard).systemPublish(withdrawnCard).reactWhile(accountIsOpen)
        .step(repaying).user(requestsRepay).systemPublish(repay).reactWhile(accountIsOpen)

    .flow("Withdraw again").after(repaying)
        .step(withdrawingCardAgain).user(requestsWithdrawingCard).systemPublish(withdrawnCard)
        .step(repeating).continuesAt(withdrawingCard)

    .flow("Cycle is over").anytime()
        .step(closingCycle).on(requestToCloseCycle).systemPublish(closedCycle)

    .flow("Limit can only be assigned once").condition(limitAlreadyAssigned)
        .step(assigningLimitTwice).user(requestsToAssignLimit).system(throwsAssignLimitException)

    .flow("Too many withdrawals").condition(tooManyWithdrawalsInCycle) 
        .step(withdrawingCardTooOften).user(requestsWithdrawingCard).system(throwsTooManyWithdrawalsException)
.build();
return model;

As you can see, the model is in the code. A model runner executes this model. The runner reacts to commands/events, similar to a state machine.

The basic flow is the "happy day scenario". The steps of a user to reach her goal. The other flows cover alternative and error scenarios.

A flow can define an explicit condition for its first step to run - e.g. after(...), anytime() or condition() in the sample.
If a flow has an explicit condition, the flow starts when the condition is fulfilled and the runner is currently in a different flow.
If a flow has no explicit condition (e.g. the basic flow in the sample), the first step runs after the runner has started, when no step has been run so far.

Starting with the second step of a flow, each step has an implicit condition. That condition is: run the step after the previous step in the same flow, unless a different flow with an explicit condition can start.
So in contrast to state machines, you don't need to specify the conditions after the first step.

Internally, state depending behavior is realized by checking a condition.
Every step contains its complete condition that defines exactly when the step can run. That's how requirements as code can treat state dependent and independent behavior alike.

Have a look at further examples to dig deeper.

When to use requirements as code

Many applications have dynamic internal behavior. This is true for distributed applications in particular. They need to deal with the fact that "the other party" is not available.

But from a user's perspective, these applications look quite predictable and regular. When I want to watch a show on Netflix or Amazon Prime, I follow the exact same steps each time until I can watch it. It looks like one step just follows the other.

That's the sweet spot for requirements as code, if used as an alternative to a state machine: defining the visible behavior of an application.

How the Credit Card application works now

  • A client sends a command to the CreditCardAggregateRoot
  • The CreditCardAggregateRoot uses the event repository to replay all the events for the credit card, to restore it
  • The CreditCardAggregateRoot uses the above model to dispatch the command to a command handling method
  • The command handling method produces an event and applies it to the CreditCard instance.
  • The event handling model of the CreditCard instance dispatches the event to a state changing method

Conclusion

I hope you enjoyed my article. Please tell me what you think in the comments.

I also want to invite you to look at the library that I used throughout the article. Try it out in practice, and let me know the result.

If you want to keep up with what I'm doing or drop me a note, follow me on LinkedIn or Twitter. To learn about agile software development, visit my online course.
Last edited April 27, 2020: updated event sourcing process

Posted on by:

Discussion

markdown guide
 

Been doing state machines for years, as a means to create predictable and known behaviours for industrial equipment, from the very large to the very small.

There is a definite skill to system and sub-system decomposition, especially to avoid the state explosion.

Usually then you go to a hierarchial set of state machines, and sometimes you need synthetic machines as intermediaries.

From experience, Uml is the problem, not the answer. Best to use a state table to force consideration of every possible transition. That way you get exhaustive and unambiguous definition of behaviour, with encapsulated transition definitions and if you need event/alarms logging you get first up and masking for free built in.

Plus not hard to train the process owner how to read state tables and then they are the common shared language between them and the implementer, plus the implementer only has to worry about system issues, not interpret some crazy narrative.

I use it that much I wrote myself a simulator with a nice Qt GUI and a bunch of functionality like change journalling, definition documentation for each transition, test all possible combos, OPC server built in to allow simulation, user training, interfacing/HMI etc and then generate code for direct import to end device (embedded or industrial controller).

Then the spec is the product and you have direct traceability back to how/why, requirement of some standards and clients to high levels of rigour.

State machines are well worth the up front effort to get comfortable with, nothing else compares (maybe formal methods, but big entry hurdle) when you need to know/predict exact behaviour of a system.

 

Would this pair nicely with the Command/Orchestration Saga pattern?

blog.couchbase.com/saga-pattern-im...

 

Yes! I even commented on the article with a link. I think it’s a particular good fit for command/query driven business processes that include multiple user interactions.