The “Tick” Pattern — A Solution for Temporal Problems in State Machines
How to automate workflows that wait for external and time conditions without exploding complexity
So you have to program stuff like:
- Send a reminder email after one day
- Process an order after the delivery date has been reached
- Mark a document as signed based on the status of an external service
These processes need some form of organization since you can’t just sleep
your process for an hour to continue a job. This is where the Tick pattern comes in.
The Tick Pattern
You might already know the term “tick.” Wikipedia describes it pretty well:
A single update of a game simulation is known as a tick […]
We usually aren’t programming games in Symfony, but we can still borrow this concept and use the catchy name.
What Is the Plan?
I suggest building a command/task/job that performs “the tick.”
“The tick” is just a periodic check if a state machine transition is possible. That allows you to define all sorts of conditions (date, inventory, user confirmation) in plain code without having to find ways to execute your logic once the condition is met.
Here is a list of reasons you might want to handle tasks like this:
- One entry point: You only need one cron job compared to a cron job for every async task.
- Simpler tooling: You only need to build monitoring and logging around one asynchronous tasks
- Robustness. If something fails, it’ll just rerun on the next tick. Think about unavailable apis or SMTP servers. With this pattern, it’ll just retry.
- Changeable: Changing delivery dates directly in the database or changing delays in a new version. All those changes are instantly reflected without migrations or special code paths
But, the pattern has some problems:
- Scaling: The more entities there are, the longer the tick will take.
- Wasteful: Every entity in some states will be checked on every tick.
Defining the Tick in Metadata
I’ll now get specific to the symfony workflow component, but you can probably adapt it to other state machines.
You want to be able to easily read the behavior in your application. State machine definitions already help, but did you know that you can add arbitrary metadata to a symfony workflow definition?
transitions:
deliver:
from: new
to: delivering
guard: subject.checkPreconditions()
metadata:
on_tick: true
It really is just arbitrary data, but that means we can describe new behavior for usage later. In this case, I’ll define on_tick
, which is just a flag for us that we want to check later.
I also used the guard
property to define a condition on when the state is allowed to change. This is nicely readable but fairly limited, you should use Guard Events instead.
Building the Tick Service
You’ll want to easily perform a tick, so let’s build a small Service
to do it. Here’s the code:
With this simple service, you can just run $service->tick($order)
to run pending transitions. This can be useful right after creating the entity to start a process immediately. Or not, if there is a condition currently blocking it.
Things that could also make sense in this service:
- A dry run method like
simulate
so you can check if there is something pending without actually doing it - A method to return all transition blockers so you can give explanations why an Order is stuck
Building the Cron Command
We’ll now need a command to trigger ticks from outside our programs world, usually a cron job. But you’ll also want to be able to manually trigger single ticks, so the CLI should be a bit fancy.
The command has to be specific to the underlying database, so I create the tick command for my example of an Order
, but you can adjust it to whatever you like or even let it run through multiple entity types. Here’s more code:
With this command, you can now run order:tick
and all transitions marked as on_tick: true
will be executed if they aren’t blocked by other conditions.
One small trick I use here is injecting the state machine directly using named autowiring with the argument $orderStateMachine
to explicitly inject the state machine with the name order
. You usually get your state machines form the Registry
service, but that has no way of accessing the state machine without having the object. And here, we want the state machine first to create a list of states, that have an on_tick
transition.
This command also accepts a list of ids as an argument. That way, you can run a tick on a specific order. This is really useful for testing.
Things you might want to improve yourself:
- Prioritizing of specific entities (eg. process older orders first)
- Loading entities/orders in batch to improve throughput
- You could try to run through the id list using multiple processes. Either by spawning processes yourself or by piping all id’s though
echo [id list] |xargs -P4 -n100 php bin/console order:tick
.
Now It’s Your Turn
Did I miss anything? Did I help you? Tell me in the comments.