Dependency Injection In Go

Dependency injection fits naturally in go

Use dependency injection like an addict

Why Dependency Injection

As a software developer split our code in different layers is a requirement if we desire to make it clean and maintainable.

Usually the boundaries are placed at least between infrastructure and business logic. When we are dealing specially with complex business logic, it is desirable that infrastructure depends on our business logic, so that we don’t break our software when changing the infrastructure.

The first decision when developing a new software project, is to materialize this layer split choosing an architecture. Most of the times I choose Clean Architecture, but you have another good options like Domain Driven Design.

Independently from the architecture you choose, we have to glue the pieces from the different layers to come up with a new feature and this is where Dependency Injection shines.

How to Implement Dependency Injection

To understand dependency injection, nothing better than an example. Imagine that we have to implement the user registration for our imaginary project and we are going to implement this with clean architecture.

clean architecture

We can come up with an entity like this:

package entity
//User for our system
type User struct {
email string
password string
name string
dateOfBirth string
}
//validates at a business level
func Validate() error {
...
}

Now let’s make an use case where we perform the user registration:

package usecase
import "entity"
func RegisterUser(user entity.User) { 
//we have to validate and store the user
}

Hum looks like we need some infrastructure module to store our user. We didn’t implement any UserRepository yet at the infrastructure level and we have another problem, by the clean architecture principles a higher level layer cannot know about the existence of a lower level layer.

How do we solve this?

Go implicit interfaces to the rescue. Fortunately we don’t have to touch any piece in the infrastructure, we are going to say to our use case that there is some kind of user saver that will save our user on registration. Want to see?

package usecase
import "entity"
//Hey use case there is out there some user saver that will save the //user
type UserSaver interface {
Save(user entity.User) error
}
//UserRegisterUseCase will store his dependencies
type UserRegisterUseCase struct {
userSaver UserSaver
}
func newUserRegisterUseCase(userSaver UserSaver) UserRegisterUseCase { 
return &{userSaver}
}
func (u UserRegisterUseCase) RegisterUser(user entity.User) error { 
user.Validate()
err := u.userSaver.Save(user)
if (err != nil) {
return err
}
}

How cool is that? We don’t even touched in the infrastructure layer and we programmed the whole use case from the input of the entity to the save action. This is dependency inversion at a high level and for me this is one of the strengths of go.

Implementing our UserRepository…

package infrastructure
import "entity"
//UserRepository to manage users persistence
type UserRepository struct {
psqlClient psql.PsqlClient
}
func NewUserRepository(psqlClient psql.PsqlClient) UserRepository 
{
return &UserRepository{psqlClient}}
//Save user
func Save(user entity.User) error {
...
}
//GetByEmail gets the user by email
func GetByEmail(email string) error {
...
}

Let’s see the dependency injection in action…

package main
import (
"usecase"
"infrastructure"
"psqlClient"
)
func main() { 
psqlClient := psqlClient.NewPsqlClient()
userRepository := infrastructure.NewUserRepository(psqlClient)
useCase := usecase.newUserRegisterUseCase(userRepository)
//Now we can inject usecase into a delivery layer like web or
//cli
}

We instantiate and we inject every dependencies in the main module. By this we can see and control the dependency tree in a single point of our program.

Advantages in Dependency Injection

dependency injection in our toy project

As you can see we ensured that the highest 2 layers, entities and use cases, do not know about the existence of the lowest layers. This is crucial because the infrastructure can change without affecting the business logic.

The second advantage is that we isolate psql third party package in a single module. This module will sustain every change made in newer versions of this third party package.

Last but not least, using dependency injection enable you to test your modules without testing their dependencies.

Let’s take a look for example to our User Repository, why the heck would you want to test also the Psql Client when testing the User Repository?

With dependency injection you can mock the Psql Client and ensure that the User Repository calls the right Psql Client methods.

Keeping it simple in Go

In my opinion instantiating manually is just enough. If the project was big enough you could create different files in the main package to do the dependency injection and then call them from the main.go.

Kind Regards,

Mick Bolt