I’ve been learning more about Azure Functions in the last weeks, and this article continues that exploration by looking at different ways to organize Functions and Function Apps. I’m still fairly new to Functions, so bare with me if there’s anything that I’ve gotten wrong!
For an explanation of the basic concepts at work in Azure Functions, have a look at my previous post on the topic. As a quick recap, here’s what Azure Functions look like conceptually:
Individual Functions are grouped into Function Apps, which share resources such as processor, memory, scaling, and cost. This made me start to wonder about the different ways that Functions could be organized within Function Apps.
I haven’t tried all of these grouping strategies yet, but they all conceptually make sense and I look forward to trying them out in the coming weeks.
#1 Group By Business Domain
This first strategy is likely to be a very common approach. It follows the principle of separation of concerns by grouping Functions together so that a Function App operates over a slice of the business domain. Then, each Function can have a single responsibility within that slice.
I’ll use a fictional example based on the aviation industry to elaborate the concept more clearly. Let’s say an airline is building a serverless-based architecture to handle its maintenance schedule and flight bookings. They create a Maintenance Function App to run the following Functions:
- An HTTP trigger Function that receives the telemetry for a plane’s latest flight segment.
- A queue trigger Function that calculates if a plane is due for maintenance based on the stored telemetry.
- A topic trigger Function that sends notifications to flight operations managers when maintenance is needed.
- Another topic trigger Function to schedule a flight to the maintenance hangar by calling some other API.
- An HTTP trigger Function that finds the best seats available on a given flight.
This is obviously a highly simplified version of what would happen in real life, but it gets the following point across: you wouldn’t put a Function that books seats for passengers within this Function App — that would be a completely different concern that deserves its own Function App.
Any change to the maintenance process is isolated to the Maintenance Function App, making it highly cohesive, and cohesiveness is always a good thing. From the point of view of the Booking Function App, any sudden change in traffic will scale the entire booking process, letting that entire slice of the application keep up with demand.
#2 Group By Event Trigger
This approach feels most appropriate for HTTP Functions, where multiple endpoints are needed. I’ll use the same fictional aviation example from above to demonstrate. The following HTTP endpoints would be needed to handle the maintenance process described above:
- An endpoint for updating a plane’s telemetry, invoked by a client application once a flight is completed.
- An endpoint to retrieve the telemetry for a flight, used to calculate the next maintenance for a given airplane.
- An endpoint for resetting the ‘maintenance counter’, invoked when maintenance has been completed for an airplane.
This could be implemented in a number of ways from a RESTful perspective. I’ll make abstraction of that since it’s not the point I’m trying to make. The key thing to note is that grouping these endpoints into a single Function App makes it easy to perform any work on the HTTP API as a whole, be it code changes, DNS updates, etc.
This approach could work for other types of triggers if they are all reacting to the same event. The example that comes to mind is multiple Functions that are all listening to the same Service Bus Topic to perform different actions. Having them grouped together would make it easier to see the impacts of a change to that process.
#3 Group by Visibility
I’m using the term visibility to describe the boundaries between what needs to be exposed publicly for client applications to consume and what is used internally only.
Here’s what grouping by visibility would look like for the aeronautical example I’ve been using:
- End-user exposed APIs grouped in a Public Endpoints Function App
- Internal APIs and background workers grouped in another Function App
The main advantage here is that a slice of the system architecture can be grouped into one Function App, making it easier to delineate between the different components of the system. A potential issue with this approach is that some Functions are likely to need more resources than others within a given slice. This, however, can be alleviated by the next strategy.
#4 Spin Off High-Use Functions to their own Apps
The unit of scale with Azure Functions is the Function App and not the individual Functions themselves. So if one Function is solicited more than the others within the same Function App, all the Functions will scale to the additional instances, consuming more CPU and memory than may be required.
For that reason, splitting off a Function that has higher demands into its own Function App could be a decent approach to only scale the components that need the additional resources, regardless of the overall organization strategy that has been put in place.
#5 Each Function To Its Own Function App
This is an edge case to be sure, but I mention it anyway since there’s nothing preventing you from doing it as far as I can tell. Taking this approach allows for the utmost isolation between Functions, and also happens to be the only way to deploy AWS Lambdas, which is where I got the idea in the first place.
Going back to the aircraft maintenance example one last time, each Function that we mentioned above would be hosted in its own Function App. This has a few consequences on the application as a whole:
- There will be much more operational overhead, with each Function needing its own deployment process and configuration.
- You’ll end up in a situation where there are so many Function Apps that it’s hard to make sense of which Functions are used where, and for what purpose.
- Laying out that many Function Apps in code will be messy at best, unmaintainable at worst.
That’s what I managed to come up with. Are there other, better approaches that I missed? If so, please point them out in the comments, as I’m still getting my feet wet with Azure Functions.