Go Project Setup Part 3 - Dependency Injection
Field Manual Book

We have done a lot on our project so far. In Part 1, we built a basic application, created a Makefile to build it, and created a Docker image for deploying it. In Part 2, we focused on configuration from environment variables and set up a reflection-based tool for filling our configuration structures with values.

This time, we will focus on how we use dependency injection to structure our application and build out a service provider for wiring up all the necessary parts.

Inversion of Control and Dependency Injection

Inversion of Control is a concept in software engineering where a component should not be responsible for creating its own dependencies. Instead, you should reverse the flow of control so that the dependencies come from outside. This principle will help you arrive at a better design since we can swap these dependencies as needed.

Dependency Injection is a way of achieving Inversion of Control by having the dependencies injected into the component from an external component or container.

Dependency Injection Containers

Let's get this out of the way: if you are coming from other programming languages such as Java or C#, you may be used to using a DI framework that handles dependencies automatically and takes over a lot of the work for you.

Go does have similar libraries, like Uber's Dig, Sarulabs DI, and Ozzo DI, which can serve this need; however, I would encourage you to at least try managing your own dependency injection instead for these reasons:

  1. Managing your own DI probably isn't as complicated as you may think - even if you are working on a complex project.
  2. A lot of DI frameworks contain a lot of magic, which makes troubleshooting errors difficult.
  3. In complicated scenarios, you end up doing much of the wiring yourself, and sometimes you end up fighting with the DI system to do so.
Github Repository

Part 3 source code can be found in a branch on this Github Repository

Refactoring

As we make progress in our development, it is essential to take time to refactor our code. We want to avoid our code growing stale with vestigial code that we no longer need.

First, we are going to open internal/config/readconfig.go and remove the three config structures we created last time - we are going to put these settings somewhere more appropriate:

// Delete this code
type AppConfig struct {
	Message        string `env:"APP_MESSAGE,default=Hello, World!"`
	ApiKey         string `env:"API_KEY,required"`
	TimeoutSeconds int    `env:"TIMEOUT_SECONDS,default=200"`
}

type DatabaseConfig struct {
	DataSourceName string `env:"DB_DSN,required"`
}

type WebServerConfig struct {
	Host string `env:"WEB_HOST,default=0.0.0.0"`
	Port int    `env:"WEB_PORT,default=8080"`

Now, let's create an internal/data directory and add database.go to that directory:

package data

type DatabaseConfig struct {
	DataSourceName string `env:"DB_DSN,required"`
}

type Database struct {
	config *DatabaseConfig
}

func NewDatabase(config *DatabaseConfig) *Database {
	return &Database{config: config}
}

After that, create a new directory internal/web and add a file called service.go in there:

package web

import (
	"github.com/ymiseddy/go-getting-started/internal/data"
)

type WebServerConfig struct {
	Host string `env:"WEB_HOST,default=0.0.0.0"`
	Port int    `env:"WEB_PORT,default=8080"`
}

type WebServer struct {
	config   WebServerConfig
	database *data.Database
}

func NewWebServer(config WebServerConfig, database *data.Database) *WebServer {
	return &WebServer{
		config:   config,
		database: database,
	}
}

Although it would be fun, we're not actually going to implement a web service this time; we are just creating placeholders to demonstrate dependency management.

Based on these two structs, we have the following dependency graph:

![[dependency_example_01.svg]]

Both of our structs depend on their respective configuration, and the WebServer depends on the Database. Shouldn't be too hard.

The Service Provider

Now we need a way to wire up our dependencies. We will create a directory internal/ioc with a file called provider.go.

First, let's create a struct to hold all our dependencies:

type ServiceProvider struct {
	databaseConfig  *data.DatabaseConfig
	database        *data.Database
	webServerConfig *web.WebServerConfig
	webServer       *web.WebServer
}
Pointers

Currently, we are using pointers to concrete types in our struct. Doing this is fine, but there may be cases where we use interface types also - don't be afraid to depend on interfaces. In many cases, this is better.

Eager loading

Now we want to wire up all these dependencies. A good first attempt might be to create a constructor that wires them all up at once:

func NewServiceProvider() *ServiceProvider {
	databaseConfig := data.DatabaseConfig{} err := config.ReadConfigInto(&databaseConfig)
	if err != nil {
		panic(err)
	}

	database := data.NewDatabase(databaseConfig)

	webServerConfig := web.WebServerConfig{}
	err = config.ReadConfigInto(&webServerConfig)
	if err != nil {
		panic(err)
	}

	webServer := web.NewWebServer(webServerConfig, database)

	return &ServiceProvider{
		databaseConfig:  databaseConfig,
		database:        database,
		webServerConfig: webServerConfig,
		webServer:       webServer,
	}
}

Yes, this works, but there are a few problems with this:

  1. Poor error handling - we don't want our constructor to panic when something goes wrong.
  2. Eager loading - what if we don't need the whole system? What if we have a sub-command that only requires the database? Using this constructor will wire up the web server even if we don't need it.

Lazy Loading

Instead of wiring them up ahead of time, let's leave them all null initially:

func NewServiceProvider() *ServiceProvider {
}

Now we can create a set of getter methods that return the types we need. These will have a similar format. First, they will check if the pointer in our ServiceProvider is nil; if so, we will do all the required setup and set the pointer inside ServiceProvider. If it already exists (is not nil). Here, we will build our DatabaseConfig - notice how we perform a call to config.ReadInto as part of the setup:

func (sp *ServiceProvider) GetDatabaseConfig() (*data.DatabaseConfig, error) {
	if sp.database == nil {
		databaseConfig := data.DatabaseConfig{}
		err := config.ReadConfigInto(&databaseConfig)
		if err != nil {
			return nil, err
		}
		sp.databaseConfig = &databaseConfig
	}
	return sp.databaseConfig, nil
}

Now let's wire up our database - see how we chain call our GetDatabaseConfig function to get the config?

func (sp *ServiceProvider) GetDatabase() (*data.Database, error) {
	if sp.database == nil {
		dbConfig, err := sp.GetDatabaseConfig()
		if err != nil {
			return nil, err
		}
		sp.database = data.NewDatabase(dbConfig)
	}
	return sp.database, nil
}
Design Principle: Dependency Inversion Principle

We can also use our ServiceProvider to return interface implementations rather than specific concrete examples. For instance, we can support multiple database implementations in the future, such as PostgreSQL and SQLite.

If this happens, we could create a database interface (repository) that has concrete implementations depending on which database system we are using behind the scenes.

In this case, our ServiceProvider could return a Repository interface and, based on the configuration, determine which concrete instance to create and return.

GetWebServerConfig works mostly the same as GetDatabaseConfig:

func (sp *ServiceProvider) GetWebServerConfig() (*web.WebServerConfig, error) {
	if sp.webServer == nil {
		webServerConfig := web.WebServerConfig{}
		err := config.ReadConfigInto(&webServerConfig)
		if err != nil {
			return nil, err
		}
		sp.webServerConfig = &webServerConfig
	}
	return sp.webServerConfig, nil
}

Finally, we can implement GetWebServer - note how we need to call GetWebServerConfig and GetDatabase to fulfill the dependencies for our WebServer:

func (sp *ServiceProvider) GetWebServer() (*web.WebServer, error) {
	if sp.webServer == nil {
		wsConfig, err := sp.GetWebServerConfig()
		if err != nil {
			return nil, err
		}
		database, err := sp.GetDatabase()
		if err != nil {
			return nil, err
		}
		sp.webServer = web.NewWebServer(*wsConfig, database)
	}
	return sp.webServer, nil
}

Now we only set up the dependencies we need when we need them. For instance, sometimes, I like to include sub-commands in my application to perform some of the maintenance tasks. When running in this mode, I can use the ServiceProvider to wire up just the components I need to complete the task.

Lifetimes

So far, the examples have been intended to be created once and live for the lifetime of the application. Most of the time, I find this is sufficient; however, there are other cases where we might run into, and we ought to have a way to handle them:

  1. Named instances that live the lifetime of the application
  2. Ephemeral instances - which we create every time we call the method and discard them as soon as the job is complete.
  3. Context lifetimes - sometimes we want to create components that live for the lifetime of a context. For instance, a component that lasts for the duration of a web request.

Named Instances

Let's say we have a tenant service that is different for each known tenant. We may have a limited number of tenants, so we can keep them around once we use them.

Here we have /internal/tenant/service.go:

package tenant

import "github.com/ymiseddy/go-getting-started/internal/data"

type TenantService struct {
	name     string
	database *data.Database
}

func NewTenantService(name string, database *data.Database) *TenantService {
	 return &TenantService{name: name, database: database}
}

We want to get a different tenant service by name. So, tracking each TenantService can be done using a map in our ServiceProvider:

type ServiceProvider struct {
	databaseConfig  *data.DatabaseConfig
	database        *data.Database
	webServerConfig *web.WebServerConfig
	webServer       *web.WebServer
	tenantServices  map[string]*tenant.TenantService
}

We also want to create our map inside the constructor:

func NewServiceProvider() *ServiceProvider {
	return &ServiceProvider{
		tenantServices: make(map[string]*tenant.TenantService),
	}
}

Then, when we need to retrieve a TenantService, we can use a similar check and create technique, which we do with our other components:

func (sp *ServiceProvider) GetTenantServiceFor(name string) (*tenant.TenantService, error) {
	service, exists := sp.tenantServices[name]
	if !exists {
		database, err := sp.GetDatabase()
		if err != nil {
			return nil, err
		}
		service = tenant.NewTenantService(name, database)
		sp.tenantServices[name] = service
	}
	return service, nil
}

Ephemeral Instances

These are probably the easiest to implement, since creating ephemeral instances does not require any checks ahead of time. Here we create internal/ephemeral/service.go:

package ephemeral

import "github.com/ymiseddy/go-getting-started/internal/data"

type EphemeralService struct {
	database *data.Database
}

func NewEphemeralService(database *data.Database) *EphemeralService {
	{
		return &EphemeralService{database: database}
	}
}

We do not need to modify the ServiceProvider struct for this one; we only need to add a function call:

func (sp *ServiceProvider) GetAnEphemeralService() (*ephemeral.EphemeralService, error) {
	database, err := sp.GetDatabase()
	if err != nil {
		return nil, err
	}
	service := ephemeral.NewEphemeralService(database)
	return service, nil
}
Naming Conventions

For ephemeral components, I like to name them GetA* or GetAn* to communicate that the lifetime of the component is limited to the scope of the requester.

Context Instances

Occasionally, we want a component to live for the lifetime of a context. Examples include HTTP middleware functions where we populate a value in the middleware function and the request function, or other middleware needs to retrieve it.

Let's say we have a RequestInfo struct in internal/request/info.go:

package request

import "math/rand"

type RequestInfo struct {
	ID       int64
	username string
}

func NewRequestInfo() *RequestInfo {
	id := generateUniqueID()
	return &RequestInfo{ID: id}
}

func generateUniqueID() int64 {
	// Generate random unique ID
	id := rand.Int63()
	return id
}

We want to store our values by key in the context, so we can check if it exists and set it if not. In Go, it is a good practice to use a specific type for variables within your context to prevent name conflicts. We create the type and value for our request info:

type requestInfoKeyType string

const requestInfoKey requestInfoKeyType = "requestInfo"

We can now use our key to check if the info exists and set it if not:

func (sp *ServiceProvider) GetRequestInfo(ctx context.Context) (*request.RequestInfo, context.Context, error) {
	reqInfo, ok := ctx.Value(requestInfoKey).(*request.RequestInfo)
	if !ok {
		reqInfo = &request.RequestInfo{}
		ctx = context.WithValue(ctx, requestInfoKey, reqInfo)
	}
	return reqInfo, ctx, nil
}

Notice that we need to pass the context into the function and return a context from it. The reason is that in Go, Contexts are immutable, so we need to add the value to the context and return it as well. If the RequestInfo already exists, we can return the existing context as well.

Using the ServiceProvider as a Dependency

A component should never rely on ServiceProvider as a dependency since this can easily result in circular references.

For example, let's say our WebServer needs to get context instances of RequestInfo, so we might be tempted to do something like this:

package web

import (
	"github.com/ymiseddy/go-getting-started/internal/data"
	"github.com/ymiseddy/go-getting-started/internal/ioc"
)

type WebServerConfig struct {
	Host string `env:"WEB_HOST,default=0.0.0.0"`
	Port int    `env:"WEB_PORT,default=8080"`
}

type WebServer struct {
	config   WebServerConfig
	database *data.Database
	serviceProvider ioc.ServiceProvider
}

func NewWebServer(config WebServerConfig, database *data.Database, serviceProvider ioc.ServiceProvider) *WebServer {
	return &WebServer{
		config:   config,
		database: database,
	}
}

If you try this, your IDE may be yelling at you. Or, if you try to build it, you will certainly run into an import cycle not allowed error:

❯ make
go build -o build/app cmd/app/*.go
package command-line-arguments
        imports github.com/ymiseddy/go-getting-started/internal/ioc from main.go
        imports github.com/ymiseddy/go-getting-started/internal/web from provider.go
        imports github.com/ymiseddy/go-getting-started/internal/ioc from service.go: import cycle not allowed
make: *** [Makefile:5: build] Error 1

This code fails because we added a dependency on the ioc package to our WebServer, but the ServiceProvider also needs access to the WebServer. So what are we to do? Create an interface to help us out.

In the internal/request/info.go file, we can create a RequestInfoProvider interface:

type RequestInfoProvider interface {
	GetRequestInfo(ctx context.Context) (*RequestInfo, context.Context, error)
}

Now we can depend on RequestInfoProvider instead of the ServiceProvider:

package web

import (
	"github.com/ymiseddy/go-getting-started/internal/data"
	"github.com/ymiseddy/go-getting-started/internal/request"
)

type WebServerConfig struct {
	Host string `env:"WEB_HOST,default=0.0.0.0"`
	Port int    `env:"WEB_PORT,default=8080"`
}

type WebServer struct {
	config              WebServerConfig
	database            *data.Database
	requestInfoProvider request.RequestInfoProvider
}

func NewWebServer(config WebServerConfig, database *data.Database, requestInfoProvider request.RequestInfoProvider) *WebServer {
	return &WebServer{
		config:              config,
		database:            database,
		requestInfoProvider: requestInfoProvider,
	}
}
Interface Segregation Principle

If you end up creating a lot of Provider type interfaces, it may be tempting to create a single large one that includes all the methods in the ServiceProvider. It is better to avoid this and instead have several individual interfaces tailored to clients. A client should not be dependent on methods it does not use.

We have solved our dependency cycle, and now we can update our GetWebServer method in the ServiceProvider; we need to pass our sp variable into the constructor:

func (sp *ServiceProvider) GetWebServer() (*web.WebServer, error) {
	if sp.webServer == nil {
		wsConfig, err := sp.GetWebServerConfig()
		if err != nil {
			return nil, err
		}
		database, err := sp.GetDatabase()
		if err != nil {
			return nil, err
		}
		sp.webServer = web.NewWebServer(*wsConfig, database, sp)
	}
	return sp.webServer, nil
}
Implicit Interface Satisfaction

If you are familiar with another programming language, such as C# or Java, you might be wondering where we implement the new RequestInfoProvider interface.

In Go, interfaces are implicitly satisfied. That means that any struct that has all the functions (with matching signatures) of the interface, implicitly implements that interface and can be substituted for that interface.

Conclusion

Dependency injection and Inversion of Control are powerful concepts that you should use to help orchestrate the components in your application. It leads to a stronger application design, which is easier to maintain in the long run.

Using a similar approach to our ServiceProvider, you can easily implement the components your application needs exactly when they are needed and no sooner. Additionally, you can manage the lifetimes of these components, whether they live for the lifetime of the application, are ephemeral, or per-context.