Backend Development 19 min read

Understanding Code Architecture: Clean, Hexagonal, Onion, COLA and a Go Implementation Guide

This article explains why code architecture matters, compares Clean, Hexagonal, Onion and COLA patterns, describes their layered structures and separation‑of‑concerns principles, and provides a practical Go project layout with concrete code examples to illustrate how each layer interacts.

Architect
Architect
Architect
Understanding Code Architecture: Clean, Hexagonal, Onion, COLA and a Go Implementation Guide

The article emphasizes the importance of a well‑defined code architecture to avoid tangled, hard‑to‑maintain projects and to achieve separation of concerns, UI‑independence, framework‑independence, external‑component‑independence, and testability.

It introduces three classic architectural styles—Clean Architecture, Onion Architecture, and Hexagonal (Ports and Adapters) Architecture—showing their concentric‑layer diagrams and the common dependency rule that outer layers may depend on inner layers but not vice‑versa.

In Clean Architecture, the innermost Entities represent core business rules, surrounded by Use Cases (application business logic), then Interface Adapters (controllers, presenters, gateways), and finally the outer Frameworks & Drivers layer (UI, databases, web servers). Example Go entity definitions:

type Blog struct { ... }
type Comment struct { ... }

The Use Cases are expressed as interfaces such as:

type BlogManager interface {
CreateBlog(...) ...
LeaveComment(...) ...
}

Onion Architecture mirrors this structure with Domain Model , Domain Services , Application Services , and outer Infrastructure layers.

Hexagonal Architecture focuses on Ports (abstract interfaces) and Adapters (technical implementations), enforcing dependency injection from the outside in.

The article then presents the COLA (Clean Object‑oriented and Layered Architecture) pattern, which adapts the same principles to a four‑layer structure (Domain, Application, Interface, Infrastructure) and is widely used in Chinese projects.

Finally, a concrete Go project skeleton is proposed, illustrating how each layer maps to directories:

├── adapter        // adapters for Gin, tRPC, Echo, etc.
├── application    // business logic independent of frameworks
│   ├── consumer   // optional message consumers
│   ├── dto        // data transfer objects
│   ├── executor   // command/query handlers
│   └── scheduler  // optional cron jobs
├── domain         // core business entities and rules
│   ├── gateway    // interfaces for infrastructure
│   └── model      // domain models
├── infrastructure // external dependencies and implementations
│   ├── cache      // Redis, Memcached, etc.
│   ├── client     // Kafka, MySQL, Redis clients
│   ├── config     // configuration parsing
│   ├── database   // persistence implementations
│   ├── distlock   // distributed lock implementations
│   ├── log        // logging wrapper
│   ├── mq         // message queue implementations
│   ├── node       // service discovery/coordination
│   └── rpc        // external service clients
└── pkg            // shared utilities

Sample adapter code using Gin demonstrates that framework‑specific code stays in the adapter layer:

import (
"mybusiness.com/blog-api/application/executor" // App layer
"github.com/gin-gonic/gin"
)
func NewRouter(...) (*gin.Engine, error) {
r := gin.Default()
r.GET("/blog/:blog_id", getBlog)
...
}
func getBlog(...) {
// b is *executor.BlogOperator
result := b.GetBlog(blogID)
c.JSON(..., result)
}

The application layer executor implements business use cases while depending only on domain interfaces:

type BlogOperator struct {
blogManager gateway.BlogManager // injected implementation
}
func (b *BlogOperator) GetBlog(...) {
blog, err := b.blogManager.Load(ctx, blogID)
return dto.BlogFromModel(...)
}

The domain layer defines the core BlogManager interface, and the infrastructure layer provides a MySQL implementation that converts data objects to domain models:

type MySQLPersistence struct {
client client.SQLClient
}
func (p ...) Load(...) {
record := p.client.FindOne(...)
return record.ToModel()
}

In conclusion, the article advises introducing such architectures only for projects with longer lifespans and multiple maintainers, as they add complexity but greatly improve maintainability and testability.

backendsoftware architecturegoclean architecturehexagonal architecturecode organization
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.