Sources of Variability in a Directed State Pattern

Although a Directed State pattern looks rather straightforward on paper,  there are an infinite number of variations in the way it can be modeled in practice. There is no single, right way to do it: a lot depends on the particular use case you are implementing.

The most common sources of variability in the eventual modeling are:

  • Life cycle of the Desired State
  • Are State instances creatable or deletable on request?
  • Is the process of evolving a Current State towards the Desired State quick or slow?
  • Is there a need for error feedback?
  • Should the Requester know whether a state change was done on his behalf?
  • Does it make sense to initiate state changes that were requested based on an outdated view of the state?

In this section, we discuss each of the aforementioned sources of modeling variability, and provide some suggestions on how to tackle specific situations.

Life Cycle of the Desired State

The Requester's Desired State is (as the name implies) conceptually a form of State. However, modeling it as such has certain implications on the structure and handling of the Desired State topic:

  • Because State is exclusively owned by its publisher, the Desired State topic cannot have the exact same structure as the Current State topic. Rather, at least one extra field identifying the Requester should be added to the key of the data structure, otherwise it would become impossible for multiple Requesters to express a Desired State for the same instance concurrently (otherwise, both publications have the same key, and the exclusivity rules will consistently hide one in favor of the other).
  • Desired State instances have a life cycle that needs to be managed: at some point, the request is no longer relevant (either because it's fulfilled, or because the Requester lost interest and gave up). The Requester has to keep track of all extant Desired States it published, and must clean them up when they are not longer relevant.

Therefore, in many cases it is pragmatic (though conceptually less clean) to represent the Desired State as Event rather than State. This makes publishing a Desired State a simple fire-and-forget operation. This has some semantic implications:

  • Desired State samples arrive at the Effector as a sequential stream, suggesting they will be handled one-by-one in the order they come in.
  • Automatic conflict resolution: two Requesters that concurrently issue conflicting Desired States for a given instance will just appear as two consecutive requests to the Effector
  • No revocation: if Desired State is a State, it is possible to revoke an existing request by just disposing of the Desired State instance. The fire-and-forget nature of Events makes this impossible.
  • The Effector cannot be late: if the Effector is not yet active or subscribed to the Desired State topic, it will miss Desired State publications, and it will not be able to act upon them. Typically, this is not a problem, as the non-existence of the Effector means no Current State is published and hence there is nothing for which to specify Desired State. If Current State instances can be created upon request however, this equation changes: perhaps it makes sense to have the Requester publish the request to create a new instance without first having to check whether an Effector is already up and running.

Are Current State Instances Creatable/Deletable on Request

Requesters can only express their desire to change the state of existing Current State instances, not their desire for the creation or deletion of such instances.

Once creation and deletion come into play, a number of extra questions need to be answered:

  • Are create and delete requests modeled on the same topic as the update (Desired State) requests?
  • Will Requesters have the need to specify explicitly which Effector should create the Current State instance (consider "I want to create a firewall rule for that specific firewall" versus "I want to set up a phone call but I don't care who handles it for me")?

To the extent possible, we advocate to use the existing Desired State topic to represent all requests (Create, Delete and Update). This is the most economical and clean way to model this. A good reason to deviate from this rule of thumb would be the differing life cycle of one type of request versus the other (maybe Create requests are State, whereas Update requests are Event?).

If the Create requests are undirected (i.e. any Effector may handle them), it is necessary to define some kind of protocol to handle the fact that multiple Effectors may step in to handle the request, resulting in the creation of multiple Current State instances instead of just the one that was needed. Some alternatives here:

  • Let the Requester issue explicit Delete requests for each of the spuriously created instances
  • Let Effectors create the new instances in a kind of "limbo" state. The Requester sees many instances being created in response to its request, and picks the one it likes most (typically the first one). For this particular instance, it publishes a new Desired State to pull it out of the "limbo" state. All other Effectors observe the state change on the alternative instance, and conclude they can dispose of their "limbo" instances, as they are not chosen by the Requester.

Whatever option one chooses, this must be documented explicitly in the QDM file

Is the Process of Evolving Current State Towards Desired State Quick or Slow

In case the actual evolution of Current State towards the desired end state takes a lot of time (dependent on a slow underlying physical process) or traverses through a lot of intermediate states, it may make sense for the Effector to advertise in which direction it is evolving the Current State, by publishing in addition a so-called Objective State. This allows the Effector to advertise to the world "this is what I'm headed for". Requesters can then observe both the Current State and Objective State, and make informed decisions based on both.

The Objective State, just like the Current State, would be modeled with the State behavior. The structure of both types (including key fields) should be identical. It is advisable to publish an Objective State instance for each Current State instance "owned" by the Effector. This allows us to assign a meaning to discrepancies in the existence of instances on both topics:

  • an instance exists as Current State but not as Objective State: it is en route to be destroyed.
  • an instance exists as Objective State but not as Current State: it is in the process of being created.

Is There a Need for Error Feedback

In general, try to avoid interaction patterns with explicit "return code"-like behavior. They inevitably lead to RPC-like tightly-coupled interactions. However, in some cases it may be relevant for the Effector to supply error information based on the Desired State it observes. In that case, define a second topic (as an Event) where the error codes/situations/explanations can be advertised to the world.

Should the Requester Know Whether a State Change Was Done On his Behalf

When the Requester absolutely needs to know (again, something to avoid if possible) whether a given state change was the result of its own Desired State, you can model this as follows:

  1. Put a unique id in the Desired State
  2. Put a field "inResponseTo" in the Current State, which replicates the unique id from the Desired State that triggered the Current State to move to its current level.

Does It Make Sense to Initiate State Changes that Were Requested Based on an Outdated View of the State

In some cases, it does not make sense to express a Desired State based on an outdated view of the Current State. Imagine a situation where there is one Effector and two Requesters (A and B). Both requesters observe Current State S0, and concurrently decide to publish a Desired State S0A (by A) and S0B (by B). These decisions make sense in the context of Current State S0. The Effector sees A's desired state first, and as a consequence it evolves the Current State towards S1 (not necessarily equal to S0A). In the context of S1, Requester B's decision to publish a Desired State S0B is no longer valid, but there is a time window in which the Effector will see Desired State S0B while Requester B still has not noticed Current State S1, and hence has not yet updated its Desired State to the new correct value S1B.

One may avoid this kind of situation by adding a discrete time stamp to the data model, with the following semantics:

  • The time stamp is an integer counter added to the Current State
  • Each new value of an instance on the Current State increments the counter
  • Desired State incorporates an "in-the-context-of" field that holds the value of the timestamp counter of the Current State value from which this Desired State was inferred.
  • The Effector ignores each Desired State value that has an "in-the-context-of" field that is out of date with respect to the current timestamp of the affected instance.