DI in the serverless world
Reason for the article
So many books teach you about how dependency injection (DI) works, how you should use it, its benefits and so on, but I haven’t read one that talks about how it actually works in the context of functions and durable functions. Let’s get a step back, ignore the NuGets, helpers and other containers, and think about how it actually works in our context, in our application, in our use-case, and stop trying to copy-paste from another project that already works, or from a book, without actually understanding what’s going on.
Key concepts
Azure functions
Definition
The “thing” (service) which makes you step into the serverless world and helps you reduce costs (since it is a pay-per-usage model), improve scalability, performance and so on without you caring about the infrastructure.
But, but why?!
Functions allow you to (write and) execute code in response to different triggers or events, such as HTTP requests, messages from Azure Event Grid or Azure Service Bus, changes in Azure Cosmos DB, timer-based triggers, and much more.
Equivalent to other vendors
Its equivalent from Amazon would be, AWS Lambda, but that’s the only thing you can “migrate” to. As far as I know, all the other vendors aren’t supporting .NET . (When you read this article you can also take a look over Google Cloud Functions, IBM Cloud Functions, and Alibaba Cloud Functions, maybe this changed in the meantime)
Keywords
Event-driven (since it responds to an outside event or trigger — pretty straightforward, right?), scalable, serverless, multi-language support and if we talk specifically about the Microsoft “product”, it has a simple integration with everything else from the Azure ecosystem — from triggers like Azure SQL Database, Event Hubs, Logic Apps, to dev tools such as visual studio (and code), multiple extensions on the market for those already, Azure CLI, Azure Portal, Azure DevOps, and those are only the first that cross my mind, but for sure there are others also.
Azure durable functions
Definition
This is an “extension” of the Azure functions, which give you the ability to get functions on steroids. You’ll get a stateful app (you’ll have a state my friend), so you’ll be good to go, to create long-running workflows in a serverless environment (and without the function close itself, how it was back in the old days).
But, but why?!
People wanted functions. They considered this the new thing that would save their life, the new silver bullet, but had long-running processes, wanted to add in their processes choreography and/or orchestrations, and guess what: after a limited timespan (I think 5 minutes in consumption plan which can be increased to 10 in premium plan), the function closed and respond with timeout. This is why the Microsoft guys created the durable part and this is how they kept their users happy.
Equivalent to other vendors
AWS Step Functions should be more or less the same thing from Amazon. For the other vendors, I didn’t try them out, but if you are curious you can search the compatibility and “capabilities” of Google Cloud Workflows, IBM Cloud Composer and Alibaba Cloud Function Workflow.
Keywords
Stateful service and the orchestration is done behind the scenes by the durable framework, so you don’t have any burden with this. Automatic “checkpointing” and state management — the orchestration will resume from it stopped, and the activities are “cached”. Parallel executions easy to be done with patterns like fan-out fan-in; plus all the others from the functions, because in the end, this is only an extension of those.
DI
Dependency Lifetimes
Transient: These dependencies are created each time they are requested. This means a new instance of the dependency is created for each function invocation.
Scoped: These dependencies are created once per function invocation scope. In Azure Functions, this typically means a new instance is created for each function execution.
Singleton: These dependencies are created once and shared across all function invocations within the same function app instance.
When to use
Transient dependencies are suitable for lightweight, stateless services or components where a new instance is needed for each function execution.
Scoped dependencies are useful when you want to share the same instance of a service within the same function execution context but don’t need to share it across multiple function invocations.
Singleton dependencies are suitable for stateless services or components that can be shared across multiple function invocations without risk of state corruption or concurrency issues.
Things to keep in mind
Concurrency — The idea of no infrastructure and auto-scaling without a guy clicking when something happens comes with a “cost”: the function will scale out to handle multiple concurrent invocations, even if you aren’t looking over your dashboards. When you use singleton or scoped you need to think about how you can avoid concurrency issues.
The same problem might appear when talking about durable functions (in an orchestrator or activity). You need to think about thread-safe scenarios.
Performance — Singleton improves performance because it reuses the same instance across multiple invocations of the function, but if you add singleton all over the place, this might have a negative impact. If your dependency tree is pretty big, and you have a lot of dependencies (and they are expensive to create), your memory allocation will go wild, and your scalability requirements will be affected.
Scalability — While we said that if singleton is all over the place it might affect this, the same goes for transient. You might get into a place in which you have unnecessary resource usage.
Resource management — This is Sparta! Sorry…this is serverless! Avoid holding on to long-live resources like database connections in a singleton. You’ll easily get to no resources at all in a heavy load time (think about Black Friday).
State management — In the case of durable functions, the rule of thumb is that scoped dependencies are appropriately scoped to the lifetime of the orchestrator or activity instance to prevent state corruption or unintended sharing of state across different function instances.
This state gives you an execution history, saved usually in storage or cosmos. When using singleton dependencies, be careful of how much data is stored in these storage services, since it can impact performance and add additional costs.
Personal Opinion
From my point of view, the most important thing when thinking about a function is that on the consumption plan (free price tier) at least, you will never have any instance available all the time. You’ll always get a cold start because you call it, it starts, it processes your request and it goes back to sleep. Starting from this you should think about how you want to handle the lifetime of your services.
As a general rule, I start with adding singleton for logging services, configuration providers, and caching services, and scoped for the others. After I see my function is working (build and simple test), I start to investigate if the scoped is really ok, or if I should change it to something else (for each of my injections).
In the end, I usually get to have transient dependencies for short-living tasks within an orchestration, singleton dependencies for what I already told you and scoped for long-living services or resources shared across multiple function invocations
Disclaimer: When I “migrated” from Azure to AWS (just for fun in a pet project) I ended up having some problems with scoped, and felt it didn’t work the same (in practice, even if on the paper it was the same). It might have been just my feeling because they were in their beginnings with step functions, but keep in mind, that there might be some differences on this level when you do a cloud migration.