What are Durable Functions?

Avatar of Sarah Drasner
Sarah Drasner on

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

Oh no! Not more jargon! What exactly does the term Durable Functions mean? Durable functions have to do with Serverless architectures. It’s an extension of Azure Functions that allow you to write stateful executions in a serverless environment.

Think of it this way. There are a few big benefits that people tend to focus on when they talk about Serverless Functions:

  • They’re cheap
  • They scale with your needs (not necessarily, but that’s the default for many services)
  • They allow you to write event-driven code

Let’s talk about that last one for a minute. When you can write event-driven code, you can break your operational needs down into smaller functions that essentially say: when this request comes in, run this code. You don’t mess around with infrastructure, that’s taken care of for you. It’s a pretty compelling concept.

In this paradigm, you can break your workflow down into smaller, reusable pieces which, in turn, can make them easier to maintain. This also allows you to focus on your business logic because you’re boiling things down to the simplest code you need run on your server.

So, here’s where Durable Functions come in. You can probably guess that you’re going to need more than one function to run as your application grows in size and has to maintain more states. And, in many cases, you’ll need to coordinate them and specify the order in which they should be run for them to be effective. It’s worth mentioning at this point that Durable Functions are a pattern available only in Azure. Other services have variations on this theme. For example, the AWS version is called Step Functions. So, while we’re talking about something specific to Azure, it applies more broadly as well.

Durable in action, some examples

Let’s say you’re selling airline tickets. You can imagine that as a person buys a ticket, we need to:

  1. check for the availability of the ticket
  2. make a request to get the seat map
  3. get their mileage points if they’re a loyalty member
  4. give them a mobile notification if the payment comes through and they have an app installed/have requested notifications

(There’s typically more, but we’re using this as a base example)

Sometimes these will all run be run concurrently, sometimes not. For instance, let’s say they want to purchase the ticket with their mileage rewards. Then you’d have to first check the awards, and then the availability of the ticket. And then do some dark magic to make sure no customers, even data scientists, can actually understand the algorithm behind your rewards program.

Orchestrator functions

Whether you’re running these functions at the same moment, running them in order, or running them according to whether or not a condition is met, you probably want to use what’s called an orchestrator function. This is a special type of function that defines your workflows, doing, as you might expect, orchestrating the other functions. They automatically checkpoint their progress whenever a function awaits, which is extremely helpful for managing complex asynchronous code.

Without Durable Functions, you run into a problem of disorganization. Let’s say one function relies on another to fire. You could call the other function directly from the first, but whoever is maintaining the code would have to step into each individual function and keep in their mind how it’s being called while maintaining them separately if they need changes. It’s pretty easy to get into something that resembles callback hell, and debugging can get really tricky.

Orchestrator functions, on the other hand, manage the state and timing of all the other functions. The orchestrator function will be kicked off by an orchestration trigger and supports both inputs and outputs. You can see how this would be quite handy! You’re managing the state in a comprehensive way all in one place. Plus, the serverless functions themselves can keep their jobs limited to what they need to execute, allowing them to be more reusable and less brittle.

Let’s go over some possible patterns. We’ll move beyond just chaining and talk about some other possibilities.

Pattern 1: Function chaining

This is the most straightforward implementation of all the patterns. It’s literally one orchestrator controlling a few different steps. The orchestrator triggers a function, the function finishes, the orchestrator registers it, and then then next one fires, and so on. Here’s a visualization of that in action:

See the Pen Durable Functions: Pattern #1- Chaining by Sarah Drasner (@sdras) on CodePen.

Here’s a simple example of that pattern with a generator.

const df = require("durable-functions")

module.exports = df(function*(ctx) {
  const x = yield ctx.df.callActivityAsync('fn1')
  const y = yield ctx.df.callActivityAsync('fn2', x)
  const z = yield ctx.df.callActivityAsync('fn3', y)
  return yield ctx.df.callActivityAsync('fn3', z)
})

I love generators! If you’re not familiar with them, check out this great talk by Bodil on the subject).

Pattern 2: Fan-out/fan-in

If you have to execute multiple functions in parallel and need to fire one more function based on the results, a fan-out/fan-in pattern might be your jam. We’ll accumulate results returned from the functions from the first group of functions to be used in the last function.

See the Pen Durable Functions: Pattern #2, Fan Out, Fan In by Sarah Drasner (@sdras) on CodePen.

const df = require('durable-functions')

module.exports = df(function*(ctx) {
  const tasks = []

  // items to process concurrently, added to an array
  const taskItems = yield ctx.df.callActivityAsync('fn1')
  taskItems.forEach(item => tasks.push(ctx.df.callActivityAsync('fn2', item))
  yield ctx.df.task.all(tasks)

  // send results to last function for processing
  yield ctx.df.callActivityAsync('fn3', tasks)
})

Pattern 3: Async HTTP APIs

It’s also pretty common that you’ll need to make a request to an API for an unknown amount of time. Many things like the distance and amount of requests processed can make the amount of time unknowable. There are situations that require some of this work to be done first, asynchronously, but in tandem, and then another function to be fired when the first few API calls are completed. Async/await is perfect for this task.

See the Pen Durable Functions: Pattern #3, Async HTTP APIs by Sarah Drasner (@sdras) on CodePen.

const df = require('durable-functions')

module.exports = df(async ctx => {
  const fn1 = ctx.df.callActivityAsync('fn1')
  const fn2 = ctx.df.callActivityAsync('fn2')

  // the responses come in and wait for both to be resolved
  await fn1
  await fn2

  // then this one this one is called
  await ctx.df.callActivityAsync('fn3')
})

You can check out more patterns here! (Minus animations. 😉)

Getting started

If you’d like to play around with Durable Functions and learn more, there’s a great tutorial here, with corresponding repos to fork and work with. I’m also working with a coworker on another post that will dive into one of these patterns that will be out soon!

Alternative patterns

Azure offers a pretty unique thing in Logic Apps, which allows you the ability to design workflows visually. I’m usually a code-only-no-WYSIWYG lady myself, but one of the compelling things about Logic Apps is that they have readymade connectors with services like Twilio and SendGrid, so that you don’t have to write that slightly annoying, mostly boilerplate code. It can also integrate with your existing functions so you can abstract away just the parts connect to middle-tier systems and write the rest by hand, which can really help with productivity.