Streamlining User Flows with Azure's Durable Functions

A Look into Serverless Orchestration
Engineering

TLDR

  1. Azure Durable Function is a tremendous serverless technology that enables orchestration through code.
  2. Mapping the user activities to azure durable functions activities brings the backend and frontend together, and you should thrive for that.

Azure's durable functions are 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 gets the stuff done on the backend with little care (on you) for infrastructure and scale. It is especially suited for product-driven development in trying 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 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 remain lean.

Firebase’s Function as a service offering 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 fires 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 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 Javascript.

So let’s walk 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 it is being 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 his pizza without the pineapple. 

This is an oversimplified flow but expresses everything we need to show on interactions between a backend and a frontend - against 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. 

How we mary the declarativeness of the user flow within the code of the orchestrator is shown next.

Let’s jump into the code

For this, we have all our code on GitHub accessible here: https://github.com/novoda/aaa

I also recorded a quick video to show how the flow gets executed if you are more a visual learner:

The code is organised between four serverless functions:

  1. The BackendAction serverless: https://github.com/novoda/AAA/blob/main/BackendAction/index.js. This represents any type of backend work. In this case, it sleeps for 2 seconds and pushes the state back to the client.
  2. The Approve HTTP server: https://github.com/novoda/AAA/blob/main/Approve/index.js. You call that HTTP function to fire the Approve event.
  3. The Orchestrator Http starter: https://github.com/novoda/AAA/blob/main/StartOrderOrchestrator/index.js. This HTTP interface starts the orchestrator for that specific device with the push notification registration Id.
  4. The orchestrator itself where the logic resides: https://github.com/novoda/AAA/blob/main/StartOrderOrchestrator/index.js

Let’s dig a bit deeper in 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?

  1. 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 a straightforward idea that opens the door to many possibilities. Restarting the function can happen within one millisecond, one second, one minute, one hour or one week. 
  2. 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.
  3. 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.
  4. 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 front end. Bringing the backend into that mindset can be tricky as the backend usually doesn’t match what is needed from the front end. Closing the gap here between the front end and back end is exciting. It allows teams to have a more unified conversation about how customers experience the services. 

Finally - and to bring more to the conversation on serverless length - 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 has some legs:

  1. A serverless function should be no more than the minimum to achieve a change in the user experience
  2. An orchestrator and its state should map as closely as possible to the user experience in achieving a full product feature.

Interested in durable functions? Join the conversation on Linkedin and tell us about what you're up to. Or, if you want to chat with me directly about using durable functions on your project, drop me an email at carl@novoda.com

Streamlining User Flows with Azure's Durable Functions

A Look into Serverless Orchestration
Engineering

TLDR

  1. Azure Durable Function is a tremendous serverless technology that enables orchestration through code.
  2. Mapping the user activities to azure durable functions activities brings the backend and frontend together, and you should thrive for that.

Azure's durable functions are 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 gets the stuff done on the backend with little care (on you) for infrastructure and scale. It is especially suited for product-driven development in trying 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 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 remain lean.

Firebase’s Function as a service offering 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 fires 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 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 Javascript.

So let’s walk 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 it is being 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 his pizza without the pineapple. 

This is an oversimplified flow but expresses everything we need to show on interactions between a backend and a frontend - against 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. 

How we mary the declarativeness of the user flow within the code of the orchestrator is shown next.

Let’s jump into the code

For this, we have all our code on GitHub accessible here: https://github.com/novoda/aaa

I also recorded a quick video to show how the flow gets executed if you are more a visual learner:

The code is organised between four serverless functions:

  1. The BackendAction serverless: https://github.com/novoda/AAA/blob/main/BackendAction/index.js. This represents any type of backend work. In this case, it sleeps for 2 seconds and pushes the state back to the client.
  2. The Approve HTTP server: https://github.com/novoda/AAA/blob/main/Approve/index.js. You call that HTTP function to fire the Approve event.
  3. The Orchestrator Http starter: https://github.com/novoda/AAA/blob/main/StartOrderOrchestrator/index.js. This HTTP interface starts the orchestrator for that specific device with the push notification registration Id.
  4. The orchestrator itself where the logic resides: https://github.com/novoda/AAA/blob/main/StartOrderOrchestrator/index.js

Let’s dig a bit deeper in 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?

  1. 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 a straightforward idea that opens the door to many possibilities. Restarting the function can happen within one millisecond, one second, one minute, one hour or one week. 
  2. 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.
  3. 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.
  4. 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 front end. Bringing the backend into that mindset can be tricky as the backend usually doesn’t match what is needed from the front end. Closing the gap here between the front end and back end is exciting. It allows teams to have a more unified conversation about how customers experience the services. 

Finally - and to bring more to the conversation on serverless length - 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 has some legs:

  1. A serverless function should be no more than the minimum to achieve a change in the user experience
  2. An orchestrator and its state should map as closely as possible to the user experience in achieving a full product feature.

Interested in durable functions? Join the conversation on Linkedin and tell us about what you're up to. Or, if you want to chat with me directly about using durable functions on your project, drop me an email at carl@novoda.com