Clean Architecture with GO
Introducing Clean architecture with Go.
Clean Architecture is designed to separate concerns by organizing code into several layers with a very explicit rule which enables us to create a testable and maintainable project. In this article, I’m going to walk you through how Clean Architecture works in Go.
Example repo
You can view the full codebase at:
Updated:
- Added transactions support
- Fixed duplicated repository
- Updated codebase structure
Pre-requisites
The target of readers in this post is who:
- Knows the basic idea of Clean Architecture
- Wants to implement Go with Clean Architecture
So if you are not familiar with Clean Architecture, you can read some recommended articles to catch up.
- The Clean Code Blog
- Clean Architecture: Part 2 — The Clean Architecture
- Better Software Design with Clean Architecture
The darkness of software design
If you’ve been working as a programmer, you’ve probably run into the code that:
- Is painful to add new features
- Is too difficult to debug
- Is hard or impossible to test without dependencies like a web server or database
- Get a view and business logic mixed in widely, even can’t be separated
- Is hard to understand what its purpose for
- Has a lot of jobs only in one function and is too long.
Even applications structured with MVC may leak business logic to the controller, or the domain model may be used throughout the project. When this happens, it may even be frustrating to commit because you don’t know how much it affects other functions.
Advantages of Clean Architecture
Clean Architecture is one of the software designs for organizing the codebase and provides the solution to these issues you’ve ever seen. By introducing Clean Architecture you’ll get your code that is:
- Highly decoupled from any UI, web framework, or the database
- Focusing more on business logic
- Easily testable
- Maintainable, visible, and easier to understand
In Clean Architecture the codebase should be flexible and portable. It should not be dependent on any specific web frameworks or databases. This means that you could switch them to an entirely new platform. For example, if you use an RDB at the beginning of a project, but for some reason have to replace it with NoSQL, you can switch without changing any business logic.
The only rules you need to know
When you implement Clean Architecture, you might try to follow the circle diagram completely, but actually, the author said:
Only Four Circles? No, the circles are schematic… There’s no rule that says you must always have just these four…
So you don’t need to implement it as it is, take the diagram as an example for understanding the architecture. But there are important rules that you need to follow.
1. The dependencies can only point inward
In Clean Architecture, the details like a web framework and databases are in the outer layers while important business rules are in the inner circles and have no knowledge of anything outside the world. Following Acyclic Dependencies Principle(ADP), the dependencies only point inward in the circle, not point outward, and have no circulation.
2. Separation of details and abstracts
The details in Clean Architecture are the data, framework, database, and APIs. Using the details in the core layer means it violates The Dependency Rule. It should always be dependent on an abstract interface not specific knowledge of the details so it will be flexible and maintainable. As seen at the right of the diagram, it shows that the Controllers and the Presenters are dependent on Use Case Input Port and Output Port which is defined as an interface, not specific logic(the details). But how is it possible to work without knowing the details of the outer layer?
Dependency Inversion Principle
The Dependency Inversion Principle (DIP) resolves this contradiction without violating the rules.
DIP is the idea of designing core modules that do not depend on other modules in an abstract way.
For example, consider a situation where the Person class depends on the Greet class.
The code should look like this:
class Greet {
say(): string {
return "Hello"
}
}
class Person {
greet() {
const greet = new Greet()
console.log(greet.say()) // Hello
}
}
If you want to reverse the dependency, it should look like this:
In order to archive this, we need an interface between them.
When the IGreet is considered as part of the Person class, the relationship between the Person and the Greet is reversed.
Let’s change the Person class:
+ interface IGreet {
+ say(): string
+ }
class Person {
+ greetService: IGreet
+ constructor(greetService: IGreet) {
+ this.greetService = greetService
+ }
greet() {
- const greet = new Greet()
- console.log(greet.say()) // Hello
+ console.log(this.greetService.say()) // hello
}
}
When the Person is initialized, a specific implementation that conforms to the IGreet interface needs to be passed. This means the Person depends on the IGreet:
And change the Greet class:
class Greet implements IGreet {
say(): string {
return "Hello"
}
}
The Greet class needs to conform to the IGreet interface like so.
Then when you use the Person class, pass the instance of Greet like so:
const greet = new Greet()
const person = new Person(greet)
console.log(person.greet()) // Hello
If you want to use a different class to say hello, create a new one:
class Greet2 implements IGreet {
say(): string {
return "Hey what's up"
}
}
Then pass it to the Person class:
const greet = new Greet2()
const person = new Person(greet)
console.log(person.greet()) // Hey what's up
That is a basic idea of DIP. We will apply this concept to Clean Architecture.
If you want to know more about DIP, you can check out some articles before proceeding.
The Layers
We’ve covered the basic rules of Clean Architecture.
Next, let’s take a look at the layers.
Entities
Entities are domain models that have enterprise business rules and can be a set of data structures and functions.
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
Age string `json:"age"`
}
Use Cases
Use cases handle application business logic and have the Input Port and the Output Port. These two ports correspond to the input and output of the server. The purpose of having ports in the Use cases is to isolate business logic from specific implementations. To separate business logic, the ports are defined as interfaces with no specific implementation, allowing the implementation to be changed without changing the business logic.
Input Port and Output Port
Let’s look at how the ports work.
Input Port is defined as an abstract interface that handles incoming data of the server.
Suppose we have an input port that gets user data, the interface looks like this:
package usecase
type UserInput interface {
Get(id int) error
}
Output Port is defined as an abstract interface that handles the output data of the server.
Suppose we have an output port that responds to the clients from the server, the interface looks like this:
package usecase
type UserOutput interface {
Render(user *model.User)
}
Then, the usecase should look like this:
package usecase
type userUsecase struct {
output UserOutput
}
func NewUserUsecase(output UserOutput) UserInput {
return &userUsecase{output}
}
func(u *userUsecase) Get(id int) error {
// Get user from database.
user, err := xxx.getUserByID(id)
if err != nil {
return err
}
// Responds to a client through the Output Port.
u.output.Render(user)
}
The usecase provides the Get
(Input Port) to handle an incoming request of the server and responds to the clients through the Render
(Output Port).
If you use the usecase in a controller, you need to prepare a specific implementation of the Output Port and pass it to the usecase like so:
package controller
type User struct {}
func (u *User) GetUser(ctx Context) error {
id := getIDFromContext(ctx)
// Pass a specific implementation of Output port and generate the usecase.
outputPort := presenter.NewUser(ctx)
userUsecase := usecase.NewUserUsecase(outputPort)
err := userUsecase.Get(id)
if err != nil {
return err
}
}
Since the Output Port handles the response and sends data to a client, the controller does not need to return anything here.
Then, the specific implementation of the Output Port should look like this:
package presenter
type User struct {
ctx Context
}
// Need to conform to the Output interface here.
func NewUser(ctx Context) usecase.UserOutput {
return &User{ctx}
}
func (u *User) Render(user *model.User) {
// json, csv, pdf, xml or whatever you want.
u.ctx.JSON(200, user)
}
The Render returns user data in JSON format in this case. If you conform to the Output interface, you can write any implementation.
Other ports
Ports expose an interface and decouple business logic from a specific implementation. A specific implementation needs to access an external service, such as accessing databases or querying external APIs. But we can’t write such an implementation in the usecase since it would lose flexibility between the layers and make it hard to change the business logic.
In order to access external services in the usecase, another port can be exposed.
Let’s suppose we need to access a database and get user data and define a port called repository
.
First, write the interface for the repository.
type UserRepository interface {
GetUserByID(id int) (*model.User, err)
}
And add the repository to the usecase.
type userUsecase struct {
output UserOutput
+ repository UserRepository
}
func NewUserUsecase(output UserOutput) UserInput {
return &userUsecase{output}
}
func(u *userUsecase) Get(id int) error {
// Get user from database.
- user, err := xxx.getUserByID(id)
+ user, err := u.repository.GetUserByID(id)
if err != nil {
return err
}
// Responds to a client through the Output Port.
u.output.Render(user)
}
Now that we’ve defined the port and exposed the interface for getting data from a database. Next, we will implement a specific code like so:
package gateway
type User struct {
db Database
}
// Need to conform to the UserRepository interface here.
func NewUser(db Database) usecase.UserRepository {
return &User{db}
}
func (u *User) GetUserByID(id int) (*model.User, error) {
// get user data
user, err := u.db.User.Find(id)
if err != nil {
return nil, err
}
return user, nil
}
And pass the implementation to the usecase in the controller like so:
package controller
type User struct {}
func (u *User) GetUser(ctx Context) error {
id := getIDFromContext(ctx)
// Pass a specific implementation of Output port and generate the usecase.
outputPort := presenter.NewUser(ctx)
+ // Pass a specific implementation of the repository.
+ db := createDB()
+ repositoryPort := gateway.NewUser(db)
- userUsecase := usecase.NewUserUsecase(outputPort)
+ userUsecase := usecase.NewUserUsecase(outputPort, repositoryPort)
err := userUsecase.Get(id)
if err != nil {
return err
}
}
In this case, we added the repository port for the database. If your application needs to send a mail after getting user data, then you could add a port called mail
with interfaces in the usecase and implement the specific mail function as we did above.
However, which layer should we define the specific implementation ? In Clean Architecture, they need to be defined in the Interface Adapter layer.
Interface Adapter
Interface Adapter handles the communication with the inner and the outer layer and provides a specific implementation for the ports that the Use Case Layer has. In Clean Architecture, there are three adapters defined.
Controllers are a set of implementations that handle incoming requests of servers. Controllers depend on the usecase layer and execute processing through their interfaces.
package controller
type User struct {}
func (u *User) GetUser(ctx Context) error {
id := getIDFromContext(ctx)
// Pass a specific implementation of Output port and generate the usecase.
outputPort := presenter.NewUser(ctx)
// Pass a specific implementation of the repository.
db := createDB()
repositoryPort := gateway.NewUser(db)
userUsecase := usecase.NewUserUsecase(outputPort, repositoryPort)
err := userUsecase.Get(id)
if err != nil {
return err
}
}
Presenters are a set of specific implementations of the Output Port. Presenters handle responses to the clients and the response can be JSON, HTML, XML, CSV, or whatever your application requires.
package presenter
type User struct {
ctx Context
}
func NewUser(ctx Context) usecase.UserOutput {
return &User{ctx}
}
func (u *User) Render(user *model.User) {
// json, csv, pdf, xml or whatever you want.
u.ctx.JSON(200, user)
}
Gateways are a set of implementations for storing data in databases. You can choose any databases or ORM libraries if they conform to the interfaces in the usecase layer.
package gateway
type User struct {
db Database
}
// Need to conform to the UserRepository interface here.
func NewUser(db Database) usecase.UserRepository {
return &User{db}
}
func (u *User) GetUserByID(id int) (*model.User, error) {
// get user data
user, err := u.db.User.Find(id)
if err != nil {
return nil, err
}
return user, nil
}
In Clean Architecture, these three adapters are defined, but in real cases, an application may have different requirements such as sending emails, push notifications, logging, and so on. So, you don’t have to define only three adapters, you can have different ports and adapters depending on the needs of your application. On the other hand, if you feel that your application does not need the Presenters, you can write the output implementation directly from the Controllers. The key is to protect dependencies and make the architecture flexible.
Frameworks and Drivers
Frameworks and drivers contain tools like databases, frameworks, or routers and basically do not have very much code.
package datastore
import (
"log"
"github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
func NewDB() *gorm.DB {
DBMS := "mysql"
mySqlConfig := &mysql.Config{
User: "root",
Passwd: "root",
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "golang_clean_architecture",
AllowNativePasswords: "true",
Params: map[string]string{
"parseTime": "true",
},
}
db, err := gorm.Open(DBMS, mySqlConfig.FormatDSN())
if err != nil {
log.Fatalln(err)
}
return db
}
Implementation of Clean Architecture in GO
Next, we will implement a Clean Architecture in Go and see how it works.
Let’s suppose that we will create a simple API responding to user data.
We have the project structure below:
pkg/
├── adapter
│ ├── controller
│ │ ├── app.go
│ │ ├── context.go
│ │ └── user.go
│ └── repository
│ ├── db.go
│ └── user.go
├── domain
│ └── model
│ └── user.go
├── infrastructure
│ ├── datastore
│ │ └── db.go
│ └── router
│ └── router.go
├── registry
│ ├── registry.go
│ └── user.go
└── usecase
├── repository
│ ├── db.go
│ └── user.go
└── usecase
└── user.go
In this case, we don’t have the Output Port and the Presenter because, in the most recent API servers, people are familiar with the architecture in that controllers handle incoming requests and output data as JSON. In most cases, that is fine. So If you don’t see any benefit in having the Output Port, then it’s okay without it.
Each directory corresponds to the layers:
Note that there’s no rule that you must always have just these four layers in Clean Architecture so it really depends on the team and the project.
Entities Layer
First off, we create a user
domain model as Entities layer in domain/model/user.go
:
package model
import "time"
type User struct {
ID uint `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Age string `json:"age"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
}
func (User) TableName() string { return "users" }
Use Cases Layer
In the Use cases layer, we have two directories, repository
and usecase
. The usecase defines the Input Port and uses the repository interface to get user data.
Let’s create an interface for the repository in usecase/repository/user.go
:
package repository
import "golang-clean-architecture/pkg/domain/model"
type UserRepository interface {
FindAll(u []*model.User) ([]*model.User, error)
}
Create a usecase for finding all users in usecase/usecase/user.go
:
package usecase
import (
"errors"
"golang-clean-architecture/pkg/domain/model"
"golang-clean-architecture/pkg/usecase/repository"
)
type userUsecase struct {
userRepository repository.UserRepository
}
type User interface {
List(u []*model.User) ([]*model.User, error)
}
func NewUserUsecase(r repository.UserRepository, d repository.DBRepository) User {
return &userUsecase{r, d}
}
func (uu *userUsecase) List(u []*model.User) ([]*model.User, error) {
u, err := uu.userRepository.FindAll(u)
if err != nil {
return nil, err
}
return u, nil
}
Interface Adapter Layer
In the Interface Adapter layer, we have a controller
and repository
. The controller handles incoming requests of the server. The repository
implements specific implementations of getting data from the database and conforms to the interfaces in the usecase and acts as the Gateway.
Let’s create a adapter/controller/user.go
:
package controller
import (
"golang-clean-architecture/pkg/usecase/usecase"
"net/http"
"golang-clean-architecture/pkg/domain/model"
)
type userController struct {
userUsecase usecase.User
}
type User interface {
GetUsers(ctx Context) error
}
func NewUserController(us usecase.User) User {
return &userController{us}
}
func (uc *userController) GetUsers(ctx Context) error {
var u []*model.User
u, err := uc.userUsecase.List(u)
if err != nil {
return err
}
return ctx.JSON(http.StatusOK, u)
}
And we add a adapters/controller/app.go
:
package controller
type AppController struct {
User interface{ User }
}
That is used in the infrastructure/router/router.go
. We will implement it later.
And add a adapter/repository/user.go
:
package repository
import (
"golang-clean-architecture/pkg/domain/model"
"golang-clean-architecture/pkg/usecase/repository"
"github.com/jinzhu/gorm"
)
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) repository.UserRepository {
return &userRepository{db}
}
func (ur *userRepository) FindAll(u []*model.User) ([]*model.User, error) {
err := ur.db.Find(&u).Error
if err != nil {
return nil, err
}
return u, nil
}
The FindAll is an actual implementation of the interface of usecase/repository/user.go
that we’ve defined above. In this case, we use the gorm as ORM. Technically, the ORM should be defined as an adapter in order to follow the rule of specific technology separation, but if you feel that it’s too separated, then it’s fine to use it as is. Even if you change another ORM in the future, then you only need to change the code in the repository at that time.
Frameworks and Drivers Layer
We have datastore
and router
in the infrastructure. The datastore package is used as creating a database instance, in this case, we use gorm with MySQL. The router package is defined as a routing request using echo.
Let’s create a infrastructure/datastore/db.go
:
package datastore
import (
"log"
"golang-clean-architecture/config"
"github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
func NewDB() *gorm.DB {
DBMS := "mysql"
mySqlConfig := &mysql.Config{
User: config.C.Database.User,
Passwd: config.C.Database.Password,
Net: config.C.Database.Net,
Addr: config.C.Database.Addr,
DBName: config.C.Database.DBName,
AllowNativePasswords: config.C.Database.AllowNativePasswords,
Params: map[string]string{
"parseTime": config.C.Database.Params.ParseTime,
},
}
db, err := gorm.Open(DBMS, mySqlConfig.FormatDSN())
if err != nil {
log.Fatalln(err)
}
return db
}
And create a infrastructure/router/router.go
:
package router
import (
"golang-clean-architecture/pkg/adapter/controller"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func NewRouter(e *echo.Echo, c controller.AppController) *echo.Echo {
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/users", func(context echo.Context) error { return c.User.GetUsers(context) })
return e
}
Registry
The registry handles resolving dependencies between ports and adapters, using constructor injection.
Let’s create a registry/registry.go
:
package registry
import (
"golang-clean-architecture/pkg/adapter/controller"
"github.com/jinzhu/gorm"
)
type registry struct {
db *gorm.DB
}
type Registry interface {
NewAppController() controller.AppController
}
func NewRegistry(db *gorm.DB) Registry {
return ®istry{db}
}
func (r *registry) NewAppController() controller.AppController {
return controller.AppController{
User: r.NewUserController(),
}
}
And add a registry/user.go
:
package registry
import (
"golang-clean-architecture/pkg/adapter/controller"
"golang-clean-architecture/pkg/adapter/repository"
"golang-clean-architecture/pkg/usecase/usecase"
)
func (r *registry) NewUserController() controller.User {
u := usecase.NewUserUsecase(
repository.NewUserRepository(r.db),
)
return controller.NewUserController(u)
}
The NewUserUsecase
initializes the usecase with a repository. The NewUserRepository
creates a repository that conforms to the usecase interfaces. And the NewUserController
generates a controller with the usecase.
Booting
Now that we’re ready to boot a server with the users
endpoint.
Let’s write some code to main.go
:
package main
import (
"fmt"
"log"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/labstack/echo"
"golang-clean-architecture/pkg/config"
"golang-clean-architecture/pkg/infrastructure/datastore"
"golang-clean-architecture/pkg/infrastructure/router"
"golang-clean-architecture/pkg/registry"
)
func main() {
config.ReadConfig()
// Create an orm instance and connect to the database.
db := datastore.NewDB()
db.LogMode(true)
defer db.Close()
// Resolve dependendies between each layer.
r := registry.NewRegistry(db)
// Create controllers and routers to hanlde the requests.
e := echo.New()
e = router.NewRouter(e, r.NewAppController())
fmt.Println("Server listen at http://localhost" + ":" + config.C.Server.Address)
if err := e.Start(":" + config.C.Server.Address); err != nil {
log.Fatalln(err)
}
}
We have echo
framework for routing and passing it to the router and controllers created by registry.NewRegistry
.
Let’s try to see how it works at http://localhost:8080/users
:
It seems to work fine.
Conclusion
We’ve created a very simple API responding to user data with Clean Architecture. I like the idea of separating the different aspects of the product as highly decoupled layers.
I hope you will find the benefits I highlighted in this article.
You can view the final codebase at: