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.
What triggers a command in our sample application? A request.

The syntax of a transition in the diagram is request[condition] / command().  It means: when a request has been received, and the condition is  fulfilled if present, process 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("Use credit card")
    .basicFlow()
        .step(ASSIGN).user(requestsToAssignLimit).system(assignsLimit)
        .step(WITHDRAW).user(requestsWithdrawal).system(withdraws).reactWhile(accountOpen)
        .step(REPAY).user(requestsRepay).system(repays).reactWhile(accountOpen)

    .flow("Withdraw again").after(REPAY)
        .step(WITHDRAW_AGAIN).user(requestsWithdrawal).system(withdraws)
        .step(REPEAT).continuesAt(WITHDRAW)

    .flow("Cycle is over").anytime()
        .step(CLOSE).on(requestToCloseCycle).system(closesCycle)

    .flow("Assign limit twice").condition(limitAlreadyAssigned)
        .step(ASSIGN_TWICE).user(requestsToAssignLimit).system(throwsAssignLimitException)

    .flow("Too many withdrawals").condition(tooManyWithdrawalsInCycle) 
         .step(WITHDRAW_TOO_OFTEN).user(requestsWithdrawal).system(throwsTooManyWithdrawalsException)
 .build();

As you can see, the model is in the code.  A model runner executes this model. The runner reacts to requests/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.

You can also generate documentation from the model. This is interesting for long term maintenance.

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.

You can use requirements as code for application internal event  processing as well, but maybe the "simple syntax" for event handling is a  better fit then.
Here's the new event handling model of the CreditCard that uses this syntax:

Model.builder()
    .on(LimitAssigned.class).system(this::limitAssigned)
    .on(CardWithdrawn.class).system(this::cardWithdrawn)
    .on(CardRepaid.class).system(this::cardRepaid)
    .on(CycleClosed.class).system(this::cycleWasClosed)
.build();

The code just dispatches each event to a state changing method. If  you need to implement more complicated behavior, consider use cases as  well. But if that still can't cope with the complexity, maybe a state  machine is still the right fit.

Also, some state machine implementations provide additional relevant  features like monitoring or concurrency support. They may tie you to  their own tech stack though.

How the Credit Card application works now

  • A client sends a request to the CreditCardModelRunner
  • The CreditCardModelRunner handles the request with its use case model, and the model forwards the request to a command.
  • A command that is part of the CreditCard class produces events
  • The event handling model of the CreditCard class dispatches the event to a state changing method

Conclusion

I hope you enjoyed my article. 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 have a question, let me know. If you want to keep up with what I'm doing, follow me on dev.to, LinkedIn or twitter.

This article was first published on dev.to.