The post is a part of F# Advent Calendar 2018. It’s Christmas time!
This summer I was hired by the office of Santa Claus. Santa is not just a fairy tale character on his own — he leads a large organization that supplies gifts and happiness to millions of children around the globe. Like any large organization, Santa’s office employs an impressive number of IT systems.
As part of its IT modernization effort, North Pole HQ restructured the whole supply chain of Christmas gifts. Many legacy components were moved from a self-managed data center at the North Pole — although the cooling is quite cheap there — to Azure cloud. Azure was an easy sell since Santa’s techy elves use Office 365, SharePoint and the .NET development stack.
One of the goals of the redesign was to leverage managed cloud services and serverless architecture wherever possible. Santa has no spare elves to keep reinventing IT wheels.
My assignment was to redesign the Wish Fulfillment service. The service receives wish lists from clients (they call children “clients”):
Christmas Card with a Wish List © my son Tim
Luckily, the list is already parsed by some other service, and also contains the metadata about the kid’s background (age, gender, and so on) and preferences.
For each item in the list, our service calls the Matching service, which uses machine learning, Azure Cognitive services, and a bit of magic to determine the actual products (they call gifts “products”) that best fit the client’s expressed desire and profile. For instance, my son’s wish for “LEGO Draak” matches to “LEGO NINJAGO Masters of Spinjitzu Firstbourne Red Dragon”. You get the point.
There might be several matches for each desired item, and each result has an estimate of how likely it is to fulfill the original request and make the child happy.
All the matching products are combined and sent over to the Gift Picking service. Gift Picking selects one of the options based on its price, demand, confidence level, and the Naughty-or-Nice score of the client.
The last step of the workflow is to Reserve the selected gift in the warehouse and shipping system called “Santa’s Archive of Products”, also referred to as SAP.
Here is the whole flow in one picture:
Gift Fulfillment Workflow
How should we implement this service?
The Wish Fulfillment service should run in the cloud and integrate with other services. It should be able to process millions of requests in December and stay very cheap to run during the rest of the year. We decided to leverage serverless architecture with Azure Functions on the Consumption Plan. Serverless Functions are:
Here is the diagram of the original design:
Workflow Design with Azure Functions and Storage Queues
We used Azure Storage Queues to keep the whole flow asynchronous and more resilient to failures and load fluctuation.
This design would mostly work, but we found a couple of problems with it:
To improve on these points, we decided to try Durable Functions — a library that brings workflow orchestration to Azure Functions. It introduces several tools to define stateful, potentially long-running operations, and handles a lot of the mechanics of reliable communication and state management behind the scenes.
If you want to know more about what Durable Functions are and why they might be a good idea, I invite you to read my article Making Sense of Azure Durable Functions (20 minutes read).
For the rest of this post, I will walk you through the implementation of the Wish Fulfillment workflow with Azure Durable Functions.
A good design starts with a decent domain model. Luckily, the project was built with F# — the language with the richest domain modeling capabilities in the .NET ecosystem.
Our service is invoked with a wish list as the input parameter, so let’s start with the type WishList
:
It contains information about the author of the list and recognized “order” items. Customer
is a custom type; for now, it's not important what's in it.
For each wish we want to produce a list of possible matches:
The product is a specific gift option from Santa’s catalog, and the confidence is a number from 0.0
to 1.0
of how strong the match is.
The end goal of our service is to produce a Reservation
:
It represents the exact product selection for the specific kid.
The Wish Fulfillment service needs to perform three actions, which can be modeled with three strongly-typed asynchronous functions.
Note: I use lowercase “function” for F# functions and capitalize “Function” for Azure Functions throughout the article to minimize confusion.
The first action finds matches for each wish:
The first line of all my function snippets shows the function type. In this case, it’s a mapping from the text of the child’s wish (string
) to a list of matches (Match list
).
The second action takes the combined list of all matches of all wishes and picks one. Its real implementation is Santa’s secret sauce, but my model just picks the one with the highest confidence level:
Given the picked gift
, the reservation is merely { Kid = wishlist.Kid; Product = gift }
, not worthy of a separate action.
The third action registers a reservation in the SAP system:
The fulfillment service combines the three actions into one workflow:
The workflow implementation is a nice and concise summary of the actual domain flow.
Note that the Matching service is called multiple times in parallel, and then the results are easily combined by virtue of the Async.Parallel
F# function.
So how do we translate the domain model to the actual implementation on top of serverless Durable Functions?
C# was the first target language for Durable Functions; Javascript is now fully supported too.
F# wasn’t initially declared as officially supported, but since F# runs on top of the same .NET runtime as C#, it has always worked. I have a blog post about Azure Durable Functions in F# and have added F# samples to the official repository.
Here are two examples from that old F# code of mine (they have nothing to do with our gift fulfillment domain):
This code works and does its job, but doesn’t look like idiomatic F# code:
context
object around for any Durable operationAlthough not shown here, the other samples read input parameters, handle errors, and enforce timeouts — all look too C#-y.
Instead of following the sub-optimal route, we implemented the service with a more F#-idiomatic API. I’ll show the code first, and then I’ll explain its foundation.
The implementation consists of three parts:
Each Activity Function defines one step of the workflow: Matching, Picking, and Reserving. We simply reference the F# functions of those actions in one-line definitions:
Each activity is defined by a name and a function.
The Orchestrator calls Activity Functions to produce the desired outcome of the service. The code uses a custom computation expression:
Notice how closely it matches the workflow definition from our domain model:
Async function vs. Durable Orchestrator
The only differences are:
orchestrator
computation expression is used instead of async
because multi-threading is not allowed in OrchestratorsActivity.call
replaces of direct invocations of functionsActivity.all
substitutes Async.Parallel
An Azure Function trigger needs to be defined to host any piece of code as a cloud Function. This can be done manually in function.json
, or via trigger generation from .NET attributes. In my case I added the following four definitions:
The definitions are very mechanical and, again, strongly typed (apart from Functions’ names).
These are all the bits required to get our Durable Wish Fulfillment service up and running. From this point, we can leverage all the existing tooling of Azure Functions:
There is a learning curve in the process of adopting the serverless architecture. However, a small project like ours is a great way to do the learning. It sets Santa’s IT department on the road to success, and children will get better gifts more reliably!
The above code was implemented with the library DurableFunctions.FSharp. I created this library as a thin F#-friendly wrapper around Durable Functions.
Frankly speaking, the whole purpose of this article is to introduce the library and make you curious enough to give it a try. DurableFunctions.FSharp has several pieces in the toolbox:
OrchestratorBuilder
and orchestrator
computation expression which encapsulates proper usage of Task
-based API of DurableOrchestrationContext
Activity
generic type to define activities as first-class valuesActivity
module with helper functions to call activitiesAsync
and Orchestrator
In my opinion, F# is a great language to develop serverless Functions. The simplicity of working with functions, immutability by default, strong type system, focus on data pipelines are all useful in the world of event-driven cloud applications.
Azure Durable Functions brings higher-level abstractions to compose workflows out of simple building blocks. The goal of DurableFunctions.FSharp is to make such composition natural and enjoyable for F# developers.
Getting Started is as easy as creating a new .NET Core project and referencing a NuGet package.
I’d love to get as much feedback as possible! Leave comments below, create issues on the GitHub repository, or open a PR. This would be super awesome!
Happy coding, and Merry Christmas!
Many thanks to Katy Shimizu, Devon Burriss, Dave Lowe, Chris Gillum for reviewing the draft of this article and their valuable contributions and suggestions.
Originally published at mikhail.io.