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: