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:
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:
The 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)
}
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)
}
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)
}
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)
}
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
}
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)
}
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)
})
}
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)
})
}
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)
})
}
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
- What is System Design?
- Twitter Timeline Design with Fanout Approach - System Design
- HTTP Request vs HTTP Long-Polling vs WebSocket vs Server-Sent Events
- YouTube Video Upload Service - System Design
- What is Consistent Hashing?
- Capacity Estimation: Back-of-the-envelope calculation - Twitter
That's it for now.
Thanks
Amit Shekhar
Co-Founder @ Outcome School
You can connect with me on:
Top comments (0)