DEV Community

Cover image for Go Backend Clean Architecture
Amit Shekhar
Amit Shekhar

Posted on • Edited on • Originally published at outcomeschool.com

Go Backend Clean Architecture

In this blog, we are going to discuss the implementation of clean architecture in the backend project written in the Go language.

Link to the project: Go Backend Clean Architecture.

Before creating this project, I have gone through more than 20 projects related to the Go(Golang) Clean Architecture on GitHub.

Thanks to all those projects, I learned a lot from all of those. As I keep saying:

The best way to learn to code is to code. But, to write good code, you will also have to read good code. Make a habit of reading good code. You can find many open-source projects on GitHub and start reading.

Then for the implementation part, I combined all of my ideas, experiences, and learnings from those projects to create this project.

And as always I would love to get feedback on my project. This helps everyone and most importantly me.

This article was originally published at Outcome School.

As I have implemented the Clean Architecture, the layers created in the project are as follows:

  • Router
  • Controller
  • Usecase
  • Repository
  • Domain

Here is the complete architecture of the backend project written in Go language:

Go Backend Clean Architecture Diagram

Why do we consider clean architecture in a project?

The clean architecture provides an array of benefits to software applications.

The benefits of clean architecture implementation are as follows:

  • Framework independent: Easier to replace a package with another package if necessary, since everything is decoupled. For example, we could change the database package that we use or add another one if we need it.
  • Highly Testable: Easier to write tests. I have written the test for the usecase, repository, and controller layers.
  • The addition of a new feature becomes easy.
  • Easy to modify the code for any required changes.

As this project follows the clean architecture principle, you can replace them very easily with packages that fit best your requirement. However, the major packages that I have used are as follows:

  • gin: Gin is an HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need a smashing performance, get yourself some Gin.
  • mongo go driver: The Official Golang driver for MongoDB.
  • jwt: JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties. Used for Access Token and Refresh Token.
  • viper: For loading configuration from the .env file. Go configuration with fangs. Find, load, and unmarshal a configuration file in JSON, TOML, YAML, HCL, INI, envfile, or Java properties formats.
  • bcrypt: Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing algorithm.
  • testify: A toolkit with common assertions and mocks that plays nicely with the standard library.
  • mockery: A mock code autogenerator for Golang used in testing.
  • Check more packages in go.mod.

Now, let's discuss all the layers used in the project starting with the Router.

Router

First of all, the request comes to the Router.

Further, this router gets divided into two routers as follows:

  • Public Router: All the public APIs should go through this router.
  • Protected Router: All the private APIs should go through this router.

The Public API request flow:

Public API Request Flow

The Private API request flow:

Private API Request Flow

JWT Authentication Middleware for Access Token Validation.

In between both routers, a middleware gets added to check the validity of the access token. So, the private request with the invalid access token should not reach the protected router at all.

Then, it gets distributed to the corresponding router. You can see the below code for an understanding:

package route

import (
    "time"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/api/middleware"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/bootstrap"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
    "github.com/gin-gonic/gin"
)

func Setup(env *bootstrap.Env, timeout time.Duration, db mongo.Database, gin *gin.Engine) {
    publicRouter := gin.Group("")
    // All Public APIs
    NewSignupRouter(env, timeout, db, publicRouter)
    NewLoginRouter(env, timeout, db, publicRouter)
    NewRefreshTokenRouter(env, timeout, db, publicRouter)

    protectedRouter := gin.Group("")
    // Middleware to verify AccessToken
    protectedRouter.Use(middleware.JwtAuthMiddleware(env.AccessTokenSecret))
    // All Private APIs
    NewProfileRouter(env, timeout, db, protectedRouter)
    NewTaskRouter(env, timeout, db, protectedRouter)
}
Enter fullscreen mode Exit fullscreen mode

Then, the router will call its corresponding controller.

In order to call the controller, we need the usecase, as the controller is dependent on the usecase.

And we also need a repository as the usecase is dependent on the repository.

Now, we have the repository and we pass it to the usecase.

After that, we have the usecase, we pass it to the controller.

Finally, our controller is ready to use inside the router.

Example code:

package route

import (
    "time"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/bootstrap"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/repository"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/usecase"
    "github.com/gin-gonic/gin"
)

func NewTaskRouter(env *bootstrap.Env, timeout time.Duration, db mongo.Database, group *gin.RouterGroup) {
    tr := repository.NewTaskRepository(db, domain.CollectionTask)
    tc := &controller.TaskController{
        TaskUsecase: usecase.NewTaskUsecase(tr, timeout),
    }
    group.GET("/task", tc.Fetch)
    group.POST("/task", tc.Create)
}
Enter fullscreen mode Exit fullscreen mode

Each request to the backend is eventually executed by a controller. A list of routes is defined which maps a given request to a controller and an action.

Controller

So now, the request is with the controller. First, it will validate the data present inside the request. If anything is invalid, it returns a "400 Bad Request" as the error response.

If everything is valid inside the request, it will call the usecase layer to perform an operation.

Example code:

package controller

import (
    "net/http"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/gin-gonic/gin"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

type TaskController struct {
    TaskUsecase domain.TaskUsecase
}

func (tc *TaskController) Create(c *gin.Context) {
    var task domain.Task

    err := c.ShouldBind(&task)
    if err != nil {
        c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()})
        return
    }

    userID := c.GetString("x-user-id")
    task.ID = primitive.NewObjectID()

    task.UserID, err = primitive.ObjectIDFromHex(userID)
    if err != nil {
        c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()})
        return
    }

    err = tc.TaskUsecase.Create(c, &task)
    if err != nil {
        c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()})
        return
    }

    c.JSON(http.StatusOK, domain.SuccessResponse{
        Message: "Task created successfully",
    })
}

func (u *TaskController) Fetch(c *gin.Context) {
    userID := c.GetString("x-user-id")

    tasks, err := u.TaskUsecase.FetchByUserID(c, userID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()})
        return
    }

    c.JSON(http.StatusOK, tasks)
}
Enter fullscreen mode Exit fullscreen mode

Usecase

The usecase layer is dependent on the repository layer.

This layer uses the repository layer to perform an operation. It is completely up to the repository how it is going to perform an operation.

Example code:

package usecase

import (
    "context"
    "time"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
)

type taskUsecase struct {
    taskRepository domain.TaskRepository
    contextTimeout time.Duration
}

func NewTaskUsecase(taskRepository domain.TaskRepository, timeout time.Duration) domain.TaskUsecase {
    return &taskUsecase{
        taskRepository: taskRepository,
        contextTimeout: timeout,
    }
}

func (tu *taskUsecase) Create(c context.Context, task *domain.Task) error {
    ctx, cancel := context.WithTimeout(c, tu.contextTimeout)
    defer cancel()
    return tu.taskRepository.Create(ctx, task)
}

func (tu *taskUsecase) FetchByUserID(c context.Context, userID string) ([]domain.Task, error) {
    ctx, cancel := context.WithTimeout(c, tu.contextTimeout)
    defer cancel()
    return tu.taskRepository.FetchByUserID(ctx, userID)
}
Enter fullscreen mode Exit fullscreen mode

Repository

The repository is the dependency of the usecase. The Usecase layer asks the repository to perform an operation.

The repository layer is free to choose any database, in fact, it can call any other independent services based on the requirement.

In the project, the repository layer makes the database query for performing operations asked by the Usecase layer.

Example code:

package repository

import (
    "context"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

type taskRepository struct {
    database   mongo.Database
    collection string
}

func NewTaskRepository(db mongo.Database, collection string) domain.TaskRepository {
    return &taskRepository{
        database:   db,
        collection: collection,
    }
}

func (tr *taskRepository) Create(c context.Context, task *domain.Task) error {
    collection := tr.database.Collection(tr.collection)

    _, err := collection.InsertOne(c, task)

    return err
}

func (tr *taskRepository) FetchByUserID(c context.Context, userID string) ([]domain.Task, error) {
    collection := tr.database.Collection(tr.collection)

    var tasks []domain.Task

    idHex, err := primitive.ObjectIDFromHex(userID)
    if err != nil {
        return tasks, err
    }

    cursor, err := collection.Find(c, bson.M{"userID": idHex})
    if err != nil {
        return nil, err
    }

    err = cursor.All(c, &tasks)
    if tasks == nil {
        return []domain.Task{}, err
    }

    return tasks, err
}
Enter fullscreen mode Exit fullscreen mode

Domain

In the domain layer, we put the following:

  • Models for request, and response.
  • Entities for the database.
  • Interfaces for usecases, and repositories.

Example code:

package domain

import (
    "context"

    "go.mongodb.org/mongo-driver/bson/primitive"
)

const (
    CollectionTask = "tasks"
)

type Task struct {
    ID     primitive.ObjectID `bson:"_id" json:"-"`
    Title  string             `bson:"title" form:"title" binding:"required" json:"title"`
    UserID primitive.ObjectID `bson:"userID" json:"-"`
}

type TaskRepository interface {
    Create(c context.Context, task *Task) error
    FetchByUserID(c context.Context, userID string) ([]Task, error)
}

type TaskUsecase interface {
    Create(c context.Context, task *Task) error
    FetchByUserID(c context.Context, userID string) ([]Task, error)
}
Enter fullscreen mode Exit fullscreen mode

Domain, model, and entity get used in the controller, usecase, and repository.

So, now that we have discussed all the layers created in this clean architecture backend project.

It's time to know about the testing, I have included tests for the controller, usecase, and repository layers.

I have used the mockery package to generate the mock code for the database, repository, and usecase. You can find the steps to generate the mock code in the README of the project itself.

Repository Test

As in the project, the repository layer uses the database, I have mocked the database, and tested the repository as below:

package repository_test

import (
    "context"
    "errors"
    "testing"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo/mocks"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/repository"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

func TestCreate(t *testing.T) {

    var databaseHelper *mocks.Database
    var collectionHelper *mocks.Collection

    databaseHelper = &mocks.Database{}
    collectionHelper = &mocks.Collection{}

    collectionName := domain.CollectionUser

    mockUser := &domain.User{
        ID:       primitive.NewObjectID(),
        Name:     "Test",
        Email:    "test@gmail.com",
        Password: "password",
    }

    mockEmptyUser := &domain.User{}
    mockUserID := primitive.NewObjectID()

    t.Run("success", func(t *testing.T) {

        collectionHelper.On("InsertOne", mock.Anything, mock.AnythingOfType("*domain.User")).Return(mockUserID, nil).Once()

        databaseHelper.On("Collection", collectionName).Return(collectionHelper)

        ur := repository.NewUserRepository(databaseHelper, collectionName)

        err := ur.Create(context.Background(), mockUser)

        assert.NoError(t, err)

        collectionHelper.AssertExpectations(t)
    })

    t.Run("error", func(t *testing.T) {
        collectionHelper.On("InsertOne", mock.Anything, mock.AnythingOfType("*domain.User")).Return(mockEmptyUser, errors.New("Unexpected")).Once()

        databaseHelper.On("Collection", collectionName).Return(collectionHelper)

        ur := repository.NewUserRepository(databaseHelper, collectionName)

        err := ur.Create(context.Background(), mockEmptyUser)

        assert.Error(t, err)

        collectionHelper.AssertExpectations(t)
    })

}
Enter fullscreen mode Exit fullscreen mode

Usecase Test

The usecase is dependent on the repository, I have mocked the repository, and tested the usecase as below:

package usecase_test

import (
    "context"
    "errors"
    "testing"
    "time"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/mocks"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/usecase"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

func TestFetchByUserID(t *testing.T) {
    mockTaskRepository := new(mocks.TaskRepository)
    userObjectID := primitive.NewObjectID()
    userID := userObjectID.Hex()

    t.Run("success", func(t *testing.T) {

        mockTask := domain.Task{
            ID:     primitive.NewObjectID(),
            Title:  "Test Title",
            UserID: userObjectID,
        }

        mockListTask := make([]domain.Task, 0)
        mockListTask = append(mockListTask, mockTask)

        mockTaskRepository.On("FetchByUserID", mock.Anything, userID).Return(mockListTask, nil).Once()

        u := usecase.NewTaskUsecase(mockTaskRepository, time.Second*2)

        list, err := u.FetchByUserID(context.Background(), userID)

        assert.NoError(t, err)
        assert.NotNil(t, list)
        assert.Len(t, list, len(mockListTask))

        mockTaskRepository.AssertExpectations(t)
    })

    t.Run("error", func(t *testing.T) {
        mockTaskRepository.On("FetchByUserID", mock.Anything, userID).Return(nil, errors.New("Unexpected")).Once()

        u := usecase.NewTaskUsecase(mockTaskRepository, time.Second*2)

        list, err := u.FetchByUserID(context.Background(), userID)

        assert.Error(t, err)
        assert.Nil(t, list)

        mockTaskRepository.AssertExpectations(t)
    })

}
Enter fullscreen mode Exit fullscreen mode

Controller Test

The controller is dependent on the usecase, I have mocked usecase, and tested the controller as below:

package controller_test

import (
    "encoding/json"
    "errors"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/mocks"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

func setUserID(userID string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("x-user-id", userID)
        c.Next()
    }
}

func TestFetch(t *testing.T) {

    t.Run("success", func(t *testing.T) {
        mockProfile := &domain.Profile{
            Name:  "Test Name",
            Email: "test@gmail.com",
        }

        userObjectID := primitive.NewObjectID()
        userID := userObjectID.Hex()

        mockProfileUsecase := new(mocks.ProfileUsecase)

        mockProfileUsecase.On("GetProfileByID", mock.Anything, userID).Return(mockProfile, nil)

        gin := gin.Default()

        rec := httptest.NewRecorder()

        pc := &controller.ProfileController{
            ProfileUsecase: mockProfileUsecase,
        }

        gin.Use(setUserID(userID))
        gin.GET("/profile", pc.Fetch)

        body, err := json.Marshal(mockProfile)
        assert.NoError(t, err)

        bodyString := string(body)

        req := httptest.NewRequest(http.MethodGet, "/profile", nil)
        gin.ServeHTTP(rec, req)

        assert.Equal(t, http.StatusOK, rec.Code)

        assert.Equal(t, bodyString, rec.Body.String())

        mockProfileUsecase.AssertExpectations(t)
    })

    t.Run("error", func(t *testing.T) {
        userObjectID := primitive.NewObjectID()
        userID := userObjectID.Hex()

        mockProfileUsecase := new(mocks.ProfileUsecase)

        customErr := errors.New("Unexpected")

        mockProfileUsecase.On("GetProfileByID", mock.Anything, userID).Return(nil, customErr)

        gin := gin.Default()

        rec := httptest.NewRecorder()

        pc := &controller.ProfileController{
            ProfileUsecase: mockProfileUsecase,
        }

        gin.Use(setUserID(userID))
        gin.GET("/profile", pc.Fetch)

        body, err := json.Marshal(domain.ErrorResponse{Message: customErr.Error()})
        assert.NoError(t, err)

        bodyString := string(body)

        req := httptest.NewRequest(http.MethodGet, "/profile", nil)
        gin.ServeHTTP(rec, req)

        assert.Equal(t, http.StatusInternalServerError, rec.Code)

        assert.Equal(t, bodyString, rec.Body.String())

        mockProfileUsecase.AssertExpectations(t)
    })

}
Enter fullscreen mode Exit fullscreen mode

This is how we can implement the clean architecture in the backend project in the Go language.

You can go through the project, I have included a step-by-step guide to run this project in the README of the project itself.

Link to the project: Go Backend Clean Architecture.

Thanks for reading. I would love to hear your feedback.

System Design Playlist on YouTube

That's it for now.

Thanks

Amit Shekhar

Co-Founder @ Outcome School

You can connect with me on:

Read all of our blogs here.

Top comments (0)