DEV Community

Elton Minetto
Elton Minetto

Posted on • Updated on

Clean Architecture, 2 years later

UPDATE: This post is old and no longer reflects what I believe to be an ideal structure for a project. In 2023, I am using and recommending what my colleagues and I have described in this post.

In February 2018 I wrote what would become the most relevant text I have ever published: Clean Architecture using Golang. With more than 105k views, the post generated presentations at some Go and PHP events and allowed me to talk about software architecture with several people.

Using this architecture for the development of Codenation's products, we gained experience and solved problems. We wrote some posts reporting these experiences:

After this whole experience I can say::

Choosing Clean Architecture was the best technical decision we made!

With this post, I want to share a repository with a new example implementation in Go. It is an update with improvements in the organization of codes and directories, as well as a more complete example for those who are looking to implement this architecture.

In the next topics, I explain what each directory means.

Entity layer

Let's start with the innermost layer of the architecture.

According to Uncle Bob's post:

Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.

The structure looked like this:

entity

In this package we have the definition of our entities and their respective unit tests. For example, the entity user:

package entity

import (
    "time"

    "golang.org/x/crypto/bcrypt"
)

//User data
type User struct {
    ID        ID
    Email     string
    Password  string
    FirstName string
    LastName  string
    CreatedAt time.Time
    UpdatedAt time.Time
    Books     []ID
}

//NewUser create a new user
func NewUser(email, password, firstName, lastName string) (*User, error) {
    u := &User{
        ID:        NewID(),
        Email:     email,
        FirstName: firstName,
        LastName:  lastName,
        CreatedAt: time.Now(),
    }
    pwd, err := generatePassword(password)
    if err != nil {
        return nil, err
    }
    u.Password = pwd
    err = u.Validate()
    if err != nil {
        return nil, ErrInvalidEntity
    }
    return u, nil
}

//AddBook add a book
func (u *User) AddBook(id ID) error {
    _, err := u.GetBook(id)
    if err == nil {
        return ErrBookAlreadyBorrowed
    }
    u.Books = append(u.Books, id)
    return nil
}

//RemoveBook remove a book
func (u *User) RemoveBook(id ID) error {
    for i, j := range u.Books {
        if j == id {
            u.Books = append(u.Books[:i], u.Books[i+1:]...)
            return nil
        }
    }
    return ErrNotFound
}

//GetBook get a book
func (u *User) GetBook(id ID) (ID, error) {
    for _, v := range u.Books {
        if v == id {
            return id, nil
        }
    }
    return id, ErrNotFound
}

//Validate validate data
func (u *User) Validate() error {
    if u.Email == "" || u.FirstName == "" || u.LastName == "" || u.Password == "" {
        return ErrInvalidEntity
    }

    return nil
}

//ValidatePassword validate user password
func (u *User) ValidatePassword(p string) error {
    err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(p))
    if err != nil {
        return err
    }
    return nil
}

func generatePassword(raw string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(raw), 10)
    if err != nil {
        return "", err
    }
    return string(hash), nil
}
Enter fullscreen mode Exit fullscreen mode

Use Case Layer

According to Uncle Bob::

The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system

The structure look like this:

domain

In packages within usecase we implement the other business rules of our product.

For example, the file usecase\loan\service.go:

package loan

import (
    "github.com/eminetto/clean-architecture-go-v2/entity"
    "github.com/eminetto/clean-architecture-go-v2/usecase/book"
    "github.com/eminetto/clean-architecture-go-v2/usecase/user"
)

//Service loan usecase
type Service struct {
    userService user.UseCase
    bookService book.UseCase
}

//NewService create new use case
func NewService(u user.UseCase, b book.UseCase) *Service {
    return &Service{
        userService: u,
        bookService: b,
    }
}

//Borrow borrow a book to an user
func (s *Service) Borrow(u *entity.User, b *entity.Book) error {
    u, err := s.userService.GetUser(u.ID)
    if err != nil {
        return err
    }
    b, err = s.bookService.GetBook(b.ID)
    if err != nil {
        return err
    }
    if b.Quantity <= 0 {
        return entity.ErrNotEnoughBooks
    }

    err = u.AddBook(b.ID)
    if err != nil {
        return err
    }
    err = s.userService.UpdateUser(u)
    if err != nil {
        return err
    }
    b.Quantity--
    err = s.bookService.UpdateBook(b)
    if err != nil {
        return err
    }
    return nil
}

//Return return a book
func (s *Service) Return(b *entity.Book) error {
    b, err := s.bookService.GetBook(b.ID)
    if err != nil {
        return err
    }

    all, err := s.userService.ListUsers()
    if err != nil {
        return err
    }
    borrowed := false
    var borrowedBy entity.ID
    for _, u := range all {
        _, err := u.GetBook(b.ID)
        if err != nil {
            continue
        }
        borrowed = true
        borrowedBy = u.ID
        break
    }
    if !borrowed {
        return entity.ErrBookNotBorrowed
    }
    u, err := s.userService.GetUser(borrowedBy)
    if err != nil {
        return err
    }
    err = u.RemoveBook(b.ID)
    if err != nil {
        return err
    }
    err = s.userService.UpdateUser(u)
    if err != nil {
        return err
    }
    b.Quantity++
    err = s.bookService.UpdateBook(b)
    if err != nil {
        return err
    }

    return nil
}

Enter fullscreen mode Exit fullscreen mode

We also found the mocks generated by Gomock, as explained in this post. The other layers of the architecture will use this mocks during the tests.

Frameworks and Drivers layer

According to Uncle Bob:

The outermost layer is generally composed of frameworks and tools such as the Database, the Web Framework, etc.This layer is where all the details go.

driver

For example, in the file infrastructure/repository/user_mysql.go we have the implementation of the interface Repository in MySQL. If we need to change to another database, this is where we would create the new implementation.

Interface Adapters layer

The codes in this layer adapt and convert the data to the format used by the entities and use cases for external agents such as databases, web, etc.

In this application, there are two ways to access the UseCases. The first is through an API and the second is using a command line application (CLI).

The CLI's structure is very simple:

cli

It makes use of domain packages to perform a book search:

dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true", config.DB_USER, config.DB_PASSWORD, config.DB_HOST, config.DB_DATABASE)
db, err := sql.Open("mysql", dataSourceName)
if err != nil {
    log.Fatal(err.Error())
}
defer db.Close()
repo := repository.NewBookMySQL(db)
service := book.NewService(repo)
all, err := service.SearchBooks(query)
if err != nil {
    log.Fatal(err)
}
for _, j := range all {
    fmt.Printf("%s %s \n", j.Title, j.Author)
}
Enter fullscreen mode Exit fullscreen mode

In the example above, you can see the use of the config package. You can see its structure below, and more details in this post.

config

The API structure is more complex, with three packages: handler, presenter, and middleware.

The handler package handle HTTP requests and responses, as well as using existing business rules in the usecases.

handler

The presenters are responsible for formatting the data generated as a response by handlers.

presenter

In this way, the entity User:

type User struct {
    ID        ID
    Email     string
    Password  string
    FirstName string
    LastName  string
    CreatedAt time.Time
    UpdatedAt time.Time
    Books     []ID
}
Enter fullscreen mode Exit fullscreen mode

It can be transformed into:

type User struct {
    ID        entity.ID `json:"id"`
    Email     string    `json:"email"`
    FirstName string    `json:"first_name"`
    LastName  string    `json:"last_name"`
}
Enter fullscreen mode Exit fullscreen mode

This gives us control over how an entity will be delivered via the API.

In the last package of the API we find the middlewares, used by several endpoints:

middlware

Support packages

They are packages that provide common functionality such as encryption, logging, file handling, etc. These features are not part of the domain of our application, and all the layers can use them. Even other applications can import and use these packages.

pkg

The README.md contains more details, such as instructions for compilation and usage examples.

My goals with this post to strengthen my recommendation on this architecture and also to receive feedback about the codes.

If you want to learn how to use this architecture in your favorite programming language, you could use this repository as an example of this learning. That way, we can have different implementations, in different languages, to ease the comparison.

Special thanks to my friend Gustavo Schirmer who gave great feedbacks on the text and the codes.

Latest comments (3)

Collapse
 
avieramoluy profile image
Agustín Viera • Edited

Elton,
Nice explanation, I'm trying to apply this architecture in my personal projects, but I have 2 questions for you:

  1. Does "usecase" act like as "service" of the previous post or that responsability is from the "manager"?
  2. How to structure my project if I want to use an ORM like GORM, ¿which is the appropiate place to put things like "gorm.Model" field and tag "gorm: ..." if its not in the entity.go? ¿Should I create a "DTO" in the repository implementation? I understand entity shouldnt know nothing about the persistance just like the API (json tags)

Thank you for reading, waiting for your response.

Collapse
 
yvespareteum profile image
yvespareteum

DTOs don't go into the repositories. It is on the outside layers it resides, precisely in the "Adapters" layer ; that layer is an enabler to the "Ports" layer, where you have the interactions with the outside world (eg. an API).
The ORM has to be abstracted within the repository.

Collapse
 
eminetto profile image
Elton Minetto

Hello Agustín, sorry for the late reply.

  1. Yes, UseCase is the Service of the previous post. The Manager is a simpler UseCase, which only handles one entity. I gave different names to make it easier to understand the difference between them.
  2. Conceptually the most "correct" would be to create a DTO, but you can be a little more flexible in that sense and put the tags on the entity. I don't see it as such a critical problem to put the tags on the entity if that makes the project less complex.

Thanks for the positive feedbacks regarding the text.