Implementing Clean Architecture in GO

Because why not? ๐Ÿ˜‰

ยท

5 min read

Implementing Clean Architecture in GO

Hello Hi! ๐Ÿ‘‹

I'm back after my exams ๐Ÿฅต and ready to continue my back-end developer journey ๐Ÿซก๐ŸŽฏ

Introduction

In my previous blogs, we saw how we can build a web server using Go and Gin. We also saw the theoretical concepts of Clean Architecture. So how about we apply the architecture to our server now?

Sounds interesting? Let's go!

The steps we will follow to construct the layers will be as follows:

  • Construct the entities layer

  • Construct the UseCase layer by defining an interface, followed by its concrete implementation

  • Construct the database interface, and its concrete implementation

  • Set up the router to handle requests

  • Connect the different layers in the main.go file

Setting Up the Layers

Entities

In the server we made in my first blog, we had only one entity: Person

Hence, our entity layer would have a single struct (I'm replacing person with User and people with Users).

The entities/user.go file will contain:

package entities

// data definitions
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

(The final file/folder structure, and the GitHub link for the complete project, are at the end of the blog for reference)

Usecase

Steps for constructing this layer:

  • Make a UserUseCase interface

  • Add the function skeletons for user actions within the interface

  • Build the concrete implementation within usecases/user/user.go

// usecases/usecases.go
// this contains all the interfaces for all usecasses
// (currently, only UserUseCase)
package usecases

import "web-server/entities"

type UserUseCase interface {
    GetAll() ([]*entities.User, error)
    GetByID(id string) (*entities.User, error)
    CreateNew(user *entities.User) error
}
// usecases/user/user.go
package usecases

import (
    "web-server/database"
    "web-server/entities"
)

// userUseCase struct contains a UserDB instance
type userUseCase struct {
    DB database.UserDB
}

// return UseCase object to be passed on to the Router
func NewUserUseCase(userRepo database.UserDB) *userUseCase {
    return &userUseCase{
        DB: userRepo,
    }
}

func (uc *userUseCase) GetAll() ([]*entities.User, error) {
    return uc.DB.GetAll()
}

func (uc *userUseCase) GetByID(id string) (*entities.User, error) {
    return uc.DB.GetByID(id)
}

func (uc *userUseCase) CreateNew(user *entities.User) error {
    return uc.DB.CreateNew(user)
}

Database

First, we will make a UserDB interface and write the skeletons for all the functions we need.

// database/database.go
package database

import "web-server/entities"

type UserDB interface {
    GetAll() ([]*entities.User, error)
    GetByID(id string) (*entities.User, error)
    CreateNew(user *entities.User) error
}

Now for concrete implementations:

// database/users/users.go
package database

import "web-server/entities"

type UserDB struct {
    users []*entities.User
}

func NewUserDB() *UserDB {
    return &UserDB{
        users: []*entities.User{
            {
                ID:   "1",
                Name: "ABC",
            },
            {
                ID:   "2",
                Name: "DEF",
            },
            {
                ID:   "3",
                Name: "GHI",
            },
        },
    }
}

func (repo *UserDB) GetAll() ([]*entities.User, error) {
    return repo.users, nil
}

func (repo *UserDB) GetByID(id string) (*entities.User, error) {
    for _, user := range repo.users {
        if user.ID == id {
            return user, nil
        }
    }
    return nil, nil // or an error indicating user not found
}

func (repo *UserDB) CreateNew(user *entities.User) error {
    repo.users = append(repo.users, user)
    return nil
}

Router Setup

Now, we will make the user router, which will handle all the requests coming to /users

// routes/users/users.go
package routes

import (
    "net/http"
    "web-server/entities"
    "web-server/usecases"

    "github.com/gin-gonic/gin"
)

func SetupRouter(userUC usecases.UserUseCase) *gin.Engine {
    router := gin.Default()

    // GET All Users
    router.GET("/users", func(c *gin.Context) {
        users, err := userUC.GetAll()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users"})
            return
        }
        c.IndentedJSON(http.StatusOK, users)
    })

    // GET User by ID
    router.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        user, err := userUC.GetByID(id)
        if err != nil {
            c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
            return
        }
        if user == nil {
            c.IndentedJSON(http.StatusNotFound, gin.H{"error": "User not found"})
            return
        }
        c.IndentedJSON(http.StatusOK, user)
    })

    // POST Create New User
    router.POST("/users", func(c *gin.Context) {
        var user entities.User
        if err := c.ShouldBindJSON(&user); err != nil {
            c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid user data"})
            return
        }
        if err := userUC.CreateNew(&user); err != nil {
            c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
            return
        }
        c.Status(http.StatusCreated)
    })

    return router
}

Assembling the Pieces

Finally, we will make the main.go file to string together all the different layers.

package main

import (
    userDatabase "web-server/database/users"
    userRouter "web-server/routes/users"
    userUseCase "web-server/usecases/user"
)

func main() {
    // Initialize repository
    userDB := userDatabase.NewUserDB()

    // Initialize use case
    userUC := userUseCase.NewUserUseCase(userDB)

    // Set up router
    router := userRouter.SetupRouter(userUC)

    // Run the server
    router.Run(":8080")
}

Here, we can see we have "injected" the dependencies when calling the constructor so that if we swap out the DB layer to let's say a Postgres instance, then we can easily do so within just the database layer (or ./database) and the new DB implementation will then be passed on to the use case layer, and since the enforced interface is still valid, no changes should be required (if the architecture implementation is perfect ๐Ÿ˜…)

In the current state, some of the steps might seem redundant, but the goal was to attempt at replicating how we would make large scale projects.

File Structure

The overall file structure of the project is:

web-server
โ”œโ”€โ”€ database
โ”‚   โ”œโ”€โ”€ users
โ”‚   โ”‚   โ””โ”€โ”€ users.go
โ”‚   โ””โ”€โ”€ database.go
โ”œโ”€โ”€ entities
โ”‚   โ””โ”€โ”€ user.go
โ”œโ”€โ”€ routes
โ”‚   โ””โ”€โ”€ users
โ”‚       โ””โ”€โ”€ users.go
โ”œโ”€โ”€ usecases
โ”‚   โ”œโ”€โ”€ user
โ”‚   โ”‚   โ””โ”€โ”€ user.go
โ”‚   โ””โ”€โ”€ usecases.go
โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ go.sum
โ””โ”€โ”€ main.go

8 directories, 9 files

Afterword

The End! ๐Ÿ˜…

I was planning on connecting PostgreSQL to the server instead of an in-memory DB within the same blog but then decided to split it into two.

So stay tuned! ๐Ÿ˜„

References

The full code with a few tweaks, some more functions, etc. is uploaded on my GitHub:

The previous blogs mentioned:

ย