In Part 1, you built out a simple shopping cart app using a long-living Workflow to track the state of the cart. Instead of storing the cart in a database, Temporal lets you represent the cart as a function invocation, using Signals to update the cart and Queries to get the state of the cart.
The long-living Workflow pattern doesn't offer any substantial benefits in this simple CRUD app. However, some tasks are trivial to handle using the long-living Workflow pattern than in a traditional web application setup. As an example, let's take a look at implementing an abandoned cart email notification system.
In eCommerce, an abandoned shopping cart is a shopping cart that has items, but the user hasn't added any new items or checked out after a few hours. The following is an example of an abandoned cart email that I recently received with an offer to incentivize checkout.
In a traditional web app architecture, abandoned cart notifications are tricky. You need to use a job queue like [Celery](https://en.wikipedia.org/wiki/Celery_(software\)) in Python or Machinery in GoLang. Then, you would schedule a job that checks if the cart is abandoned, and reschedule that job every time the cart is updated.
With Temporal, you don't need a separate job queue. Instead, you define a Selector with two event handlers: one that responds to a Workflow signal and one that responds to a timer.
By creating a new Selector on each iteration of the
for loop, you're telling Temporal to handle the next update cart signal it receives or send an abandoned cart email if it doesn't receive a signal for
Select() on a Selector blocks the Workflow until there's either a signal or
Temporal's GO SDK Selectors make it easy to orchestrate asynchronous signals in the Workflow logic, like responding to either user input or an abandoned cart timeout.
You do not need to implement a job queue, write a separate worker, or handle rescheduling jobs.
All you need to do is create a new Selector after every signal and use
AddFuture() to defer code that needs to happen after the associated timeout is selected.
Temporal does the hard work of persisting and distributing the state of your Workflow for you.
Next, let's take a closer look at Activities and the
ExecuteActivity() call above that is responsible for sending the abandoned cart email.
You can think of Activities as an abstraction for side effects in Temporal. Workflows should be pure, idempotent functions to allow Temporal to re-run a Workflow to recreate the Workflow's state. Any side effects, like HTTP requests to the Mailgun API, should be in an Activity.
For example, the following blob of code contains the implementation of the
It loads Mailgun keys from environment variables and sends an HTTP request to the Mailgun API using Mailgun's official Go library.
The function takes two parameters: the Workflow context and the email as a string.
As a reminder, the code below is the
ExecuteActivity() call from the cart Workflow.
The third parameter to
ExecuteActivity() becomes the second parameter to
ExecuteActivity() function also exposes some neat options.
For example, since Temporal automatically retries failed activities, it would automatically retry the
SendAbandonedCart() Activity for up to 5 times if
SendAbandonedCart() returns an error.
You can configure how long Temporal will take while attempting to execute your Activity (including setting a retry policy), with
Long-living Workflows in Temporal are excellent for scheduled tasks. You can build durable time-based logic, like checking whether the user hasn't modified their shopping cart for a given period of time without using a job queue. In the next post, we'll look at patterns for unit testing Temporal Workflows.