State Transitions through Sequence Diagrams

This post is my contribution to F# Advent 2018. For years I’ve contributed here and there to a large number of projects, so it is hard to pick a topic. I decided to choose something that cuts across all my various hobby projects through the years and in which I recently found inspiration and practical value when designing software systems, specifically those portions of software systems that want to expose and/or enforce a correct sequence of user actions.

Motivation

I’ve built many systems where the user interaction design was either missing or delayed, both of which led to user frustration or confusion as to how to correctly interact with the software. I’ve also experienced this from both perspectives as a producer and consumer of software library APIs. Most people think it easier to expose all functionality to a user and provide documentation as to how the software should be used. In many cases, this is a pretty good trade-off. Other cases — especially those where a specific sequence of steps should be followed — can lead to misuse, bugs, and lots of frustration.

Many of these issues can be solved by simply thinking through and documenting the desired flow and then implementing that flow within the software. The latter part is often easier said than done, at least in my previous experience.

While at Open FSharp this year, I read a tweet by Mike Amundsen linking to his talk from RESTFest:

I didn’t get to see or hear the talk, but the slides and linked source code clued me in on the concept, which I found very compelling. Here’s my take:

Use sequence diagrams to design resource state transitions, i.e. workflows.

Mike’s slides show a slightly different direction, using the sequence diagram to identify resources, not states, and the transitions between them (though I could be misinterpreting his slides). I found that using each actor line as a state within a single resource made more sense for what I’ve needed to do at work, and the model is much simpler than writing a full-fledged Open API spec. Mike’s slides indicate he’s thinking along these lines, as well as the potential for generating the full specification, documentation, etc. from a simple sequence diagram.

Design

For this post, I’ll stick with Mike’s example so as to remain consistent and progress the conversation. The example is that of an onboarding workflow. I’m going to identify this as an onboarding resource. The first step, then, is to identify the states the onboarding resource may have at any given stage:

home
WIP
customerData
accountData
finalizeWIP
cancelWIP

We then need to identify all the transitions from state to state that we want to support. This is where we immediately diverge from what most API formats allow you to specify and where you can immediately find value from this process. (Note: the funny syntax for the arrows is the convention used by Web Sequence Diagrams (WSD) for specifying sequence diagrams in text format.)

home->+WIP:
WIP->+customerData:
customerData-->-WIP:
WIP->+accountData:
accountData-->-WIP:
WIP-->+finalizeWIP:
finalizeWIP->-home:
WIP-->+cancelWIP:
cancelWIP->-home:

The snippet above shows, for example, that we cannot navigate directly from home to accountData or WIP to home or customerData to finalizeWIP. There are specific transitions that are allowed from each state. Most libraries and frameworks for building applications give you the ability to specify all possible methods, not a means of limiting actions from a given state. This is sufficient to get our first glimpse of a visual representation.

However, we don’t currently have a means of instructing the resource when to move from state to state. We can do this by specifying messages for each transition.

home->+WIP: startOnboarding
WIP->+customerData: collectCustomerData
customerData-->-WIP: saveToWIP
WIP->+accountData: collectAccountData
accountData-->-WIP:saveToWIP
WIP-->+finalizeWIP:completeOnboarding
finalizeWIP->-home:goHome
WIP-->+cancelWIP:abandonOnboarding
cancelWIP->-home:goHome

With these additions, we can see the messages provided to transition from state to state.

Adding parameters provides further context and something we could conceivably call to produce results. The following follows Mike’s formatting from his slides.

home->+WIP: startOnboarding(identifier)
WIP->+customerData: collectCustomerData(identifier,name,email)
customerData-->-WIP: saveToWIP(identifier,name,email)
WIP->+accountData: collectAccountData(identifier,region,discount)
accountData-->-WIP:saveToWIP(identifier,region,discount)
WIP-->+finalizeWIP:completeOnboarding(identifier)
finalizeWIP->-home:goHome
WIP-->+cancelWIP:abandonOnboarding(identifier)
cancelWIP->-home:goHome

Running this specification through the WSD tool produces the following visual rendering, which I find very informative.

Onboarding API Sequence Diagram
Web Sequence Diagram

Implementation

You can get what I’ve presented thus far by reading through Mike’s slides. When I first read through them and visited his code repository, I was hoping to see an implementation of some of the additional directions he proposed. However, to date the repository contains only a CLI for generating the visuals above from the text specifications. With the intent of trying to drive this idea further, I ported the libraries to F#.

In order to show the capabilities of this approach, I planned on implementing several tools:

Unfortunately, life happens and I have not made as much progress as I would like. (Fortunately, I haven’t made too much progress, or you would be settling in for a very long post indeed.) I’m pleased to show a rough proof-of-concept using F# agents that I think demonstrates the utility of this approach nicely.

/// Defines a transition from one state to another state based on a message.
type Transition<'State, 'Message> =
    { FromState : 'State
      Message : 'Message
      ToState : 'State }

/// A resource-oriented agent that transitions the state based on messages received.
type Agent<'State,'Message when 'State : comparison and 'Message : comparison> =
    new : identifier:System.Uri * initState:'State, transitions:Transition<'State, 'Message> list * comparer:('Message * 'Message -> bool) -> Agent<'State,'Message>
    /// Returns the identifier for the agent.
    member Identifier : System.Uri
    /// Retrieves the current state and allowed state transitions.
    member Get : unit -> 'State * Transition<'State,'Message> list
    /// Posts a message to transition from the current state to another state.
    member Post : message:'Message -> unit
    /// Registers a handler to perform a side-effect, e.g. save a value, on a state transition.
    member Subscribe : transition:Transition<'State,'Message> * handler:('Message -> unit) -> unit

With these definitions, we can translate the WSD syntax into types and values.

type State =
    | State of name:string

type Message =
    | Message of name:string * data:string

let createAgent initState =
    let transitions = [
        // home->+WIP: startOnboarding(identifier)
        { FromState = State "home"
          ToState = State "WIP"
          Message = Message("startOnboarding", "identifier") }
        // WIP->+customerData: collectCustomerData(identifier,name,email)
        { FromState = State "WIP"
          ToState = State "customerData"
          Message = Message("collectCustomerData", "identifier,name,email") }
        // customerData-->-WIP: saveToWIP(identifier,name,email)
        { FromState = State "customerData"
          ToState = State "WIP"
          Message = Message("saveToWIP", "identifier,name,email") }
        // WIP->+accountData: collectAccountData(identifier,region,discount)
        { FromState = State "WIP"
          ToState = State "accountData"
          Message = Message("collectAccountData", "identifier,region,discount") }
        // accountData-->-WIP:saveToWIP(identifier,region,discount)
        { FromState = State "accountData"
          ToState = State "WIP"
          Message = Message("saveToWIP", "identifier,region,discount") }
        // WIP-->+finalizeWIP:completeOnboarding(identifier)
        { FromState = State "WIP"
          ToState = State "finalizeWIP"
          Message = Message("completeOnboarding", "identifier") }
        // finalizeWIP->-home:goHome
        { FromState = State "finalizeWIP"
          ToState = State "home"
          Message = Message("goHome", "") }
        // WIP-->+cancelWIP:abandonOnboarding(identifier)
        { FromState = State "WIP"
          ToState = State "cancelWIP"
          Message = Message("abandonOnboarding", "identifier") }
        // cancelWIP->-home:goHome
        { FromState = State "cancelWIP"
          ToState = State "home"
          Message = Message("goHome", "") }
    ]
    Agent(Uri "urn:agent:1", initState, transitions, function (Message(expected,_)), (Message(actual,_)) -> expected = actual)

The F# translation is a bit more verbose than the WSD text format, but the connection can clearly be seen. It’s important to note that the Message type splits the message name and parameters. I’ve kept the parameters as-is for now, but we could easily extend this to use the query parameter portion of URI Templates to specify an expected schema for the parameters against which to validate and extract arguments.

Testing

With these transitions defined, what do we expect to happen? In most apps and APIs, you have access to every supported method or interaction right away. However, we don’t want to expose everything; we want to expose a workflow and restrict user actions. Will the Agent provide the correct response for a given state?

The following tests verify that an Agent in the home state represents that it is in the home state and can only transition to the WIP state with the startOnboarding message.

test "agent starts in 'home' state" {
    let expected = State "home"
    let agent = createAgent (State "home")
    let actual, _ = agent.Get()
    Expect.equal actual expected "Should have been able to transition only to WIP."
}

test "agent can transition to 'WIP' from 'home'" {
    let expected = [
        { FromState = State "home"
          ToState = State "WIP"
          Message = Message("startOnboarding", "identifier") }
    ]
    let agent = createAgent (State "home")
    let _, actual = agent.Get()
    Expect.equal actual expected "Should have been able to transition only to WIP."
}

So far, so good. What happens if we transition to the WIP state?

test "agent transitions to 'WIP' after receiving a message of 'startOnboarding'" {
    let expected =
        State "WIP", [
            { FromState = State "WIP"
              ToState = State "customerData"
              Message = Message("collectCustomerData", "identifier,name,email") }
            { FromState = State "WIP"
              ToState = State "accountData"
              Message = Message("collectAccountData", "identifier,region,discount") }
            { FromState = State "WIP"
              ToState = State "finalizeWIP"
              Message = Message("completeOnboarding", "identifier") }
            { FromState = State "WIP"
              ToState = State "cancelWIP"
              Message = Message("abandonOnboarding", "identifier") }
        ]
    let agent = createAgent (State "home")
    agent.Post(Message("startOnboarding", ""))
    let actual = agent.Get()
    Expect.equal actual expected "Should transition to WIP state with 4 transitions."
}

The Agent represents that it is in the WIP state and can transition from WIP to four other states, just as we specified in our WSD spec. Here are a few more tests for good measure.

test "agent transitions to 'finalizeWIP' after receiving a message of 'completeOnboarding'" {
    let expected =
        State "finalizeWIP", [
            { FromState = State "finalizeWIP"
              ToState = State "home"
              Message = Message("goHome", "") }
        ]
    let agent = createAgent (State "WIP")
    agent.Post(Message("completeOnboarding", ""))
    let actual = agent.Get()
    Expect.equal actual expected "Should transition to finalizeWIP state with 1 transition to home."
}

test "agent transitions to 'home' from 'finalizeWIP' after receiving a message of 'goHome'" {
    let expected =
        State "home", [
            { FromState = State "home"
              ToState = State "WIP"
              Message = Message("startOnboarding", "identifier") }
        ]
    let agent = createAgent (State "finalizeWIP")
    agent.Post(Message("goHome", ""))
    let actual = agent.Get()
    Expect.equal actual expected "Should transition to home state with 1 transition to WIP."
}

test "agent transitions to 'cancelWIP' after receiving a message of 'abandonOnboarding'" {
    let expected =
        State "cancelWIP", [
            { FromState = State "cancelWIP"
              ToState = State "home"
              Message = Message("goHome", "") }
        ]
    let agent = createAgent (State "WIP")
    agent.Post(Message("abandonOnboarding", ""))
    let actual = agent.Get()
    Expect.equal actual expected "Should transition to cancelWIP state with 1 transition to home."
}

test "agent transitions to 'home' from 'cancelWIP' after receiving a message of 'goHome'" {
    let expected =
        State "home", [
            { FromState = State "home"
              ToState = State "WIP"
              Message = Message("startOnboarding", "identifier") }
        ]
    let agent = createAgent (State "cancelWIP")
    agent.Post(Message("goHome", ""))
    let actual = agent.Get()
    Expect.equal actual expected "Should transition to home state with 1 transition to WIP."
}

All the states return the expected representations.

Starting test execution, please wait...

Total tests: 8. Passed: 8. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 1.3494 Seconds

Conclusion

There are clearly a lot more directions in which we could take this. I’m very interested in writing the parser and generating a representation like this, as well as creating some similar implementations for things like Freya, Service Fabric, Azure Functions, etc. I was also surprised at how few lines were required for this implementation (Agent.fs is 73 lines, including white space). I’ve tried doing similar things in the past only to give up because I was overcomplicating the schema format, implementation, or something else.

I hope you’ll give the Sequence Diagram approach a shot, and I would love to know how it does or doesn’t work for you.

You can find all the code for this post at https://github.com/panesofglass/wsd-gen/tree/wsd-agents. Thanks for reading, and Merry Christmas!


Advertisements

2 thoughts on “State Transitions through Sequence Diagrams

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s