In my early days of Go development, I fell into the common trap of putting everything in a main package or a giant utils folder. As soon as I needed to switch from a MySQL database to PostgreSQL, or from a REST API to gRPC, I found myself rewriting nearly 60% of my codebase. That’s when I discovered Clean Architecture.

This golang clean architecture tutorial is designed to help you avoid that pain. Clean Architecture isn’t about following a strict set of rules for the sake of it; it’s about dependency inversion. The goal is to ensure your core business logic doesn’t know or care whether you’re using Gin, Fiber, Gorm, or a raw SQL driver.

Prerequisites

Before we dive into the code, make sure you have the following installed and configured:

Step-by-Step Implementation

Step 1: Defining the Domain (Entities)

The center of the onion is the Domain layer. This contains your business objects. These are plain Go structs that have no dependencies on any other layer.

package domain

type User struct {
	ID    int64  `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

Step 2: Creating the Repository Interface

Still within the domain or a dedicated ‘repository’ package, we define how we want to interact with data, without specifying where that data comes from. This is key to the golang testing best practices I always advocate for, as it allows for easy mocking.

package domain

import "context"

type UserRepository interface {
	GetByID(ctx context.Context, id int64) (*User, error)
	Create(ctx context.Context, user *User) error
}

Step 3: The Use Case Layer (Business Logic)

The Use Case layer coordinates the flow of data. It depends on the interface we just created, not a concrete database implementation.

package usecase

import (
	"context"
	"myproject/domain"
)

type UserUseCase struct {
	repo domain.UserRepository
}

func NewUserUseCase(r domain.UserRepository) *UserUseCase {
	return &UserUseCase{repo: r}
}

func (uc *UserUseCase) GetUser(ctx context.Context, id int64) (*domain.User, error) {
	return uc.repo.GetByID(ctx, id)
}

As shown in the architecture diagram above, the UseCase layer only points inward toward the Domain. It has no idea if the repository is using MongoDB or a CSV file.

Step 4: The Infrastructure Layer (Implementation)

Now we implement the repository. This is where we use Gorm, sqlx, or any other driver. If we decide to change databases later, this is the only place we change code.

package repository

import (
	"context"
	"database/sql"
	"myproject/domain"
)

type mysqlUserRepository struct {
	db *sql.DB
}

func NewMySQLUserRepository(db *sql.DB) domain.UserRepository {
	return &mysqlUserRepository{db: db}
}

func (r *mysqlUserRepository) GetByID(ctx context.Context, id int64) (*domain.User, error) {
	// Actual SQL logic here
	return &domain.User{ID: id, Name: "John Doe"}, nil
}
Visual representation of dependency injection flow in Go Clean Architecture
Visual representation of dependency injection flow in Go Clean Architecture

Step 5: The Delivery Layer (HTTP/Handlers)

Finally, we create the entry point. I usually use the Gin framework here. The handler calls the use case, and the use case calls the repository.

package delivery

import (
	"net/http"
	"strconv"
	"github.com/gin-gonic/gin"
	"myproject/usecase"
)

type UserHandler struct {
	useCase *usecase.UserUseCase
}

func (h *UserHandler) GetUser(c *gin.Context) {
	id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
	user, err := h.useCase.GetUser(c.Request.Context(), id)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, user)
}

Pro Tips for Go Architecture

Troubleshooting Common Issues

Issue: “Import cycle not allowed”
This happens when you try to reference a handler inside a usecase. Remember: Handler → UseCase → Repository. Never the other way around.

Issue: Interface Bloat
If your UserRepository interface has 50 methods, it’s too big. Break it down into smaller, focused interfaces (e.g., UserReader and UserWriter). This follows the Interface Segregation Principle.

What’s Next?

Now that you’ve mastered the layout, the next step is ensuring your logic is bulletproof. I highly recommend reading up on golang testing best practices to learn how to mock those repository interfaces using tools like mockgen.

Ready to scale your productivity? Check out my other guides on organizing large Go projects to ensure your team can collaborate without stepping on each other’s toes.