Many articles have been written about starting with FaaS and most of them create the impression that the functions must be simple and that they have to comply with a single responsibility principle.
However, the business is never that simple. There are many workflows that describe each business domain and most are complex and an event at the beginning can influence a process at the end of a workflow. Does it mean that serverless isn’t suitable for such a situation? Should we only use it with CRUD scenarios?
Let’s imagine this situation: ordering a product. After the request is saved, it’s required to prepare an invoice for a customer.
One function to rule them all
The simplest approach is to create one function that will do all the required stuff. So, the function is invoked by HttpRequest with an order. After validation, the order is saved to a database, e.g. Azure Table Storage. Then, the function generates an invoice and saves it to storage e.g. Azure Blob Storage. In the end, the function sends a status code to a caller (fig. 1).
A great advantage of the solution is simplicity. The entire logic is closed into one function and it handles exceptions easily. Moreover, we, as software developers, are accustomed to monoliths, so such an approach is more understandable.
On the other hand, the function breaks the single responsibility principle. The solution has many dependencies and after applying retry policies, the code can be complex and hard to understand. Furthermore, the caller has to wait for a response until the execution is finished. Preparing an invoice can be time-consuming, which means that a response will be provided after a few seconds. We have to remember that the function execution time is limited and packing a lot of dependencies can cause it to exceed its limits.
This function is to damn big
We decided that SRP is important and we want to apply it to the current solution. The function has two main dependencies: saving an order to a table and preparing an invoice for a customer. Let’s create two functions triggered by HTTP requests. The first function contains an order validation and saves it to a table. The second one creates an invoice and saves to blob storage (fig. 2).
Currently, the function is free of breaking SRP problems. Each function is responsible for one thing. The functions are simple, so the code would be easy to understand. Moreover, the approach complies with execution time limits.
Unfortunately, we still have a problem with a caller that waits a few seconds for a response. Even though it’s not such a big deal, it can be expensive. The first function is waiting for the second one’s outcome. If we used a standard approach with servers that are always on, it would be fine. However, it is an antipattern in the serverless world. The functions are currently highly coupled and we pay twice as much because the idle time of the first function is treated as execution time (fig. 3).
Why so synchronous?
As we can see, the current solution is too synchronous. We don’t want to make a customer wait for a response, because an invoice can be provided later. Let’s change a little bit of the previous solution: the second function is triggered by a message from a queue. It means that the first function adds a message to a queue, e.g. Azure Queue Storage, instead of making an HTTP request (fig. 4). Now, the function returns a response to a caller immediately. Furthermore, we don’t have to pay twice for execution, because the first function doesn’t wait for an outcome from the second one.
Nevertheless, we lost the possibility to return information to a caller about an invoice. How can we inform him that something went wrong with the second function? Secondly, we should implement a dead letter queue service. The solution is more complicated and hard to understand for newcomers. Moreover, can we implement retry policy and exceptions handling easily?
Well, now we know what not to do
In Azure, there is a framework called Durable Functions that allows us to create processing workflows (more in post First face-off: Azure Durable Functions). Let’s try to use it. We need four functions now. The first one triggered by an HTTP request starts an orchestration. The second one describes the orchestration. It invokes that an order should be saved to a table (using the third function). Secondly, it delegates that an invoice should be generated and saved on storage (using the fourth function). Two more functions are responsible for executing the mentioned work.
Currently, the solution is simpler. Because the entire workflow is described in code, it is understandable for devs. Moreover, it complies with SRP rule and we don’t have to pay for idleness. Beyond that, inside the orchestrator function, it is possible to add retry policies and exceptions handling easily. Moreover, the framework can create and return a link to a processing status. And other than that, it is possible to provide output values, e.g. a link to a generated invoice. It allows a caller to request status and an invoice file location.
There are still many gaps in the serverless world. However, it doesn’t mean that it is only for demos and basic solutions. You just have to know when and how to use appropriate tools and concepts, just as we used Durable Functions in our solution. If you want to start with serverless, move your solution to serverless, or you need a review for your current architecture, together with ServerlessGuru, we will be glad to help you.
Founder — Serverless Wroclaw
LinkedIn — @dariuszparzygnat
Twitter — @dariusparzygnat
Thanks for reading 😃