ETA workflow notification

This week, a startup doing some vehicle tracking asked me if Zenaton could be used to send an ETA (estimated time of arrival) alert. The idea is to send an alert (text, email, push notification, etc) one hour before a vehicle arrives at its destination.

So here is an example of how we could build a workflow using Zenaton and accomplish this feature without using any additional infrastructure — just write it into our code let the Zenaton workflow engine compute the logic and execute the tasks on our workers.

The following examples are presented in PHP, but are similar in other languages supported by Zenaton. If there is something you do not understand, please read the documentation and/or leaves us a comment below.

Calculate ETA and send a notification to the User

The first implementation is really easy:

<?php
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Traits\Zenatonable;
use Zenaton\Tasks\Wait;
class NotifyEtaWorkflow implements WorkflowInterface
{
    use Zenatonable;
    // inform user # seconds before ETA
    const BEFORE = 3600;
    // trip id
    protected $tripId;
    // user to notify
    protected $user;
    public function __construct($tripId, $user)
    {
        $this->tripId = $tripId;
        $this->user = $user;
    }
    public function handle()
    {
        // calulate current duration & ETA
        [$duration, $eta] = (new CalculateTimeToArrivalTask($this->tripId))->execute();
        // wait until ETA - 1hour
        (new Wait())->timestamp($eta - self::BEFORE)->execute();
        // notify user
        (new NotifyUserOfEtaTask($this->user, $eta))->execute();
    }
}

This workflow is easily readable:

  • first we calculate the current ETA (the CalculateTimeToArrivalTask returns an array [$duration, $eta]. Remember: in order to ensure idempotency, we cannot use current time in a workflow implementation);
  • then, we wait up to ETA minus 1 hour (3600 seconds);
  • then, we notify the user (the NotifyUserOfEtaTask is where we code the sending of the ETA notification to the user).

To launch such a workflow — eg., when the vehicle starts its trip —we add the following

 (new NotifyEtaWorkflow($tripId, $user))->dispatch();

Improved implementation: Update ETA based on traffic conditions

Of course, in real life the ETA will change over time due to traffic conditions or driver behavior. A routing engine will regularly update this ETA and we need to take this into account for an optimal user experience.

So we can add a while loop the will recalculate the ETA until $duration is close enough to 1 hour. A very simple algorithm is used to avoid putting too much stress on the routing engine used in CalculateTimeToArrivalTask (note: it would have been really complicated to do the same using crons).

<?php
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Traits\Zenatonable;
use Zenaton\Tasks\Wait;
class NotifyEtaWorkflow implements WorkflowInterface
{
    use Zenatonable;
    // inform user # seconds before ETA
    const BEFORE = 3600;
    // precision target
    const PRECISION = 120;
    // trip id
    protected $tripId;
    // user to notify
    protected $user;
    public function __construct($tripId, $user)
    {
        $this->tripId = $tripId;
        $this->user = $user;
    }
    public function handle()
    {
        // calulate current ETA
        [$duration, $eta] = $this->getTimeToArrival();
        // retry calculation until $duration is close enough to 1h
        while ($duration > self::BEFORE + self::PRECISION) {
            // wait half of lasting time before notification
            (new Wait())->seconds(($duration - self::BEFORE) / 2)->execute();
            // recalculate duration and eta
            [$duration, $eta] = $this->getTimeToArrival();
        }
        // send message
        (new InformUserOfEtaTask($this->user, $eta))->execute();
    }
    protected function getTimeToArrival()
    {
        return (new CalculateTimeToArrivalTask($this->tripId))->execute();
    }
}

Final implementation: Notify the user if ETA changes by more than 20 minutes

The previous implementation works, but once we’ve put this into production, we realise that we can have modified traffic conditions AFTER having sent the first notification. As such, we may want to also warn the user if the ETA changes by a large amount (eg., more than plus or minus 20 min.) after they receive the first notification.

So, After the first notification, we recalculate ETA every 20 minutes, to check if the ETA is updated to be more than 20 min from the previous ETA notification, then we send an “updated ETA” notification.

This workflow ends when $duration is close enough to 0.

See how we have coded this below:

<?php
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Traits\Zenatonable;
use Zenaton\Tasks\Wait;
class NotifyEtaWorkflow implements WorkflowInterface
{
    use Zenatonable;
    // inform user # seconds before ETA
    const BEFORE = 3600;
    // precision target
    const PRECISION = 120;
    // update threshold : inform user if ETA changed more than # seconds
    const UPDATE = 1200;
    // trip id
    protected $tripId;
    // user to notify
    protected $user;
    public function __construct($tripId, $user)
    {
        $this->tripId = $tripId;
        $this->user = $user;
    }
    public function handle()
    {
        // calulate current ETA
        [$duration, $eta] = $this->getTimeToArrival();
        // retry calculation until $duration is close enough to 1h
        while ($duration > self::BEFORE + self::PRECISION) {
            // wait half of lasting time before notification
            (new Wait())->seconds(($duration - self::BEFORE) / 2)->execute();
            // recalculate duration and eta
            [$duration, $eta] = $this->getTimeToArrival();
        }
        // send message
        (new InformUserOfEtaTask($this->user, $eta))->execute();
        // inform user of significant change of ETA until arrival
        while ($duration > self::PRECISION) {
            // wait 20 min
            (new Wait())->seconds(self::UPDATE)->execute();
            // recalculate duration and eta
            [$duration, $eta2] = $this->getTimeToArrival();
            // if new eta changed significantly, send new notification
            if (abs($eta2 - $eta) >= self::UPDATE) {
                // inform user of updated ETA
                $eta = $eta2;
                (new NotifyUserOfUpdatedEtaTask($this->user, $eta))->execute();
            }
        }
    }
    
    protected function getTimeToArrival()
    {
        return (new CalculateTimeToArrivalTask($this->tripId))->execute();
    }
}

So just to recap, with about 20 lines of PHP code, Zenaton can orchestrate the process of sending a notification 1 hour before arrival (taking traffic conditions into account) AND a new notification each time the ETA changes by more than 20 minutes relative to the previous notification.

Conclusion

This example illustrates just how easy Zenaton is to use to implement powerful and readable workflows without having to deal with states or cron, without any database modification or request.

One of the most powerful aspects of Zenaton is demonstrated here — you can really fine-tune and improve your business workflows, literally in just minutes.

Zenaton (https://zenaton.com) is for technical teams that understand that their primary mission is to improve the business through quick iterations and new ideas, not spending most of their time solving purely technical issues. If you have additional questions, feel free to contact me at gilles at zenaton.com or to ask them below 👇