The objective of this article is to demonstrate how to implement DI in a REST API developed in GO with Gin-Gonic, using DDD and Hexagonal Architecture to guide the structure of our solution, and making use of the folder/project structure explained in Kat Zien’s talk at GopherCon Denver 2018 ( GitHub Repo).
What do we need before we get down to business?
- Knowledge of software design
- Knowledge of design patterns, especially in DI (Dependency Injection)
- Knowledge of GO is a plus
In this repository you can find the code from this article.
The Problem
Let’s say that someone has hired us to create a web system for an educational establishment, we decide to make this system using microservices.The first microservice that we are going to build is the student administration one, and as BackEnd developers, this is where we are going to start. We will create a REST API web service that allows us to perform CRUD operations (Create, Retrieve, Update, and Delete) on a resource. In this case, that resource is Student.
Solution Structure
The first problem that arises is how to structure our project in GO . In this language, there is no official structure guide, but there are some forms adopted by the community considered “good practices”. For me, the easiest to understand is based on a Hexagonal Architecture with DDD (explained by Kat Zien at GopherCon 2018).https://www.youtube.com/watch?v=oL6JBUk6tj0&feature=emb_titleIf you have watched the video and understood the structure, you should come up with something very similar (or indeed identical) to this:
Let’s take a look at each of the folders:
- cmd: This is where our main code will go, in other words, where the project will start and where we want it to be mounted
- cmd/server: Where our main code to initialize the code by running a web server is
- pkg: Where we will put our code related to the business
- pkg/adding: Where we will put our code to perform the operations of adding entities
- pkg/deleting: Where we will put our code to perform the operations of deleting entities
- pkg/listing: Where we will put our code to perform the entity listing operations
- pkg/logging: Where we will put our log service for the application
- pkg/storage: Where we will put our code related to data persistence
- pkg/storage/memory: Where we will find the code related to the persistence of data in memory
As we can see, in pkg/storage we have several data persistence options (in memory and JSON files). Therefore we could use an excellent design pattern to decide which persistence option we want and “inject” it, for which we will create our own DI mechanism in GO.
Note: To delve a little deeper into the topic of control inversion and dependency injection, check out this great article by Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern.
To implement this pattern we need the following.
- An Interface that specifies the messages
- One or more implementations of this interface
- The service that will use the dependency must receive it as a parameter
- A handler that specifies which implementation is to be used
Let’s Code
Now we are going to specify these steps in code:
1. Interface
In this case,
the interface represents a repository of student data,
therefore we will use two methods- GetStudents () and GetStudentById ()
Note that the interface is defined in pkg/listing/service.go. This is an interesting subtlety, since as we are using DDD and Hexagonal Architecture, we are going to write our service and define all the dependencies in a single file, and use how GO performs the “interface implementations” to our advantage.Put simply, when writing our method to obtain a list of students in a service, we know that we will need a data repository, so what better place to define what conditions must be met in these repositories to achieve this objective than in the same file!For example, if we needed to report the creation of a new student a to a message queue (RabbitMQ, Kafka, etc), we would define a MessageBroker interface that has a QueueMessage() method in the service, and then we would need to implement this method in some other package and inject it as a dependency.
2. Implementations of the Interface
This file located in pkg /storage/memory/repository.go is the implementation of in-memory data persistence. As you can see, it implements the GetStudent() and GetStudentById() methods as was defined in the interface of point 1, so according to GO these methods “implement” said interface perfectly and we can use it in our DI pattern.There is not much else to add here, as the pkg/storage /json/repository.go file implements the same methods as this file but the data exists in a JSON file instead of memory.
3. Defining the dependency
Here we must take a quick pause, as in other languages, like C # or Java, we have constructors (special methods that realize the construction of an object), and it is within these constructors that the dependencies are defined.In GO there are no constructors, so what can we do? Well, the answer is very simple- write a method following good practices which emulates a constructor:
In this portion of the code we can see: the service interface (lines 18 to 21), the corresponding struct that will be our implementation of this interface (lines 23 to 27), and the constructor method that receives Repository and logging. Service instances as parameters (a dependency to write logs in the console).So, with this, from our “handler” we can invoke the NewService() method passing the corresponding parameters and we will have achieved our manual injection of dependencies.
4. DI Handler
In the cmd/ server/main.go file we see the following code, so let’s explain it step by step.
- Lines 12 and 13: An instance of a Logging service defined in pkg/logging/service.go is created which will be injected into the business services
- Lines 15 to 20: In-memory persistence is instantiated, the RunSeeds() method is run to get data when using the service, and, if there is an error, the process is cut
- Lines 22 to 24: The adding and listing business services are instantiated with the required dependencies- Repository and logging.Service
Here we could use some logic to decide if we need persistence in memory or JSON file, for example, an environment variable that defines if it is run locally or in a productive environment, etc.
Conclusion
I hope I have been able to present a simple way to implement DI in a DDD-oriented architecture in GO.Keep in mind that the most important thing is:
- Have folders for functionality (adding, removing, updating, pushing, notifying, etc)
- Each one must have its representation of the entity
- Separate the infrastructure from the business logic (storage, queues, eventhubs, etc)
About the Author
Mauricio Bergallo is a Systems Engineer with extensive knowledge in a variety of programming languages. Great experience and understanding with all aspects of the software development life cycle.