Clean Architecture in Go

Rubin
7 min readJun 26, 2021

--

Writing clean, maintainable and testable Api in Go

Photo by Maarten Deckers on Unsplash

Motivation.

I recently finished reading this amazing book on Clean Architecture called “Get Your Hands Dirty on Clean Architecture” by Tom Hombergs. The book was the missing link for me between the Clean Architecture blog by Uncle Bob and actually putting those ideas into code. This book is concise and full of code examples for every concept. I have been constantly practicing the methods outlined in the book and still learning on the same. The book and the associated examples are on Java/Springboot but the underlying principles are language independent. That was the motivation for me to try the methods mentioned in the book and apply to build a simple API using GoLang. Here is the GitRepo for reference.

Clean Architecture.

Clean Architecture Flow of Control

As mentioned in the Clean Architecture blog by Uncle Bob, the different clean architectures have the same underlying principle : Separation of Concerns. That is to isolate one layer of code from another layer in such a way that changes in one layer do not affect the other layer. A simplified diagram of the clean architecture above, shows the control flow. The code control moves from outer layers of external framework (eg. DB, HTTP etc) to the inner layers of controllers. The controllers in turn call the underlying Use Cases where the application specific business rules are coded. Lastly the flow reaches the domain entities where the Enterprise Wide Business rules are coded. The important thing to notice here is the central position the domain entities occupies. Since Domain Entities, and the Use cases, are the innermost layers containing the actual business rules, it should not be affected by the changes to the outermost layers. For example, moving to a new Database or UI changes. In plain and simple terms, the inner layers cannot have reference or depend upon outer layers. But how do we access the DB layers (outer layer) from UseCases(inner layer) as we need to populate the data into Entities ? We will come to that in a bit. Read on.

Hexagonal Architecture.

One such implementation of the Clean Architecture is Hexagonal Architecture.

Hexagonal Architecture

As can be seen from the diagram above, the isolation of Domain (Use Cases and Entities) is achieved through Dependency Inversion. The dependency inversion happens at the input and output ports as shown above. The ports are interfaces with functionalities defined. Use Cases (Service Layer) implement the input ports. The controller actually talks to the input port and has no idea on the service layer and the use case that implements it. Similarly, the problem of accessing DB layer from Use Cases is also solved here by talking to the output ports. The output ports are implemented by external adapters. Lets put this knowledge into code now.

Problem Statement.

With this knowledge about code architecture, lets try to implement a simple use case. Lets build an api which returns the employee details and also calculates the total pay from the basic and bonus data returned from the DB. We will start with an In-Memory DB (more like in-memory map) and Go’s net/http package for building the api. Once we are done, and if we have followed the theory correctly, we will attempt to swap the DB with Postgres and use gorilla/mux for api with no impact to the underlying Domain code. Let’s Begin.

TDD and Domain.

Since the Domain code is isolated from external frameworks and layers, we can and should cover the code in this layer with Unit Tests and achieve a high-code coverage. For example, for the employee entity we start with a unit test for the total pay calculation.

With a failing test written we will write code, to make it pass.

With this kind of incremental and tested change we can quickly build domain logic. Also this is a good place to implement proper design patterns to make your code more flexible and maintainable.

TDD and Application.

Just like the Domain layer, we can and should add Unit Tests for code in the Service layer (Use Cases). Since Use Cases might be using Output Ports, this is a good place to mock them out in the tests and focus on testing the application logic. Lets write a sample test for the GetEmployeeDetailsUseCase.

Few important things to notice here are we are not actually interacting with a DB at this point. As can be seen from the test we can easily mock the LoadEmployeeDataPort (Output Port) and only test the Application logic contained within the service. Lets have a look at the service and see how it implements an Input Port (Use Case) and uses Output Ports for getting data.

The two functions GetEmployeeDetails and GetAllEmployees are declared in the the interface GetEmployeeDetailsQuery. Since the service defines these two functions, it implicitly implements the Input Port which is given below.

Integration Tests and Adapters.

Now that we have written our inner layers of Domain and Application and covered them with Unit Tests, its time to move a layer above. We reach the layer where Adapters reside. Adapters are of two kinds : Driver Adapters (Trigger Flow Control into the inner Domain layer)and Driven Adapters (These are triggered by, usually by the inner layers). In our case a simple controller is the driving adapter and a persistence adapter is the driven adapter. But the most important thing is, what kind of testing should we do here. Since we have covered the core domain logic with Unit Tests, it makes more sense to write some Integration tests in this layer to see if the interactions are working as expected. For example, in case of web controllers we might want to test if the underlying service is indeed getting triggered and a proper response is being sent. Similarly, in case of DB, we want to actually hit the DB and see if the interactions are working. I have used docker compose to start a postgres container for PersistenceAdapter Integration testing. For testing the web layer used Go’s net/http/httptest package.

Wire it all up.

Now that we have defined all the code pieces we need to wire them up. Usually using a factory pattern is the best way to do it. For simplicity and sheer laziness I have used main.go file to wire all the pieces together.

Notice the DB. Calling it an InMem DB is an overkill :). Its just a simple map of employee data.

Lets start the api and see if we are getting the results as expected:

Awesome. We are able to fetch the employee details from inmemory map and also see that the total Salary calculation is happening.

Moment of truth.

With the wiring working correctly and flow implemented, its time to put into practice what we were trying to achieve in the first place. Swap out the DB to Postgres and the router to gorilla/mux and see how it impacts our code.

First we swap the DB out

Second we change the router

Now lets restart the api with the changes and hit the endpoints again. I have inserted a different set of users in Postgres just to differentiate between the outputs received from in memory map.

And the results from the api. TADA!!! :

The api call works just fine. All we had to do was rewire in the main.go file and add postgres repository code implementing the repository interface. No code changes in the adapter, service or domain layer.

Conclusion.

Following best practices and clean architecture not only helps write maintainable and clean code, it also helps speed up the overall development process. But all the abstractions and separation of concerns does bloat the overall code. This is a necessary step we need to deal with if we really want to write code which is rich in Domain and Application rules. For example, the employee api above follows the happy path. How would you change or add validations for different data in the api ? No matter what the extra logic we can think of for the example, not only is there a specific place for the new code to go, but also clear testing strategies on how to validate those changes. With proper isolation we were able to demo that with Clean Architecture in code we can very well isolate Domain/Application and External Frameworks. Happy Coding!!!

References.

--

--