What even is “Dependency Injection”? (a practical example using Go)

LittleLeverages
3 min readFeb 26, 2024

--

I’m not satisfied with most explanations of dependency injection. I honestly don’t even use the term anymore but it’s so common online that I have to. I promise the concept is much simpler than you think it is. If you’ve struggled to understand it don’t worry, I had the same problem. This post will clear things up :)

Dependency injection is about being able to substitute one implementation of a thing for another implementation of a thing. To do this, you have to pass in the thing to something, instead of that something making the thing itself. Let’s work through an example

package main

type Entitlement struct {
AccountId string
CanTransfer bool
}

type EntitlementsClient interface {
GetEntitlement(accountId string) Entitlement
}

type Account struct {
UserId string
AccountId string
//...
}

type AccountsRepository interface {
GetByUserId(userId string) ([]Account, error)
}

Okay, so we have two interfaces here that both deal with accounts. One gets us accounts, given a userId, and the other gets an entitlement ( which is just a thing that tells us what an account can do ). We’re not going to spend time looking at the implementations because they’re unimportant — I didn’t even implement them! But let’s see how dependency injection works..

type Service interface {
GetTransferAccounts(userId string) ([]Account, error)
}

type service struct {
accountsRepository AccountsRepository
aentitlementClient EntitlementsClient
}

// DEPENDENCY INJECTION
func NewService(acctsRepo AccountsRepository, entitlementsClient EntitlementsClient) service {
return service{
acctsRepo,
entitlementsClient,
}
}

func (s *service) GetTransferAccounts(userId string) ([]Account, error) {
accounts, err := s.accountRepository.GetByUserId(userId)
if err != nil {
return nil, err
}

output := []Account{}

for _, a := range accounts {
entitlement, err := s.accountEntitlementClient.GetEntitlement(a.AccountId)
if err != nil {
return nil, err
}

if entitlement.CanTransfer {
output = append(output, a)
}
}

return output, nil
}

And there it is!! That is literally dependency injection right there. The passing in of an interface for our NewService, the AccountsRepository and EntitlementsClient, is dependency injection — those are the dependencies we are injecting into the Service.

Service does not care how AccountsRepository is implemented. It only cares that whatever is injected adheres to the AccountsRepository interface. The Service is not creating dependencies itself, but is instead asking for them. And the benefit of this is we can provide any implementation of those interfaces to the Service. We can provide a MockAccountsRepository for testing — and this is the most likely thing you will do. You can provide a Postgres version, a Mysql version, a Redis version, a whatever version. No matter what you provide, the code for the Service does not need to change, because it doesn’t know what implementation you’re providing anyway.

Here’s what the use of our “dependency injection” allows us to do in a test

type mockAccountRepository struct {}
func (r *mockAccountRepository) GetByUserId(userId string) ([]Account, error) {
return []Account{{"1", "2"}}, nil
}

type mockEntitlementsClient struct {}
func (r *mockAccountRepository) GetEntitlement(accountId string) (Entitlement, error) {
return Entitlement{"2", true}, nil
}

//Okay we have a mocked version of each 'dependency'
accountRepository := mockAccountRepository{}
entitlementsClient := mockEntitlementsClient{}

service := NewService(&accountRepository, &entitlementsClient)
x, _:= service.GetTransferAccounts("1234")

We create two versions of the dependencies and we can pass them to our Service. Again, it does not matter how the dependency is implemented. The Service is decoupled (only used through an interface instead of directly) from the dependencies, because it’s only using the dependencies through an interface. The use of an interface is the decoupling — you’re decoupled from a specific implementation. The dependencies can be tested independently of the Service and the Service can be tested independently of the dependencies. And realize that this test code is basically the code you’d use for different implementations. You’d have a Postgres one or a Redis one. It’s all the same thing.

The hardest thing about “dependency injection” is the words.

--

--