Clean Architecture in Go

What I want to tell you

The Structure

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

Entities— the yellow layer

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
}
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
}
func (u *User) Duplicated(email string) bool {
// Find user by email from persistence layer...
}
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
}

Use Cases — the red layer

type UserUsecase interface {
ListUser() ([]*User, error)
RegisterUser(email string) error
}
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
}
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
}

Interface — the green layer

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
}
type User struct {
ID string
Email string
}
% tree
.
├── rpc.go
└── v1.0
├── protocol
│ ├── user_service.pb.go
│ └── user_service.proto
├── user_service.go
└── v1.go
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
}
func Apply(server *grpc.Server, ctn *registry.Container) {
protocol.RegisterUserServiceServer(server, NewUserService(ctn.Resolve("user-usecase").(usecase.UserUsecase)))
}
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
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store