Clean Architecture with GO

Introducing Clean architecture with Go.

Manato Kuroda
15 min readNov 7, 2019
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:

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 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

The 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.

The dependencies only point inward

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?

The flow of control

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.

Person depends on Greet

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 &registry{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:

--

--

Responses (15)