Implementing Clean Architecture in GO
A step by step guide with a real-world example
We always seek to make improvements to our software. It is certain that every few years ( sometimes within months) you will be facing the challenge of updating your software. It is impossible to write perfect software that you never need to change. So instead, we focus on writing software that scales, that can easily be modified and doesn't break when you make changes. To address this problem many programmers came up with code architecture patterns that allow easy modification of code. One such architecture is the “Clean Architecture” — By Robert C Martin popularly know as “Uncle Bob”.
The clean architecture follows the design principle of separation of concerns. In other words, the architecture encourages people to divide the code into layers and each layer is independent of each other. Each layer addresses a separate concern. When code is segmented in such ways, the code can be reused and can be easily replaced or updated without affecting other parts of the code. So suppose you want to move from your SQL Database to a NoSQL database you can do it without affecting the rest of your code.
To keep this post concise and more practical I will not be talking about the particulars of Clean Architecture here. I recommend you to read this Clean Code blog to get a better understanding of the architecture.
To follow along with the code you can check out this Github Repo
The Dependency Rule
Uncle Bob speaks about dividing the code into four layers:
- Domain: Domain is the business logic that is independent of the application. In other words, the domain is common for the whole organization.
- Use Case: Use Case is business logic that is particular to the application.
- Interface: Interface consists of controllers, adapters, presenters. Interfaces allow us to connect our code to Infrastructure.
- Infrastructure: Infrastructure is the layer where all the detail go. Web, Database, third-party libraries, file systems all belong to the interface layer.
The Dependency rules say that any inner layer should never know anything about the outer layer. That is the code in the inner layer should be independent of the code in the outer layer. And this statement confuses a lot of people, so I add one more line to the Dependency rule — “The inner layer tells the outer layer exactly what it needs and its the responsibility of the outer layer to full fill the inner layers requirement.”
Don’t worry you will understand this better once we walk through the code.
The Book Library Application
We will be writing a Book Library App. With the help of this Library app, we will see how to implement the clean architecture in Go.
This is a bare minimum applicaion to keep the focus on clean architecture concepts. In real life scenarios we will have more values and objects.
This is what the file structure of our project looks like
│ ├── author.go
│ └── book.go
│ ├── db
│ │ └── mongodb.go
│ └── router
│ ├── mux-router.go
│ └── router.go
│ ├── controllers
│ │ ├── author-controller.go
│ │ ├── book-controller.go
│ │ └── controller.go
│ └── repository
│ ├── author-repository.go
│ ├── book-repository.go
│ └── repository.go
Our Library Software will be dealing with two entities — authors & books. In the domain layer, we put the piece of code that is core business logic but is independent of the application. So we will write the author and book entity in our domain layer.
As you can see in the
book.go the file we don't have any imports. Because the domain layer is the innermost layer and according to the dependency rule it should be independent of everything. To implement a book entity we have written a struct
Bookthat defines all the parameters of a book type. If there are any Business-specific methods with respect to this entity then it should be implemented in this file.
One thing worth pointing out is that we have a
BookRepository Interface. A repository is a concept of Domain-driven Design, we are specifying that the Book Entity needs functions that enables the books to be fetched from somewhere or to be saved somewhere. We are not specifying where the data will come from or where it will go to — this is a detail, we don't write details for an outer layer in a lower layer. We are just specifying that we need to have a struct that implements methods to persist the data. Now the implementation of the method, whether its a file or a DB or is it in memory we don't specify here. It is up to the outer layers to write the implementation and inject it in the form of
Similarly, We have a
author.go domain file.
Now we can define the application-specific business logic for our Library app. What we want from the app is to allow users to browse all the books present in our Library. We also want the ability to add books and authors to our Library. So in total, we have three use cases:
- Save Author in our library
- Save Book in our library
- Fetch all books from our library
Since this is a small application I didn’t find the need to divide each use-case into different files. So I have divided all the book related use-cases in
book-usecase.go and similarly, all the author related use-cases will go to
author-usecase.go file. In a bigger application, I would have written each use-case in a separate file.
Let’s study the
Note, how we are strictly following the dependency rule, imports only come from the inner layers.
In this file, we have implemented
FindAll() methods. These methods are essentially our use-cases. We have bound our use-case methods with a struct
BookInteractor we have done this to allow for dependency injection.
BookInteractor takes in the value of
BookRepository. And if you remember
BookRepository is an interface with
FindAll() methods. So we are essentially telling the outer layers that if you want to make use of
FindAll() use-cases you have to provide me with a struct of type
Similarly, we have our
Now our business logic is ready. Business logic is the part of code that isn't expected to change much hence it forms the inner layers. The beauty of clean architecture is that at this point, you can decide whatever set of infrastructure you want to use. Let’s say you want to create a command-line application and store your data in a file. Then you can implement interfaces for command line and files. Or if you want to write a Java Program to use your application you can also do that.
In our case, we will be writing a Web interface (APIs) and we will be using Database for persistence. Our aim is to write interfaces for our infrastructure. Our interfaces will be independent of our infrastructure. It will be the responsibility of the infrastructure to implement methods of our interface. So let's say we want to change our database we will only have to make changes in our infrastructure layer.
Now Our application requires two interfaces one for APIs and another for databases. You may want to write more interfaces like command-line, logs, etc. But, to keep it simple I will only implement database and API.
Following the DDD naming convention, I am using the term repository for our database interface. We have a
author-repository.go file for author related database methods and
book-repository.go for book related methods. We will also create the file
repository.go. This file contains the part of code that is common for both author and book.
repository.go file, We will implement a
DBHandler interface. This interface is a template which the database infrastructure will have to implement. In other words, we are telling the database if you need to connect to our application please provide a struct with the following methods
Let’s take a look inside the
book-repository.go file we will implement our
findAll methods which are bounded by the
BookRepo struct. This allows for the
DBHandler injection which in-turn allows us to use the
Note that we have not actually implemented the
DBHandler methods yet. We will be implementing these methods in the infrastructure layer where we write our database related methods. And then inject them in our
Similarly, we have our
With this, we have finished our Database Interface. Notice How we have finished writing the Datavinterface but we never discussed which database we are going to use. What this means is that till this layer we are truly independent of the database we are going to use.
Controllers will be our API interfaces. Controllers will implement all the functions that our endpoints will call to connect to our application. The controller interface allows for the interaction of our application with the API framework that will be implemented in the infrastructure layer.
Let’s take a look in our
All our API endpoints related to
Book are written as functions. ie. methods for our
BookController takes in a
BookInteractor parameter that allows for the injection of
As you can see that the controller methods are fairly simple because the main business logic is handled by
book-usecase with the help of
Similarly, we have
We have finally reached our last and the outermost layer. The Infrastructure layer. We have two infrastructure components — Database and Router. Now we can choose the database and router of our choice. I am choosing the MongoDB database and Mux router.
mongodb.go consists of a
DBHandler struct which contains all the code related to the database. One thing we need to make sure is that our database handler should match the
DBHandler interface defined in our interface layer. This is the perfect example of the statement — “it is the responsibility of the outer layer to fulfill the requirements of the inner layer”
mongodb.go file looks like this.
DBHandler struct implements the
SaveAuthor methods that let us persist/fetch data from the database.
mux-router.go consists of a Router Struct Which implements the
SERVE methods. These methods allow you to call there respective methods from
Putting it together
We can finish up by writing our
main.go file which will bind all the layers together and create and serve our application.
The first thing that this code does is connect to the database and create a DBHandler.
Then we have two functions
getAuthorControlller. Inside these functions, we will see how we have a chain of dependency injection. First, we have injected the
DBHandler struct to
BookRepo and created an instance of
BookRepo. Then we injected that
BookInteractor and created a new instance of
bookInteractor which was further injected into the
bookController to return a controller. And then we can finally call the controller methods with there corresponding endpoints.
And with this, we have written a Clean Architecture implementation in GO.
We have created a very simple code that implements Clean Architecture. This is by no means a complete application a lot of improvements can still be made like adding loggers using dotEnvs etc. But I hope that I was able to provide a proper basic idea behind writing software that is scalable and easily manageable.