By Soham Kamani
One of the most challenging aspects of building applications in Go is managing the many dependencies an application uses.
“Dependencies” here refers to any external service or transport layer an application needs to communicate with.
In an ideal world, the application would be stateless — with only one input and one output — essentially acting like a pure function.
However, most medium to large scale applications will have at least some dependencies. An application at GO-JEK, commonly has more than one dependency:
- A database
- A cache
- One or more HTTP clients
- A message queue
For each of these dependencies, there are a bunch of things to consider:
- Initialization: How to set up the initial state of a dependency? This needs to happen only once in the applications life cycle.
- Testing: How to write independent test cases for modules using an external dependency? Keep in mind while writing test cases, one need to simulate the failure of dependencies as well.
- State: How to expose a reference to each dependency (which is supposed to be constant) without creating any sort of global state (which, as we all know, is the root of all evil)?
Dependency Injection to the rescue
TLDR; Dependency injection can make your application more modular and testable.
The dependency injection pattern helps us solve these problems. Treating the external dependencies of our applications as individual dependencies for each module in our codebase allows us to have a more modular and focused approach during development, and follows a few premises:
- Dependencies are stateful : The only reason to consider treating something as a dependency is if it had some sort of a state. For example, a database has its connection pool as its state. This also means there should be some kind of initialisation involved before you can use a dependency
- Dependencies are represented by interfaces : A dependency is characterised by its contract. The module using it should not know about its implementation, or internal state.
Using dependency injection in a Go application
We can illustrate the use of dependency injection by building an application that makes use of it. Let’s consider a module that is dependent on a database as its store. The module will fetch an entry from a database, and log the result after performing some validations.
We can define the module as a structure with the store as its dependency:
database.Store is the dependencies interface, that we can define in another package:
The module can then use the dependency through its methods:
Note: we have not defined the implementation of our dependency as yet (in fact, that’s one of the last things we will do)
Testing the module
One of the most powerful features of dependency injection, is you can test any dependent module without having any actual implementation of the dependency. In fact, we can mock our dependency to behave the way we want it to, so it can test our module to handle different failure scenarios.
First, we will have to mock the dependency:
The mock store can now simulate the dependency in our module:
Error scenarios can also be simulated with the mock store:
Implementing and initializing the real store
Now that we know our module works well with the mock store, we can implement the actual one:
We can now put together the “store” as a dependency to the module and construct a simple command line app:
With dependency injection, we’ve converted something that looks like a dependency graph, into something that looks like a pure function, with the dependencies now part of the module:
In our code, this is implemented by composing each dependency as an instance variable of a module.
Alternative implementations of dependency injection
Adding dependencies as object attributes isn’t the only way to inject them. Sometimes, when your interface has just one method, it’s more convenient to use a more functional form of dependency injection. If we were to assume that our module only had a
GetNumber method, we could use curried functions to add dependencies as variables inside the closure:
And you can generate the
GetNumber function by calling the function constructor with a store implementation:
GetNumber := NewGetNumber(store)
GetNumber now has the same functionality as the previous OOP based approach. This method of deriving dependant functions using currying is especially useful when you require single functions instead of a whole suite of methods (for example, in HTTP handler functions).
When to avoid dependency injection
As with all things, there is no silver bullet to solve all your problems, and this is true for the dependency injection pattern as well. Although it can make your code more modular, this pattern also comes with the cost of increased complexity during initialization. You cannot simply call a method of a dependency without explicitly passing it down during initialization. This also makes it harder to add new modules, since there is more boilerplate code to get it up and running the first time. Sometimes, when there are a lot of embedded dependencies (if your dependencies have their own dependencies), initialization can be a nightmare.
If the application you are building is simple, or if you have many embedded dependencies then you should probably assess if it is worth the trade-offs to implement this pattern.
You can find the working source code of this article here
And… We’re hiring. We have the equivalents of foodtech, fintech, ride-sharing, home services, logistics and more under one roof. Check out gojek.jobs for more.