Dependency Injection in Go

When I mention dependency injection to folks who have Java or .NET backgrounds it usually invokes a few sets of questions — to which my usual response is:

It’s likely not as sophisticated as what you’re thinking — and it doesn’t have to be!

While there are methods to use more sophisticated approaches in Go, starting off projects the simplest way possible is still the preferred approach.

Definitions

In object-oriented computer programming, SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.

Why is dependency injection important in practice?

Let’s use a hypothetical application which we can sum up as:

allow customers to store their code and run them in some sort of containerized environment.

In this product, our ubiquitous language might have the following entities:

- Code — represents a customer’s arbitrary code.

In addition, we might require the following service abstractions:

- Repository — provides an abstraction of data.
- Runtime — allows you to execute arbitrary code.

And a classical router, controller, or command layer to receive requests and act on them:

- Server — can be considered as the Command layer as in CQRS.

Sketching a minimal example of this:

Playing out what tech we’ll use

- Repository — will likely just be backed by postgres.
- Runtime — we can explore using lambda (or openwhisk, cloudflare workers, the list goes on).

How will we test these?

# just works without any dependencies at all.
go test ./…
# does integration testing using env vars or flags.
env $(cat .env) go test /… -tags integration

So what does this mean if we want to be able to test without any postgres dependency? Likely it means we either don’t test the postgres implementation of the Repository at all. That’s probably fine, but then would we also skip testing the Server layer because it depends on *Repository?

Similarly, if our Runtime implementation relies on lambda, how will we test that reliably?

This is where interfaces, and good design helps!

It’s all about the interface

Then we can at least test the Server layer by providing a mock implementation for our dependency-less testing.

We can then do -tags integration style testing in CI to provide a real database, and a test AWS environment for us to interact with lambda so we’re guaranteed that any change we do hits the real thing all the time — but we still empower developers with a faster feedback loop on average.

So what about dependency injection?

And then in our test, we can imagine providing all the mock / stub
implementations as necessary.

Will we end up writing a lot of boilerplate mock code?

If your domain is really just interacting with a database — maybe you can argue that in order for your devs to work with the code, a local postgres must be available (or one in docker) — and that’s fine! Ultimately it’s all about trade-offs.

Once the set of dependencies becomes a bit too heavy though, we’d want to keep the developer feedback loop as fast as possible, while answering the following questions:

1. How much of the project business logic involves these heavy dependencies? Is it 10%? 50%?
2. How often do developers modify these external heavy dependencies? (In the example we have, how much of the work becomes modifying lambda code, vs modifying PostgreSQL code, vs modifying the server code?)

As is often the case with software engineering, it’s all about trade-offs — and when you need to reach for dependency injection it’s just right there in your tool belt.

Footnotes

software engineer at auth0, writing code for humans, with humans.