Avatar

Dependency management for the dependency injection pattern: a Golang kata

← Back to list
Posted on 01.09.2023
Last updated on 12.11.2024
Image by AI on Midjourney
Refill!

Table of contents

Remember I made a kata on the dependency injection pattern? Well, the way I managed the dependencies was fine at that time. However, in a real life project, when the amount of dependencies grows, this may, and will turn into a hassle. Especially prominently it manifests during unit test writing, or adding a new dependency: every time when a service is initialized, it is required to import and initialize its dependencies. The amount of imports and the boilerplate size becomes crazy high, and it basically gets repeated in every file!

This is where a dependency manager comes into play.

# The idea

The idea is simple: for every dependency there is a factory function, that returns an instance of that dependency. Should that instance also have some dependencies, the corresponding factory functions are called recursively. This kind of dependency nesting can be as deep as I want, the only condition is: the dependency graph must be acyclic. However, this is true by the definition of the pattern.

The calling code just needs to invoke one function, and the manager then unwraps the dependency tree.

One more cool thing is the fact, that Go actually allows a function to return an interface. That means I am, potentially, able to mock parts of the dependency tree. This is useful in unit testing for services, when repositories are actually mocked. But this is another story.

Another important thing is: from the perspective of the dependencies nothing actually changes: it is still old good dependency injection pattern in place. It's just the amount of work to be done externally reduces drastically.

# The code

So I took the repos and services from the kata mentioned above.

For the repository I've defined an interface:

👉 📃  internal/interfaces/repositories.go
package interfaces
import (
"dependencymanager/internal/domain/database/book"
)
type BookRepository interface {
GetBooks(filter string, page int32) (books []*book.Book, err error)
GetBookCount(filter string) (count int64, err error)
}
The code is licensed under the MIT license

Same for the service:

👉 📃  internal/interfaces/services.go
package interfaces
import "dependencymanager/internal/domain/business/book"
type BookService interface {
GetBooks(filter string, page int32) (result *book.GetBooksResult, err error)
}
The code is licensed under the MIT license

Splitting the manager into two parts (one for repos and the other for services) makes mocking the repositories possible.

👉 📃  internal/manager/repository.go
package repository
import (
"dependencymanager/internal/interfaces"
"dependencymanager/internal/repository/book"
"gorm.io/gorm"
)
type Manager struct {
session *gorm.DB
bookRepository *book.Repository
}
func New(session *gorm.DB) *Manager {
return &Manager{
session: session,
}
}
func (m *Manager) GetBookRepository() interfaces.BookRepository {
if m.bookRepository == nil {
m.bookRepository = book.New(m.session)
}
return m.bookRepository
}
The code is licensed under the MIT license

And then for the service:

👉 📃  internal/manager/service.go
package service
import (
"dependencymanager/internal/interfaces"
"dependencymanager/internal/manager/repository"
bookService "dependencymanager/internal/service/book"
"gorm.io/gorm"
)
type Manager struct {
session *gorm.DB
repositoryManager *repository.Manager
bookService *bookService.Service
}
func New(session *gorm.DB, repositoryManager *repository.Manager) *Manager {
return &Manager{
session: session,
repositoryManager: repositoryManager,
}
}
func (m *Manager) GetBookService() interfaces.BookService {
if m.bookService == nil {
m.bookService = bookService.New(m.session, m.repositoryManager.GetBookRepository())
}
return m.bookService
}
The code is licensed under the MIT license

Now, putting all that together saves some space in the main.go file, as I only need to create two managers and one service.

👉 📃  cmd/main.go
package main
import (
"context"
"fmt"
"net"
"net/http"
"os"
"sync"
"dependencymanager/internal/controller/book"
"dependencymanager/internal/manager/repository"
"dependencymanager/internal/manager/service"
"dependencymanager/internal/util/db"
)
const (
keyServerAddress = "serverAddress"
)
func main() {
session, err := db.Connect()
if err != nil {
panic(err)
}
svcManager := service.New(session, repository.New(session))
// no need to create the books repository manualy now
bookController := book.Controller{
BookService: svcManager.GetBookService(),
}
// ... the rest of the code is the same as before
}
The code is licensed under the MIT license

# Waaait, you think you're smart, don't you?

The manual solution explained in the article can be easily integrated into already existing codebase. However, for a newly-started project I encourage you (and myself) to try some libraries that already can do that kind of stuff. Take a look:

That's it for now folks. The benefits are obvious: should you decide to add or remove a dependency, there is no need to f**k around the code anymore, changing the New() function in 100 places, adding more and more imports along the way. Just go to the manager, add the dependency there, and it will be available everywhere.

The code is, as usual, here. Enjoy.


Avatar

Sergei Gannochenko

Business-oriented fullstack engineer, in ❤️ with Tech.
Golang, React, TypeScript, Docker, AWS, Jamstack.
19+ years in dev.