Backend Development 20 min read

Understanding Code Architecture: Clean, Onion, Hexagonal, and COLA with a Go Project Scaffold

The article explains why layered architectures such as Clean, Onion, and Hexagonal are essential for reducing coupling and improving maintainability, introduces the COLA (Clean Object‑oriented and Layered Architecture) derived from them, and presents a concrete Go project scaffold with clearly defined Domain, Application, Infrastructure, and Adapter layers and recommended directory layout.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Code Architecture: Clean, Onion, Hexagonal, and COLA with a Go Project Scaffold

To reduce coupling between system components and improve maintainability, a well‑designed code framework is essential. This article introduces three widely known code architectures—Clean Architecture, Onion Architecture, and Hexagonal Architecture—and derives the COLA (Clean Object‑oriented and Layered Architecture) from them, presenting a Go language project scaffold based on COLA.

Why a code architecture is needed

Large projects with many contributors often become tangled codebases. Without clear architectural guidelines, adding a small feature can require ten times the effort to understand business logic and another ten times to write tests. A good architecture provides separation of concerns, making the system independent of specific UI, frameworks, or external components, and greatly simplifies testing.

Benefits of separation of concerns

UI independence : the UI can be swapped (web, desktop, console) without affecting business logic.

Framework independence : whether using Koa, Express, Gin, Echo, or any other framework, the core logic remains untouched.

External component independence : databases (MySQL, MongoDB, Neo4j), caches (Redis, Memcached) can be replaced without changing business code.

Easy testing : core logic can be tested without UI, database, or web server dependencies.

Clean Architecture

The core rule is the Dependency Rule: outer layers may depend on inner layers, but inner layers must not depend on outer layers. The typical four‑layer structure is:

Entities (core business models)

Use Cases (application business logic)

Interface Adapters (controllers, presenters, gateways)

Frameworks & Drivers (UI, DB, external services)

Example entity definitions:

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

Use‑case interface example:

type BlogManager interface {
    Load(... ) ...
    Save(... ) ...
    // other business methods
}

Data Transfer Object (DTO) example:

type BlogDTO struct {
    Content string `json:"..."`
    // DTO ↔ model conversion logic
}

Implementation of a simple repository method:

func CreateBlog(b *model.Blog) {
    dbClient.Create(&blog{...})
    // other persistence logic
}

Onion Architecture

Similar to Clean Architecture, it uses concentric circles where the innermost layer is the Domain Model, surrounded by Domain Services (Use Cases), Application Services, and finally the outer UI/Infrastructure layers.

Hexagonal (Ports & Adapters) Architecture

Defines Ports (interfaces) that are technology‑agnostic and Adapters that implement those ports. The direction of dependencies is always from the outside inward, matching the Dependency Rule.

Port example (interface):

type BlogManager interface {
    Load(... ) ...
    Save(... ) ...
}

Adapter example (MySQL implementation):

type MySQLPersistence struct {
    client client.SQLClient // uses a pre‑built SQL client
}

func (p *MySQLPersistence) Load(... ) ... {
    record := p.client.FindOne(...)
    return record.ToModel() // convert DO to domain model
}

COLA Architecture

Inspired by the three previous architectures, COLA (Clean Object‑oriented and Layered Architecture) emphasizes a clean, object‑oriented, layered design. Its layers are:

Domain (core business models and rules)

Application (use‑case orchestration, DTOs)

Infrastructure (external dependencies, persistence, messaging, etc.)

Adapter (framework and protocol adapters such as Gin, tRPC, Echo)

The following directory structure is recommended for a Go project following COLA:

├── adapter          // framework adapters (Gin, Echo, etc.)
├── application       // business logic independent of frameworks
│   ├── consumer      // optional message consumers
│   ├── dto           // DTO definitions
│   ├── executor      // command/query handlers
│   └── scheduler     // optional cron jobs
├── domain            // pure business entities and rules
│   ├── gateway       // domain interfaces
│   └── model         // domain models
├── infrastructure    // external components implementations
│   ├── cache         // Redis, Memcached
│   ├── client        // middleware clients (Kafka, MySQL, etc.)
│   ├── config        // configuration parsing
│   ├── database      // DB persistence
│   ├── distlock      // distributed lock implementations
│   ├── log           // logging wrapper
│   ├── mq            // message queue implementations
│   ├── node          // service discovery / coordination
│   └── rpc           // third‑party service access
└── pkg               // shared utilities

Sample Gin router (Adapter layer):

import (
    "mybusiness.com/blog-api/application/executor"
    "github.com/gin-gonic/gin"
)

func NewRouter() (*gin.Engine, error) {
    r := gin.Default()
    r.GET("/blog/:blog_id", getBlog)
    // other routes …
    return r, nil
}

func getBlog(c *gin.Context) {
    // b is *executor.BlogOperator injected elsewhere
    result := b.GetBlog(blogID)
    c.JSON(200, result)
}

Application layer executor example:

type BlogOperator struct {
    blogManager gateway.BlogManager // injected implementation from Infra
}

func (b *BlogOperator) GetBlog(... ) ... {
    blog, err := b.blogManager.Load(ctx, blogID)
    // error handling …
    return dto.BlogFromModel(blog)
}

Domain layer gateway definition:

type BlogManager interface {
    Load(... ) (model.Blog, error)
    Save(... ) error
    // other domain operations
}

Infrastructure layer MySQL persistence implementation (adapter for the domain gateway):

type MySQLPersistence struct {
    client client.SQLClient // pre‑built SQL client
}

func (p *MySQLPersistence) Load(... ) (model.Blog, error) {
    record := p.client.FindOne(...)
    return record.ToModel(), nil
}

When to adopt an architecture

Projects with a lifecycle longer than three months and multiple maintainers benefit from introducing a structured architecture. For long‑term (> three years) and larger teams (>5 developers), adopting an architecture is strongly recommended. Short‑term (< three months) or single‑developer projects may forego it due to added complexity.

Conclusion

Adopting a layered architecture such as COLA helps isolate business logic from technical concerns, reduces coupling, and improves maintainability. The article provides both conceptual guidance and concrete Go code examples to help developers design and implement such systems.

software architectureBackend DevelopmentGoclean architecturehexagonal architecturecode scaffolding
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

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.