back to listing index

Clean Architecture in Go – Yusuke Hatanaka – Medium

[web search]
Original source (medium.com)
Tags: golang go software-architecture clean-architecture
Clipped on: 2018-11-07
Image (Asset 1/6) alt=So I tried to make one example that is conscious of clean architecture in Go using gRPC.

This small project represents user registration example. Please feel free to respond anything.

The Structure

8am is based on clean architecture, the project structure is like below.

% tree
.
├── Makefile
├── README.md
├── app
│ ├── domain
│ │ ├── model
│ │ ├── repository
│ │ └── service
│ ├── interface
│ │ ├── persistence
│ │ └── rpc
│ ├── registry
│ └── usecase
├── cmd
│ └── 8am
│ └── main.go
└── vendor
├── vendor packages
|...

The top directory contains three directories:

  • app: application package root directory
  • cmd: main package directory
  • vendor: several vendor packages directory

Clean architecture has some conceptual layer like below:

Image (Asset 2/6) alt=So I named this layer to app/domain due to that is confused with the entity of DDD.

app/domain has three packages:

  • model: has aggregate, entity and value object
  • repository: has repository interfaces of aggregate
  • service: has application services that depend on several models

I explain what detail of implementation for each package.

model

model has user aggregate like below:

This is not actually aggregate, but I hope you will be on a premise that various entity and value object will be added in the future.
package model

type User struct {
id string
email string
}

func NewUser(id, email string) *User {
return &User{
id: id,
email: email,
}
}

func (u *User) GetID() string {
return u.id
}

func (u *User) GetEmail() string {
return u.email
}

The aggregate is the boundary of a transaction in order to keep their consistency of business rules. Thus there is one repository against the one aggregate.

repository

In this layer, the repository is just an interface because of that should not know about the detail of persistence implementation. But also persistence is an important essence for this layer.

implementation of the user aggregate repository is:

package repository

import "github.com/hatajoe/8am/app/domain/model"

type UserRepository interface {
FindAll() ([]*model.User, error)
FindByEmail(email string) (*model.User, error)
Save(*model.User) error
}

FindAll fetches all users who are persisted in the system. And Save persists user to the system. I say it again, this layer should not know what where is the object is saved or serialized.

service

The service layer is gathered business logic that should not be included in the model. For example, this application don’t allow existing email address registration. If model has this validation, we will get to feel something wrong like below:

func (u *User) Duplicated(email string) bool {
// Find user by email from persistence layer...
}

Duplicated function is not related to User model.
For solving this, we can add the service layer like below:

type UserService struct {
repo repository.UserRepository
}
func (s *UserService) Duplicated(email string) error {
user, err := s.repo.FindByEmail(email)
if user != nil {
return fmt.Errorf("%s already exists", email)
}
if err != nil {
return err
}
return nil
}

...

Entities contain business logic and interface through the other layers.
Business logic should be included in the model and service, and should not be depended any other layers. If we need to access any other layer, we should through the layers using repository interface. By inverting the dependencies like this, may packages to be isolated, more testable and maintainable.

Use Cases — the red layer

Use cases are unit of the one operation for application.
In 8am, listing user and user registration are defined as use case.
Those use cases are represented this interface below:

type UserUsecase interface {
ListUser() ([]*User, error)
RegisterUser(email string) error
}

Why is it an interface? This is because use case is used from interface layer — the green layer. We should always define interface if we are going to through between layers.

UserUsecase implementation is simple like below:

type userUsecase struct {
repo repository.UserRepository
service *service.UserService
}
func NewUserUsecase(repo repository.UserRepository, service *service.UserService) *userUsecase {
return &userUsecase {
repo: repo,
service: service,
}
}
func (u *userUsecase) ListUser() ([]*User, error) {
users, err := u.repo.FindAll()
if err != nil {
return nil, err
}
return toUser(users), nil
}
func (u *userUsecase) RegisterUser(email string) error {
uid, err := uuid.NewRandom()
if err != nil {
return err
}
if err := u.service.Duplicated(email); err != nil {
return err
}
user := model.NewUser(uid.String(), email)
if err := u.repo.Save(user); err != nil {
return err
}
return nil
}

userUsercase depend two packages repository.UserRepository interface and *service.UserService struct. These two packages are must injected when use case initialize by use case user. Those dependencies are solved by DI container in normally, this will be wrote later in this entry.

ListUser use case fetch all registered users and RegisterUser use case register a user to the system if it is not registered same email address.

One point, the User is not model.User. model.User may has many business knowledges, but other layers should not better to know about that. So I defined DAO for use case users due to encapsulate the knowledges.

type User struct {
ID string
Email string
}
func toUser(users []*model.User) []*User {
res := make([]*User, len(users))
for i, user := range users {
res[i] = &User{
ID: user.GetID(),
Email: user.GetEmail(),
}
}
return res
}

...

So, why do you think about this service is used as concrete implementation instead of using interface? This is because that this service depends no other layers. Conversely, the repository through layers and the implementation depends detail of devices that should not known from other layer, thus that was defined an interface. I think this is a most important thing in this architecture.

Interface — the green layer

This layer is placed the concrete object like handler of the API endpoint, repository of the RDB or other boundaries for interfaces. In this case, I added two concrete objects that memory storage accessor and gRPC service.

Memory storage accessor

I added concrete user repository as memory storage accessor.

type userRepository struct {
mu *sync.Mutex
users map[string]*User
}
func NewUserRepository() *userRepository {
return &userRepository{
mu: &sync.Mutex{},
users: map[string]*User{},
}
}
func (r *userRepository) FindAll() ([]*model.User, error) {
r.mu.Lock()
defer r.mu.Unlock()
    users := make([]*model.User, len(r.users))
i := 0
for _, user := range r.users {
users[i] = model.NewUser(user.ID, user.Email)
i++
}
return users, nil
}
func (r *userRepository) FindByEmail(email string) (*model.User, error) {
r.mu.Lock()
defer r.mu.Unlock()
    for _, user := range r.users {
if user.Email == email {
return model.NewUser(user.ID, user.Email), nil
}
}
return nil, nil
}
func (r *userRepository) Save(user *model.User) error {
r.mu.Lock()
defer r.mu.Unlock()
    r.users[user.GetID()] = &User{
ID: user.GetID(),
Email: user.GetEmail(),
}
return nil
}

This is concrete implementation of repository. We’ll need to another implementation if we need to persist the user to the RDB or other. But even in such case, we don’t need to change the model layer. The model layer is depending for only repository interface, and not interest for this implementation detail. This is amazing.

This User is defined for only in this package. This also for solving about decapsulating of knowledge through between the layers.

type User struct {
ID string
Email string
}

gRPC service

I think gRPC service is also included the interface layer.
These are defined app/interface/rpc directory like below:

% tree
.
├── rpc.go
└── v1.0
├── protocol
│ ├── user_service.pb.go
│ └── user_service.proto
├── user_service.go
└── v1.go

protocol directory contains protocol buffers DSL file(user_service.proto) and generated RPC service code(user_service.pb.go).

user_service.go is the wrapper of gRPC endpoint handler:

type userService struct {
userUsecase usecase.UserUsecase
}
func NewUserService(userUsecase usecase.UserUsecase) *userService {
return &userService{
userUsecase: userUsecase,
}
}
func (s *userService) ListUser(ctx context.Context, in *protocol.ListUserRequestType) (*protocol.ListUserResponseType, error) {
users, err := s.userUsecase.ListUser()
if err != nil {
return nil, err
}
    res := &protocol.ListUserResponseType{
Users: toUser(users),
}
    return res, nil
}
func (s *userService) RegisterUser(ctx context.Context, in *protocol.RegisterUserRequestType) (*protocol.RegisterUserResponseType, error) {
if err := s.userUsecase.RegisterUser(in.GetEmail()); err != nil {
return &protocol.RegisterUserResponseType{}, err
}
return &protocol.RegisterUserResponseType{}, nil
}
func toUser(users []*usecase.User) []*protocol.User {
res := make([]*protocol.User, len(users))
for i, user := range users {
res[i] = &protocol.User{
Id: user.ID,
Email: user.Email,
}
}
return res
}

userService depends for only use case interface.
If you want to use use case from another layer (e.g, CUI), you can implement in this interface layer as you like.

v1.go is resolver of object dependencies using DI container:

func Apply(server *grpc.Server, ctn *registry.Container) {
protocol.RegisterUserServiceServer(server, NewUserService(ctn.Resolve("user-usecase").(usecase.UserUsecase)))
}

v1.go apply package that was retrieved from *registry.Container to gRPC service.

At the last, let’s take a look about DI container implementation.

registry

The registry is DI container that resolve dependency of object.
I have been used github.com/sarulabs/di as DI container.

github.com/surulabs/di can be used easily:

type Container struct {
ctn di.Container
}
func NewContainer() (*Container, error) {
builder, err := di.NewBuilder()
if err != nil {
return nil, err
}
    if err := builder.Add([]di.Def{
{
Name: "user-usecase",
Build: buildUserUsecase,
},
}...); err != nil {
return nil, err
}
    return &Container{
ctn: builder.Build(),
}, nil
}
func (c *Container) Resolve(name string) interface{} {
return c.ctn.Get(name)
}
func (c *Container) Clean() error {
return c.ctn.Clean()
}
func buildUserUsecase(ctn di.Container) (interface{}, error) {
repo := memory.NewUserRepository()
service := service.NewUserService(repo)
return usecase.NewUserUsecase(repo, service), nil
}

For example in above, I associate user-usecase string with concrete use case implementation by using buildUserUsecase function. Thus we can replace any concrete implementation of use case in one place registry.


...

Thank you for reading this entry. Feedback is welcome. You have any ideas and improvements feel free to respond me!