Clean Architecture in Go Using go-clean-arch
James Reed
Infrastructure Engineer · Leapcell

What is the code architecture of your Go project? Hexagonal architecture? Onion architecture? Or perhaps DDD? No matter which architecture your project adopts, the core goal should always be the same: making the code easy to understand, test, and maintain.
This article will start from Uncle Bob’s Clean Architecture, briefly analyze its core ideas, and, combined with the go-clean-arch repository, dive deep into how to implement these architectural concepts in a Go project.
Clean Architecture
Clean Architecture is a software architecture design philosophy proposed by Uncle Bob. Its goal is to make software systems easier to understand, test, and maintain through a layered structure and clear dependency rules. Its core idea is to separate concerns and ensure that the core business logic (Use Cases) in the system does not depend on implementation details (such as frameworks, databases, etc.).
The core idea of Clean Architecture is independence:
- Independent of frameworks: It does not rely on specific frameworks (such as Gin, GRPC, etc.). Frameworks should be treated as tools, not the core of the architecture.
- Independent of UI: The user interface can be easily changed without affecting other parts of the system. For example, a web UI can be replaced by a console UI without modifying business rules.
- Independent of databases: The database can be switched (e.g., from MySQL to MongoDB) without impacting the core business logic.
- Independent of external tools: External dependencies (such as third-party libraries) should be isolated to avoid directly affecting the system core.
Structure Diagram
As shown in the diagram, Clean Architecture is described as a set of concentric circles, with each layer representing different system responsibilities:
-
Core Entities
- Location: The innermost layer
- Responsibility: Defines the business rules of the system. Entities are the core objects in the application, with an independent lifecycle.
- Independence: Completely independent of business rules, changing only as business rules change.
-
Use Cases / Services
- Location: The layer right next to Entities
- Responsibility: Implements the business logic of the application. Defines the flow of various operations (use cases) in the system, ensuring user requirements are met.
- Role: The use case layer calls the entity layer, coordinates data flow, and determines responses.
-
Interface Adapters
- Location: The next outer layer
- Responsibility: Responsible for converting data from external systems (such as UI, database, etc.) into a format understandable by the inner layers, and also for converting core business logic into a form usable by external systems.
- Examples: Converting HTTP request data into internal models (such as classes or structs), or presenting use case output data to users.
- Components: Includes controllers, gateways, presenters, etc.
-
Frameworks & Drivers
- Location: The outermost layer
- Responsibility: Implements interaction with the external world, such as databases, UI, message queues, etc.
- Feature: This layer depends on the inner layers, but not vice versa. This is the part of the system that is easiest to swap out.
go-clean-arch Project
go-clean-arch is a sample Go project that implements Clean Architecture. The project is divided into four domain layers:
Models Layer
Purpose: Defines the core data structures of the domain, describing the business entities in the project, such as Article and Author.
Corresponding Theoretical Layer: Entities layer.
Example:
package domain import ( "time" ) type Article struct { ID int64 `json:"id"` Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` Author Author `json:"author"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }
Repository Layer
Purpose: Responsible for interacting with data sources (such as databases and caches) and providing a unified interface for the use case layer to access data.
Corresponding Theoretical Layer: Frameworks & Drivers.
Example:
package mysql import ( "context" "database/sql" "fmt" "github.com/sirupsen/logrus" "github.com/bxcodec/go-clean-arch/domain" "github.com/bxcodec/go-clean-arch/internal/repository" ) type ArticleRepository struct { Conn *sql.DB } // NewArticleRepository will create an object that represents the article.Repository interface func NewArticleRepository(conn *sql.DB) *ArticleRepository { return &ArticleRepository{conn} } func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) { rows, err := m.Conn.QueryContext(ctx, query, args...) if err != nil { logrus.Error(err) return nil, err } defer func() { errRow := rows.Close() if errRow != nil { logrus.Error(errRow) } }() result = make([]domain.Article, 0) for rows.Next() { t := domain.Article{} authorID := int64(0) err = rows.Scan( &t.ID, &t.Title, &t.Content, &authorID, &t.UpdatedAt, &t.CreatedAt, ) if err != nil { logrus.Error(err) return nil, err } t.Author = domain.Author{ ID: authorID, } result = append(result, t) } return result, nil } func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = ?` list, err := m.fetch(ctx, query, id) if err != nil { return domain.Article{}, err } if len(list) > 0 { res = list[0] } else { return res, domain.ErrNotFound } return }
Usecase/Service Layer
Purpose: Defines the core application logic of the system and serves as a bridge between domain models and external interactions.
Corresponding Theoretical Layer: Use Cases / Service.
Example:
package article import ( "context" "time" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" "github.com/bxcodec/go-clean-arch/domain" ) type ArticleRepository interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } type AuthorRepository interface { GetByID(ctx context.Context, id int64) (domain.Author, error) } type Service struct { articleRepo ArticleRepository authorRepo AuthorRepository } func NewService(a ArticleRepository, ar AuthorRepository) *Service { return &Service{ articleRepo: a, authorRepo: ar, } } func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { res, err = a.articleRepo.GetByID(ctx, id) if err != nil { return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { return domain.Article{}, err } res.Author = resAuthor return }
Delivery Layer
Purpose: Responsible for receiving external requests, calling the use case layer, and returning results to the outside (such as HTTP clients or CLI users).
Corresponding Theoretical Layer: Interface Adapters.
Example:
package rest import ( "context" "net/http" "strconv" "github.com/bxcodec/go-clean-arch/domain" ) type ResponseError struct { Message string `json:"message"` } type ArticleService interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } // ArticleHandler represents the HTTP handler for articles type ArticleHandler struct { Service ArticleService } func NewArticleHandler(e *echo.Echo, svc ArticleService) { handler := &ArticleHandler{ Service: svc, } e.GET("/articles/:id", handler.GetByID) } func (a *ArticleHandler) GetByID(c echo.Context) error { idP, err := strconv.Atoi(c.Param("id")) if err != nil { return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error()) } id := int64(idP) ctx := c.Request().Context() art, err := a.Service.GetByID(ctx, id) if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } return c.JSON(http.StatusOK, art) }
The basic code architecture of the go-clean-arch project is as follows:
go-clean-arch/
├── internal/
│ ├── rest/
│ │ └── article.go # Delivery Layer
│ ├── repository/
│ │ ├── mysql/
│ │ │ └── article.go # Repository Layer
├── article/
│ └── service.go # Usecase/Service Layer
├── domain/
│ └── article.go # Models Layer
In the go-clean-arch project, the dependencies between each layer are as follows:
- The Usecase/Service layer depends on the Repository interface but is unaware of the implementation details of the interface.
- The Repository layer implements the interface, but it is an outer component that depends on the entities in the Domain layer.
- The Delivery layer (such as the REST Handler) calls the Usecase/Service layer and is responsible for transforming external requests into business logic calls.
This design follows the Dependency Inversion Principle, ensuring that the core business logic is independent of external implementation details, resulting in higher testability and flexibility.
Summary
This article, by combining Uncle Bob’s Clean Architecture and the go-clean-arch sample project, introduced how to implement Clean Architecture in Go projects. By dividing the system into layers such as core entities, use cases, interface adapters, and external frameworks, concerns are clearly separated, and the core business logic (Use Cases) is decoupled from external implementation details such as frameworks and databases.
The go-clean-arch project architecture organizes code in a layered manner, with clear responsibilities for each layer:
- Models Layer (Domain Layer): Defines core business entities and is independent of external implementations.
- Usecase Layer: Implements application logic, coordinating entities and external interactions.
- Repository Layer: Implements the specific details of data storage.
- Delivery Layer: Handles external requests and returns results.
This is just a sample project. The architecture design of an actual project should be flexibly adjusted according to practical requirements, team development habits, and standards. The core goal is to maintain the principle of layering, ensure the code is easy to understand, test, and maintain, and support the long-term scalability and evolution of the system.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ