TLDR
- Azure Durable Functions is a tremendous serverless technology that enables orchestration through code.
- Mapping user journeys to Azure Durable Functions activities brings the backend and frontend together, and you should strive for that.
Azure's Durable Functions is a remarkable technology that has piqued my interest. After exploring serverless technology for a while, I find it incredibly efficient for back-end for front-end (BFF) development with minimal concern for infrastructure and scalability. It allows you to get stuff done on the backend, while taking care of the infrastructure and scale for you. It is especially suited for product-driven development, allowing you to express user flows at the front end of a mobile app and in the back end, where the interactions with your services get executed. In the past, my goto technology has always been Firebase Cloud Functions as its integration to the Firebase suite of technologies makes its use super easy for building prototypes. Firebase takes the developer experience to a new level if you mainly target front-end mobile and your infrastructure remains lean.
Firebase’s Function as a service is good but can get overwhelmingly confusing as your application grows in complexity. That is especially true when you start attaching functions to database events. It becomes a bit of a firework of change; one field is updated here that triggers a function that changes another bit on another part of the database that triggers another function - and so on. The whole process takes a couple of seconds (or minutes), and suddenly, your result is NOT what you expected. Now what? You need a way to organise the execution of your functions. A way to make sense of your infrastructure and reason about the flow of execution within it.
Enter orchestration. Orchestrations attempt to coordinate and manage multiple serverless functions to execute a larger, more complex workflow. With AWS, you have Step Functions as an orchestration technology. At Google cloud, you have the new Google Workflow. Both use configuration language (YAML) to glue the functions together. The idea is that the orchestration orchestrates the execution of the serverless functions. When one finishes, it starts the second one. Or one starts a few others and waits for each to finish before continuing. Even an async for-loop - fan in/fan out - can be a bit tricky in serverless land. Orchestration aims to solve that in a distributed fashion.
As I explored those challenges a few years ago, I came across Azure’s durable functions. And I fell in love with the technology. It expresses the orchestration the same way you would write a serverless function - within your code. This is especially interesting for languages with async generators, such as JavaSscript.
So let’s go over an example and see it in action.
Let’s say we have an imaginary delivery company. It does the usual user registration, menu search, food ordering, etc. In this example, we will focus our attention on ordering an excellent pineapple pizza from a Neapolitan restaurant.
The user flow might look something like this:
- The user orders the pineapple pizza, and the order is received at the restaurant
- The restaurant prepares the pizza and processes the order.
- But the pizzeria just can’t process the order because pineapple on a pizza is just a no go, so it is asking the user to accept a change in the order
- The user changes the order via the mobile application
- The user receives their pizza without the pineapple.
I know, I know, it is a bit tongue in cheek but we wanted to represent several interactions that flow from the backend to the frontend but also from frontend to backend. If we express the user flow in a diagram, it might look like this:
This is an oversimplified flow but it expresses everything we need to show on interactions between a backend and a frontend - in both directions. For each stage of the flow, we want to push a state back to the user’s device (i.e. via push notifications), and - as developers - we would like to make sense of where we are within that flow. We should also be able to easily reason about it so we can change as our business grows and need to add (or remove) steps in that flow.
Declarative expression of the user flow in the code of the orchestrator is shown next.
Let’s jump right in
For this; we have all our code on GitHub accessible here
I also recorded a quick video to show how the flow gets executed if you are more of a visual learner.
The code is organised between four serverless functions:
- The BackendAction serverless. This represents any type of backend work. In this case, it sleeps for 2 seconds and pushes the state back to the client.
- The Approve Http server. You call that HTTP function to fire the Approve event
- The Orchestrator Http starter. This is the HTTP interface that starts the orchestrator for that specific device with the push notification registration Id.
- The orchestrator itself where the logic resides
Let’s dig a bit deeper into the orchestrator.
import { orchestrator } from "durable-functions";
/**
* This orchestrator starts with the Push notification ID as regID.
*/
export default orchestrator(function* (context) {
const regId = context.bindings.context.input.regId
const outputs = [];
/**
* We received the order.
* The backend action is processing the order
*/
outputs.push(yield context.df.callActivity("BackendAction", { "orderStatus": "received", "regId": regId }));
/**
* We are waiting for the order to be processed
*/
outputs.push(yield context.df.callActivity("BackendAction", { "orderStatus": "processing", "regId": regId }));
/**
* The flow requires a user action for approval
*/
const approved = yield context.df.waitForExternalEvent("Approval");
/**
* Finally the order has been processed
*/
outputs.push(yield context.df.callActivity("BackendAction", { "orderStatus": "finished", "regId": regId }));
return outputs;
});
So what do we have here?
- First, the function is a generator expressed as
function*()
. A generator allows you to yield the execution back to the caller. Practically it means at the first instance of a yield, the function stops and awaits the caller to resurrect it with the value returned by the caller. This is a straightforward idea that opens the door to many possibilities. The function can be restarted after one millisecond, one second, one minute, one hour or one week.
To call activities in the backend, we use a durable function client that calls those activities and yields back the actual execution of those calls to the caller: ‘’’yield context.df.callActivity’’’. What it means is that the orchestrator serverless function will stop executing (and you are not paying for it to wait on the result) until the activity that it calls returns a result.
We can also wait for the user to do ‘something’ in the “waitForExternalEvent”. The execution will not continue until the event is fired. Again, you are not paying for the time the orchestrator is waiting on that event. When we do an HTTP call to the Approve function, the execution within the orchestrator will continue.
And finally, we return the results. There is not much use of the result here, but you could have the state of the orchestrator as a way to validate its final status. I would even consider the user having access to the status of the orchestrator as a meaningful expression of the flow.
That is about it.
The concept of yielding the execution back to the framework and focusing on expressing your user flows in code has tremendous power. For one, it is easy to modify and reason about. If you need another backend execution, just add a call to “callActivity”. If you need to wait for another event, you can add another call to the code. What I like the most is how well the workflow maps to the user experience.
We usually work with product owners that focus on the front end as a way to express features. We talk about the experience of the user using the frontend. Bringing the backend into that mindset can be a bit tricky as the backend usually doesn’t match what is needed from the frontend. Closing the gap here between the frontend and backend is exciting. It gives teams the ability to have a more unified conversation about how customers experience the services.
Finally I have explored how much activity should hold as logic and how the orchestrator should express it. Not quite there yet but I feel the following statements are a good starting point:
- A serverless function should be no more than the minimum to achieve a change in the user experience
- An orchestrator and its state should map as closely as possible to the user experience in achieving a full product feature.
Top comments (0)