Practical SOLID in Golang: Dependency Inversion Principle
- 11 minutes read - 2301 wordsWe continue our journey through the SOLID principles by presenting the one that has the most significant impact on unit testing in Go: The Dependency Inversion Principle.

Learning a new programming language is often a straightforward process. I often hear: “The first programming language you learn in a year. The second one in a month. The third one in a week, and then each next one in a day.” Saying that is an exaggeration, but it is not too distant from the truth in some cases. For example, jumping to a language relatively similar to the previous one, like Java and C#, can be a straightforward process. But sometimes, switching is tricky, even when we switch from one Object-Oriented language to another. Many features influence such transitions, like strong or weak types, if a language has interfaces, abstract classes, or classes at all. Some of those difficulties we experience immediately after switching, and we adopt a new approach. But some issues we experience later, during unit testing, for example. And then, we learn why The Dependency Inversion Principle is essential, especially in Go.
When we do not respect The Dependency Inversion
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Above is the definition of DIP as presented by Uncle Bob in his paper. There are also more details inside his blog. So, how can we understand this, especially in the context of Go? First, we should accept Abstraction as an object-oriented programming concept. We use this concept to expose essential behaviors and hide the details of their implementation.
Second, what are high and low-level modules? In the context of Go, high-level modules are software components used at the top of the application, such as code used for presentation. It can also be code close to the top level, like code for business logic or some use-case components. It is essential to understand it as a layer that provides real business value to our application. On the other hand, low-level software components are mostly small code pieces that support the higher level. They hide technical details about different infrastructural integrations. For example, this could be a struct that contains the logic for retrieving data from the database, sending an SQS message, fetching a value from Redis, or sending an HTTP request to an external API. So, what does it look like when we break The Dependency Inversion Principle, and our high-level component depends on one low-level component?
Let’s examine the following example:
The Infrastructure Layer
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{
db: db,
}
}
func (r *UserRepository) GetByID(id uint) (*domain.User, error) {
user := domain.User{}
err := r.db.Where("id = ?", id).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
The Domain Layer
type User struct {
ID uint `gorm:"primaryKey;column:id"`
// some fields
}
The Application Layer
type EmailService struct {
repository *infrastructure.UserRepository
// some email sender
}
func NewEmailService(repository *infrastructure.UserRepository) *EmailService {
return &EmailService{
repository: repository,
}
}
func (s *EmailService) SendRegistrationEmail(userID uint) error {
user, err := s.repository.GetByID(userID)
if err != nil {
return err
}
// send email
return nil
}
In the code snippet above, we defined a high-level component, EmailService
. This struct belongs to the application
layer and is responsible for sending an email to newly registered customers. The idea is to have a method,
SendRegistrationEmail
, which expects the ID
of a User
. In the background, it retrieves a User from UserRepository
,
and later (probably) it delivers it to some EmailSender
service to execute email delivery. The part with EmailSender
is currently out of our focus. Let’s concentrate on UserRepository
instead. This struct represents a repository
that communicates with a database, so it belongs to the infrastructure layer. It appears that our high-level component,
EmailService
, depends on the low-level component, UserRepository
. In practice, without defining a connection to the
database, we cannot initialize our use-case struct. Such an anti-pattern immediately impacts our unit testing in Go.
Let’s assume we want to test EmailService
, as shown in the code snippet below:
Unit Tests for EmailService
import (
"testing"
// some dependencies
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func TestEmailService_SendRegistrationEmail(t *testing.T) {
db, mock, err := sqlmock.New()
assert.NoError(t, err)
dialector := mysql.New(mysql.Config{
DSN: "dummy",
DriverName: "mysql",
Conn: db,
})
finalDB, err := gorm.Open(dialector, &gorm.Config{})
repository := infrastructure.NewUserRepository(finalDB)
service := NewEmailService(repository)
//
// a lot of code to define mocked SQL queries
//
// and then actual test
}
In contrast to some languages, like PHP, we cannot
simply mock whatever we would like in Go. Mocking in Go relies on the usage of interfaces, for which we can define a
mocked implementation, but we cannot do the same for structs. Therefore, we cannot mock UserRepository
, as it is a
struct. In such a case, we need to create a mock on the lower level, in this case, on the Gorm
connection object, which we can achieve using the SQLMock package.
However, even with this approach, it is neither reliable nor efficient for testing. We need to mock too many SQL queries and have extensive knowledge about the database schema. Any change inside the database requires us to adapt unit tests. Apart from unit testing issues, we face an even bigger problem. What will happen if we decide to switch the storage to something else, like Cassandra, especially if we plan to have a distributed storage system for customers in the future? In such a scenario, if we continue using this implementation of UserRepository, it will lead to numerous refactorings. Now, we can see the implications of a high-level component depending on a low-level one. But what about abstractions that rely on details?
Let’s check the code below:
UserRepository interface
type User struct {
ID uint `gorm:"primaryKey;column:id"`
// some fields
}
type UserRepository interface {
GetByID(id uint) (*User, error)
}
To address the first issue with high and low-level components, we should start by defining some interfaces. In this
case, we can define UserRepository
as an interface on the domain layer. This step allows us to decouple EmailService
from the database to some extent, but not entirely. Take a look at the User
struct; it still contains a definition
for mapping to the database. Even though such a struct resides in the domain layer, it retains infrastructural details.
Our new interface UserRepository
(abstraction) still depends on the User
struct with the database schema (details),
which means we are still breaking the Dependency Inversion Principle (DIP). Changing the database schema will inevitably
lead to changes in our interface. This interface may still use the same User
struct, but it will carry changes from
a low-level layer.
In the end, with this refactoring, we haven’t achieved much. We are still in the wrong position, and this has several consequences:
- We cannot effectively test our business or application logic.
- Any change to the database engine or table structure affects our highest levels.
- We cannot easily switch to a different type of storage.
- Our model is strongly coupled to the storage layer.
So, once again, let’s refactor this piece of code.
How we do respect The Dependency Inversion
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Let’s revisit the original directive for The Dependency Inversion Principle and focus on the bold sentences. They
provide us with some guidance for the refactoring process. We need to define an abstraction (an interface) that both
of our components, EmailService
and UserRepository
, will depend on. This abstraction should not be tied to any
technical details, such as the Gorm object.
Let’s take a look at the following code:
The Infrastructure Layer
type UserGorm struct {
// some fields
}
func (g UserGorm) ToUser() *domain.User {
return &domain.User{
// some fields
}
}
type UserDatabaseRepository struct {
db *gorm.DB
}
var _ domain.UserRepository = &UserDatabaseRepository{}
func NewUserDatabaseRepository(db *gorm.DB) UserRepository {
return &UserDatabaseRepository{
db: db,
}
}
func (r *UserDatabaseRepository) GetByID(id uint) (*domain.User, error) {
user := UserGorm{}
err := r.db.Where("id = ?", id).First(&user).Error
if err != nil {
return nil, err
}
return user.ToUser(), nil
}
In the new code structure, we observe the UserRepository
interface as a component that relies on the User
struct,
both of which reside within the domain layer. The User
struct no longer directly reflects the database schema;
instead, we use the UserGorm
struct for this purpose, which belongs to the infrastructure layer. The UserGorm
struct provides a method called ToUser
, which facilitates the mapping to the actual User
struct.
The Domain Layer
type User struct {
// some fields
}
type UserRepository interface {
GetByID(id uint) (*User, error)
}
In this setup, UserGorm
serves as part of the implementation details within UserDatabaseRepository
, which acts as
the concrete implementation for UserRepository
. Within the domain and application layers, our dependencies are
exclusively on the UserRepository
interface and the User
Entities,
both originating from the domain layer. Within the infrastructure layer, we can define as many implementations for
UserRepository
as needed, such as UserFileRepository
or UserCassandraRepository
.
The Application Layer
type EmailService struct {
repository domain.UserRepository
// some email sender
}
func NewEmailService(repository domain.UserRepository) *EmailService {
return &EmailService{
repository: repository,
}
}
func (s *EmailService) SendRegistrationEmail(userID uint) error {
user, err := s.repository.GetByID(userID)
if err != nil {
return err
}
// send email
return nil
}
The high-level component (EmailService
) depends on an abstraction, as it contains a field with the type
UserRepository
. Now, let’s explore how the low-level component depends on this abstraction.
In Go, structs implicitly implement interfaces, so there’s no need
to explicitly add code indicating that UserDatabaseRepository
implements UserRepository
. However, we can include a
check with a blank identifier to ensure this relationship.
This approach allows us to have better control over our dependencies. Our structs depend on interfaces, and if we ever
need to change our dependencies, we can define different implementations and inject them. This technique aligns with the
Dependency Injection pattern,
a common practice in various frameworks.
In Go, several DI libraries are available, such as the one from Facebook, Wire, or Dingo.
Now, let’s examine how this refactoring affects our unit testing.
Unit Tests for EmailService
import (
"errors"
"testing"
)
type GetByIDFunc func(id uint) (*User, error)
func (f GetByIDFunc) GetByID(id uint) (*User, error) {
return f(id)
}
func TestEmailService_SendRegistrationEmail(t *testing.T) {
service := NewEmailService(GetByIDFunc(func(id uint) (*User, error) {
return nil, errors.New("error")
}))
//
// and just to call the service
}
Following this refactoring, we can easily create a straightforward mock using a new type, GetByIDFunc
. This type
defines a function signature that matches the GetByID
method of the UserRepository
interface. In Go, it’s a common
practice to define a function type and assign a method to it in order to implement an interface. This approach greatly
improves the elegance and efficiency of our testing process. We now have the flexibility to inject different
UserRepository
implementations for various use cases and precisely control the test outcomes.
Some more examples
Breaking the Dependency Inversion Principle (DIP) isn’t limited to structs alone; it can also occur with standalone, independent functions. For instance:
Breaking DIP in Functions
type User struct {
// some fields
}
type UserJSON struct {
// some fields
}
func (j UserJSON) ToUser() *User {
return &User{
// some fields
}
}
func GetUser(id uint) (*User, error) {
filename := fmt.Sprintf("user_%d.json", id)
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var user UserJSON
err = json.Unmarshal(data, &user)
if err != nil {
return nil, err
}
return user.ToUser(), nil
}
We aim to retrieve data for a User
, and for this task, we utilize files in JSON format. The GetUser
method reads
from a file and converts the file content into a User
object. However, this method is tightly coupled with the
presence of these files, making it challenging to write effective tests. This is especially true when we introduce
additional validation rules to the GetUser
method at a later stage. Our code’s heavy reliance on specific details
creates testing difficulties, emphasizing the need for abstractions:
Respecting DIP in Functions
type User struct {
// some fields
}
type UserJSON struct {
// some fields
}
func (j UserJSON) ToUser() *User {
return &User{
// some fields
}
}
func GetUserFile(id uint) (io.Reader, error) {
filename := fmt.Sprintf("user_%d.json", id)
file, err := os.Open(filename)
if err != nil {
return nil, err
}
return file, nil
}
func GetUserHTTP(id uint) (io.Reader, error) {
uri := fmt.Sprintf("http://some-api.com/users/%d", id)
resp, err := http.Get(uri)
if err != nil {
return nil, err
}
return resp.Body, nil
}
func GetDummyUser(userJSON UserJSON) (io.Reader, error) {
data, err := json.Marshal(userJSON)
if err != nil {
return nil, err
}
return bytes.NewReader(data), nil
}
func GetUser(reader io.Reader) (*User, error) {
data, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
var user UserJSON
err = json.Unmarshal(data, &user)
if err != nil {
return nil, err
}
return user.ToUser(), nil
}
With this revised implementation, the GetUser
method depends on an instance of the Reader interface. This interface
is part of the Go core package, IO. Using this approach, we can define various methods
that provide implementations for the Reader
interface, such as GetUserFile
, GetUserHTTP
, or GetDummyUser
(which is useful for testing the GetUser
method). This strategy can be employed in various scenarios to address
challenges related to unit testing or dependency cycles in Go. By introducing interfaces and multiple implementations,
we can achieve effective decoupling.
Conclusion
The Dependency Inversion Principle is the last SOLID principle, represented by the letter D in the word SOLID. This principle asserts that high-level components should not rely on low-level components. Instead, all our components should be built on abstractions, specifically interfaces. These abstractions enable us to use our code with greater flexibility and to conduct thorough testing.