Whether you actively plan for it or not, dependency management is part of a developer’s everyday work. In this post we’ll present dynamic dependency injection, a technique that allows you to break dependencies and swap service implementations at runtime. We’ll start our journey by explaining how dependencies can become a problem, then we’ll see how to reduce them and how to break them altogether.
For the sake of simplicity, we’ll only focus on Apex dependencies in this post, but keep in mind that there are two types of dependencies that you have to manage: dependencies in code (Apex, Aura Components, Lightning Web Components, etc) and dependencies between metadata (processes, flows, actions, etc).
Dependency injection techniques can help for dependencies in code such as Apex, Aura Components, and with some kinds of metadata dependencies like processes or flows but it is not supported by all features. For example, dependency injection is not possible with Lightning web components where imports are static only.
When dependencies turn into liabilities
Theory
Let’s start with a simple and common scenario and pretend that you have a Client
class that needs to use a Service
class to perform some operations. The easiest way to implement this is to simply write this in Client
:
While this is perfectly valid code, it introduces a dependency between the Client
and Service
classes. In other words, Client
becomes tightly coupled with Service
:
You can easily witness this dependency by trying to delete Service
. You’ll see that you can’t because Client
is using it.
This tight coupling is generally not an issue for small projects but there are a number of factors that can turn it into a concern, such as:
- growing code base
- different development teams working in parallel
- code shared by multiple apps or orgs
- unforeseen implementation changes
- need for configuration flexibility at runtime
Because of these different factors, you’ll end up having a hard time to develop, test, package, deploy and maintain your code if you don’t adopt a dependency management strategy. Your Apex code will end up forming a huge monolith that you can’t tear apart.
The good news is that most dependency management issues can be addressed by adopting enterprise design patterns such as Service Layer and Domain & Selector Layers as start. However, if you are looking for some some extra runtime flexibility, you will have go further and dive into the world of dependency injection.
Practice
Let’s take a practical example and suppose that we have a shipping service based on FedEx that generates a shipment tracking number. We are using the shipping service to send parcels with the following code:
Now, let’s suppose that we want to reuse FedExService
in another app or an in another org but we don’t want to impact OrderingService
. And, let’s take it even a step further and suppose that a new business requirement dictates we need to support an alternate and optional shipping service such as DHL depending on the account’s country.
With that, we can only implement these business requirements if we break the dependency between the two services. We can’t do that with the current implementation so we’ll need to refactor our code to reduce dependencies.
Reducing dependencies with inversion of control
Theory
The problem with a direct dependency scenario like the one we just saw is that the client using the service has direct access to the service implementation. In other words, it has control over it. The solution to break the dependency is to invert this control so that the client does not know about the service implementation details.
We can achieve that with a strategy design pattern. The strategy pattern introduces an interface that defines the service and a strategy class that controls which service implementation is returned to the client. This removes all implementation class references from the client class.
Practice
Let’s apply inversion of control to our shipping service by implementing a strategy design pattern. We start by creating an ShippingService
interface.
Then we add the two service implementations (mocked here for the sake of simplicity):
Finally, we implement a simple strategy: we only use FedEx to for orders shipped in the United States and we use DHL for the other countries.
With that, we have implemented inversion of control. We can use the shipping service without having a dependency between OrderingService
and the shipping implementations:
This approach reduces dependencies but we still have dependencies between ShippingStrategy
and our two shipping implementation classes. Let’s take it a step further and explore dependency injection to get rid of those dependencies altogether.
Breaking runtime dependencies with dependency injection
Theory
Dependencies exist because the compiler needs to know which service implementation classes our client code uses. It needs that because it’s looking for those classes and checking whether they compile. So the question is: how do we get rid of these compile-time checks?
The answer to that question is to use dynamic class instantiation to bypass these checks. In other words, if we only load the service implementation classes at runtime, the compiler cannot establish the dependencies earlier.
Just like we used a strategy pattern, we can rely on an injector class to dynamically load and return our implementation classes. The injector does not have a compile-time reference to a particular implementation class. In the end, the client only has a dependency to the Service interface and the the Injector class. As a consequence, the implementations can be shipped into one or more separate and optional packages.
Practice
Let’s apply dependency injection to our shipping example. We start by writing a generic Injector
class that uses System.Type to instantiate any Apex class from its class name:
Notice that we chose to keep Injector
generic so we aren’t directly returning a ShippingService
instance but an Object
. We’ll cast the returned object as needed when we call our injector.
Let’s now rewrite our account service class to use the injector and instantiate our shipping service implementations without introducing dependencies:
This is a basic example of dependency injection. Notice that we got rid of the hard-coded conditions by introducing a custom metadata type. This allows admins to hot-swap service implementations at runtime with just clicks. This code can run with only one of the two the shipping services deployed (provided that you don’t try to instantiate the missing one of course).
Check out this complete sample project for an in-depth example of how to achieve that.
Summary
This concludes our dynamic dependency injection journey. We started by exploring what dependencies are and how they can be problematic. We’ve demonstrated how inversion of control helps to reduce those dependencies. We then used dependency injection to suppress those runtime dependencies. With that knowledge, you can now build flexible and modular apps that can be easily configured by admins.
Here is a recap of the benefits and limitations of Apex dependency injection:
Benefits
- Allows to break dependencies and split code into on or more packages.
- Adds support for multiple optional implementations that can be hot-swapped at runtime without code modifications.
- Eases testing by facilitating the use of stubbing either with the Stub API or dependency injection.
Limitations
- Brings an overhead in terms of architecture complexity. You can’t easily locate implementations and map them with code that calls them.
- Increases execution time error risks. Because we use dynamic class instantiation to “cheat” the compiler by removing compile-time links, this implies that you can easily break your code and only notice failures at execution time.
With that in mind, you’ll want to apply dependency injection on strategic dependencies that you need to break but don’t overdo it.
You can either implement dependency injection yourself starting from this sample code based on the example discussed in this post or you can use a community-contributed library like Force DI. Finally, do remember that dependency injection applies to more than just Apex code.
About the author
Philippe Ozil is Lead Developer Evangelist at Salesforce where he focuses on the Salesforce Platform. He writes technical content and speaks frequently at conferences. He is a full stack developer and a VR expert. Follow him on Twitter @PhilippeOzil or check his GitHub projects @pozil.