This article covers The Complete Guide to Clean Architecture. Architecture-focused. Every step describes what is involved and the role it plays in clean architecture

Table of Contents

Introduction to Clean Architecture {#introduction}

What is Clean Architecture?

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is an architectural pattern that separates software into layers with strict dependency rules. The goal is to create systems that are:

Independent of frameworks : Your business logic doesn't depend on external libraries

: Your business logic doesn't depend on external libraries Testable : Business rules can be tested without UI, database, or external services

: Business rules can be tested without UI, database, or external services Independent of UI : You can swap UIs without changing business logic

: You can swap UIs without changing business logic Independent of database : Business rules don't know about the database

: Business rules don't know about the database Independent of external agencies: Business rules don't know about the outside world

The Dependency Rule

The fundamental rule: Source code dependencies must point only inward, toward higher-level policies.

┌─────────────────────────────────────────┐ │ Frameworks & Drivers │ External │ (Web, DB, UI, External Services) │ Layer ├─────────────────────────────────────────┤ │ Interface Adapters │ Adapters │ (Controllers, Presenters, Gateways) │ Layer ├─────────────────────────────────────────┤ │ Use Cases │ Application │ (Application Business Rules) │ Layer ├─────────────────────────────────────────┤ │ Entities │ Enterprise │ (Enterprise Business Rules) │ Layer └─────────────────────────────────────────┘ ↑ ↑ ↑ ↑ Dependencies point INWARD only

Key insight: Inner layers define interfaces; outer layers implement them. This inverts the typical dependency flow.

Why Clean Architecture?

Traditional layered architecture problems:

type UserService struct { db *sql.DB } func (s *UserService) CreateUser(name string ) error { _, err := s.db.Exec( "INSERT INTO users..." ) return err }

Clean Architecture solution:

type UserRepository interface { Save(user *User) error } type UserService struct { repo UserRepository } func (s *UserService) CreateUser(name string ) error { user := &User{Name: name} return s.repo.Save(user) }

Core Principles and Rules {#principles}

1. The Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

type OrderProcessor struct { mysql *MySQLDatabase } type OrderRepository interface { Save(order *Order) error } type OrderProcessor struct { repo OrderRepository } type MySQLOrderRepository struct { db *sql.DB } func (r *MySQLOrderRepository) Save(order *Order) error { }

2. The Stable Abstractions Principle

Abstractions (interfaces) should be more stable than implementations.

type PaymentGateway interface { Charge(amount Money, card CreditCard) (TransactionID, error ) } type StripePaymentGateway struct { apiKey string client *stripe.Client } func (g *StripePaymentGateway) Charge(amount Money, card CreditCard) (TransactionID, error ) { }

3. Screaming Architecture

Your architecture should scream what the application does, not what frameworks it uses.

❌ Framework-centric: myapp / ├── controllers / ├── models / ├── views / └── routes / ✅ Domain-centric: myapp / ├── users / ├── orders / ├── products / ├── payments / └── shipping /

4. Separation of Concerns

Each layer has a specific responsibility:

Entities : Enterprise business rules and data

: Enterprise business rules and data Use Cases : Application-specific business rules

: Application-specific business rules Interface Adapters : Convert data between use cases and external world

: Convert data between use cases and external world Frameworks & Drivers: External tools and frameworks

Layer-by-Layer Breakdown {#layers}

Layer 1: Entities (Enterprise Business Rules)

Purpose: Core business objects and rules that are independent of any application.

Characteristics:

Pure business logic

No dependencies on outer layers

Minimal dependencies on frameworks

Highly reusable across applications

package domain import ( "errors" "time" ) type User struct { ID string Email string Password string CreatedAt time.Time UpdatedAt time.Time } func (u *User) Validate() error { if u.Email == "" { return errors.New( "email is required" ) } if len (u.Password) < 8 { return errors.New( "password must be at least 8 characters" ) } return nil } func (u *User) IsActive() bool { return time.Since(u.CreatedAt) < 365 * 24 *time.Hour }

Layer 2: Use Cases (Application Business Rules)

Purpose: Application-specific business rules that orchestrate the flow of data to and from entities.

Characteristics:

Define what the application does

Orchestrate entities

Define interfaces for data access

Independent of UI, database, frameworks

package usecase import ( "context" "errors" "myapp/domain" ) type UserRepository interface { Save(ctx context.Context, user *domain.User) error FindByEmail(ctx context.Context, email string ) (*domain.User, error ) FindByID(ctx context.Context, id string ) (*domain.User, error ) } type PasswordHasher interface { Hash(password string ) ( string , error ) Compare(hashedPassword, password string ) error } type CreateUserUseCase struct { userRepo UserRepository hasher PasswordHasher } func NewCreateUserUseCase (repo UserRepository, hasher PasswordHasher) *CreateUserUseCase { return &CreateUserUseCase{ userRepo: repo, hasher: hasher, } } func (uc *CreateUserUseCase) Execute(ctx context.Context, email, password string ) (*domain.User, error ) { existing, err := uc.userRepo.FindByEmail(ctx, email) if err == nil && existing != nil { return nil , errors.New( "user already exists" ) } user := &domain.User{ Email: email, Password: password, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := user.Validate(); err != nil { return nil , err } hashedPassword, err := uc.hasher.Hash(password) if err != nil { return nil , err } user.Password = hashedPassword if err := uc.userRepo.Save(ctx, user); err != nil { return nil , err } return user, nil }

Layer 3: Interface Adapters

Purpose: Convert data between the format most convenient for use cases and entities, and the format most convenient for external agencies.

Characteristics:

Controllers, presenters, gateways

Implement interfaces defined by use cases

Convert data formats

Handle framework-specific concerns

package repository import ( "context" "database/sql" "myapp/domain" ) type PostgresUserRepository struct { db *sql.DB } func NewPostgresUserRepository (db *sql.DB) *PostgresUserRepository { return &PostgresUserRepository{db: db} } func (r *PostgresUserRepository) Save(ctx context.Context, user *domain.User) error { query := ` INSERT INTO users (id, email, password, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) ` _, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.Password, user.CreatedAt, user.UpdatedAt) return err } func (r *PostgresUserRepository) FindByEmail(ctx context.Context, email string ) (*domain.User, error ) { query := `SELECT id, email, password, created_at, updated_at FROM users WHERE email = $1` user := &domain.User{} err := r.db.QueryRowContext(ctx, query, email).Scan( &user.ID, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt) if err == sql.ErrNoRows { return nil , nil } if err != nil { return nil , err } return user, nil }

Layer 4: Frameworks & Drivers

Purpose: External frameworks and tools (web frameworks, databases, etc.)

Characteristics:

Outermost layer

Implementation details

Easily swappable

Main entry point and dependency wiring

package main import ( "database/sql" "log" "net/http" "myapp/handler" "myapp/repository" "myapp/security" "myapp/usecase" "github.com/gorilla/mux" _ "github.com/lib/pq" ) func main () { db, err := sql.Open( "postgres" , "postgres://..." ) if err != nil { log.Fatal(err) } defer db.Close() userRepo := repository.NewPostgresUserRepository(db) hasher := security.NewBcryptHasher() createUserUC := usecase.NewCreateUserUseCase(userRepo, hasher) userHandler := handler.NewUserHandler(createUserUC) router := mux.NewRouter() router.HandleFunc( "/users" , userHandler.CreateUser).Methods( "POST" ) log.Println( "Server starting on :8080" ) log.Fatal(http.ListenAndServe( ":8080" , router)) }

Sample Project: User Management System {#sample-project}

We'll build a complete user management system with the following features:

User registration

User authentication

Profile management

Role-based access control

Project Structure

user-management / ├── cmd / │ └── api / │ └── main.go ├── internal / │ ├── domain / │ │ ├── user.go │ │ ├── role.go │ │ └── errors.go │ ├── usecase / │ │ ├── user_registration.go │ │ ├── user_authentication.go │ │ ├── user_profile.go │ │ └── interfaces.go │ ├── repository / │ │ ├── postgres / │ │ │ ├── user_repository.go │ │ │ └── role_repository.go │ │ └── memory / │ │ └── user_repository.go │ ├── handler / │ │ ├── http / │ │ │ ├── user_handler.go │ │ │ ├── auth_handler.go │ │ │ └── middleware.go │ │ └── dto / │ │ ├── user_dto.go │ │ └── auth_dto.go │ ├── security / │ │ ├── hasher.go │ │ └── jwt.go │ └── config / │ └── config.go ├── pkg / │ ├── validator / │ │ └── validator.go │ └── logger / │ └── logger.go ├── migrations / │ ├── 001 _create_users_table.sql │ └── 002 _create_roles_table.sql ├── go.mod └── go.sum

Complete Go Implementation {#implementation}

Domain Layer

internal/domain/user.go: