Go Architecture Patterns Series: ← Layered Architecture | Series Overview | Next: Hexagonal Architecture →


What is Clean Architecture?

Clean Architecture, popularized by Robert C. Martin (Uncle Bob), is an architectural pattern that emphasizes separation of concerns and independence from frameworks, databases, and external agencies. It organizes code in concentric circles with dependencies pointing inward.

Key Principles:

  • Independence of Frameworks: Architecture doesn’t depend on frameworks
  • Testability: Business rules can be tested without UI, database, or external elements
  • Independence of UI: UI can change without changing business rules
  • Independence of Database: Business rules don’t know about the database
  • Independence of External Agency: Business rules don’t know about the outside world
  • Dependency Rule: Dependencies only point inward toward higher-level policies

Architecture Overview

graph TD subgraph "External Layer (Frameworks & Drivers)" A1[Web Framework] A2[Database] A3[External APIs] end subgraph "Interface Adapters" B1[Controllers] B2[Presenters] B3[Gateways] end subgraph "Use Cases (Application Business Rules)" C1[Interactors] C2[Use Case Logic] end subgraph "Entities (Enterprise Business Rules)" D1[Domain Models] D2[Business Logic] end A1 --> B1 A2 --> B3 B1 --> C1 B3 --> C1 C1 --> D1 style D1 fill:#FFD700 style C1 fill:#87CEEB style B1 fill:#90EE90 style A1 fill:#FFB6C1

Clean Architecture Circles

graph LR subgraph "Layer 1: Entities" E[Core Business
Rules & Models] end subgraph "Layer 2: Use Cases" U[Application
Business Rules] end subgraph "Layer 3: Interface Adapters" I[Controllers
Presenters
Gateways] end subgraph "Layer 4: Frameworks & Drivers" F[Web
DB
UI] end F -.->|depends on| I I -.->|depends on| U U -.->|depends on| E style E fill:#FFD700 style U fill:#87CEEB style I fill:#90EE90 style F fill:#FFB6C1

Dependency Flow

graph TB subgraph "Outer Layers (Low-level Details)" direction LR DB[Database] Web[Web Server] API[External APIs] end subgraph "Interface Adapters" direction LR Repo[Repository
Implementation] Controller[HTTP Controller] Gateway[API Gateway] end subgraph "Use Cases" direction LR UC1[Create User
Use Case] UC2[Get User
Use Case] end subgraph "Entities (Core)" direction LR Entity[User Entity] Rules[Business Rules] end DB --> Repo Web --> Controller API --> Gateway Repo -.->|implements| UC1 Controller -.->|calls| UC1 Gateway -.->|implements| UC2 UC1 -.->|uses| Entity UC2 -.->|uses| Entity style Entity fill:#FFD700,stroke:#FF8C00,stroke-width:3px style UC1 fill:#87CEEB style Repo fill:#90EE90 style DB fill:#FFB6C1

Real-World Use Cases

  • Enterprise Applications: Complex business logic that needs isolation
  • Long-lived Systems: Applications that need to evolve over time
  • Multi-platform Applications: Same core logic, different interfaces
  • Testing-Critical Systems: Financial, healthcare, or mission-critical apps
  • API-first Applications: Where business logic is reused across interfaces
  • Microservices: Each service following clean architecture principles

Project Structure

├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── entity/                    # Layer 1: Entities
│   │   ├── user.go
│   │   └── errors.go
│   ├── usecase/                   # Layer 2: Use Cases
│   │   ├── user_usecase.go
│   │   ├── interfaces.go         # Repository & Presenter interfaces
│   │   └── user_interactor.go
│   ├── adapter/                   # Layer 3: Interface Adapters
│   │   ├── repository/
│   │   │   └── user_repository.go
│   │   ├── presenter/
│   │   │   └── user_presenter.go
│   │   └── controller/
│   │       └── user_controller.go
│   └── infrastructure/            # Layer 4: Frameworks & Drivers
│       ├── database/
│       │   └── postgres.go
│       ├── router/
│       │   └── router.go
│       └── config/
│           └── config.go
└── go.mod

Layer 1: Entities (Core Business Rules)

package entity

import (
    "errors"
    "regexp"
    "time"
)

// User represents the core user entity with business rules
type User struct {
    ID        string
    Email     string
    Name      string
    Age       int
    Status    UserStatus
    CreatedAt time.Time
    UpdatedAt time.Time
}

// UserStatus represents user account status
type UserStatus string

const (
    UserStatusActive   UserStatus = "active"
    UserStatusInactive UserStatus = "inactive"
    UserStatusSuspended UserStatus = "suspended"
)

// Business rule validation errors
var (
    ErrInvalidEmail      = errors.New("invalid email format")
    ErrInvalidName       = errors.New("name must not be empty")
    ErrInvalidAge        = errors.New("age must be between 0 and 150")
    ErrUserNotFound      = errors.New("user not found")
    ErrUserAlreadyExists = errors.New("user already exists")
    ErrUnauthorized      = errors.New("unauthorized action")
)

// NewUser creates a new user with business rule validation
func NewUser(email, name string, age int) (*User, error) {
    user := &User{
        Email:     email,
        Name:      name,
        Age:       age,
        Status:    UserStatusActive,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }

    if err := user.Validate(); err != nil {
        return nil, err
    }

    return user, nil
}

// Validate validates the user entity according to business rules
func (u *User) Validate() error {
    // Business Rule: Email must be valid format
    if !isValidEmail(u.Email) {
        return ErrInvalidEmail
    }

    // Business Rule: Name must not be empty
    if u.Name == "" {
        return ErrInvalidName
    }

    // Business Rule: Age must be realistic
    if u.Age < 0 || u.Age > 150 {
        return ErrInvalidAge
    }

    return nil
}

// UpdateEmail updates user email with validation
func (u *User) UpdateEmail(email string) error {
    if !isValidEmail(email) {
        return ErrInvalidEmail
    }
    u.Email = email
    u.UpdatedAt = time.Now()
    return nil
}

// UpdateName updates user name with validation
func (u *User) UpdateName(name string) error {
    if name == "" {
        return ErrInvalidName
    }
    u.Name = name
    u.UpdatedAt = time.Now()
    return nil
}

// Activate activates the user account
func (u *User) Activate() {
    u.Status = UserStatusActive
    u.UpdatedAt = time.Now()
}

// Deactivate deactivates the user account
func (u *User) Deactivate() {
    u.Status = UserStatusInactive
    u.UpdatedAt = time.Now()
}

// Suspend suspends the user account
func (u *User) Suspend() {
    u.Status = UserStatusSuspended
    u.UpdatedAt = time.Now()
}

// IsActive checks if user is active
func (u *User) IsActive() bool {
    return u.Status == UserStatusActive
}

// CanPerformAction checks if user can perform actions (business rule)
func (u *User) CanPerformAction() bool {
    return u.Status == UserStatusActive
}

// isValidEmail validates email format
func isValidEmail(email string) bool {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    return emailRegex.MatchString(email)
}

// Product entity with business rules
type Product struct {
    ID          string
    Name        string
    Description string
    Price       Money
    Stock       int
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// Money represents monetary value (value object)
type Money struct {
    Amount   int64  // in cents
    Currency string
}

// NewMoney creates a new money value object
func NewMoney(amount int64, currency string) Money {
    return Money{
        Amount:   amount,
        Currency: currency,
    }
}

// Add adds two money values
func (m Money) Add(other Money) (Money, error) {
    if m.Currency != other.Currency {
        return Money{}, errors.New("currency mismatch")
    }
    return Money{
        Amount:   m.Amount + other.Amount,
        Currency: m.Currency,
    }, nil
}

// Multiply multiplies money by a factor
func (m Money) Multiply(factor int) Money {
    return Money{
        Amount:   m.Amount * int64(factor),
        Currency: m.Currency,
    }
}

Layer 2: Use Cases (Application Business Rules)

Use Case Interfaces

package usecase

import (
    "context"
    "myapp/internal/entity"
)

// UserRepository defines the interface for user data access
// This interface is defined in the use case layer but implemented in outer layers
type UserRepository interface {
    Create(ctx context.Context, user *entity.User) error
    GetByID(ctx context.Context, id string) (*entity.User, error)
    GetByEmail(ctx context.Context, email string) (*entity.User, error)
    Update(ctx context.Context, user *entity.User) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context, offset, limit int) ([]*entity.User, error)
}

// UserPresenter defines the interface for presenting user data
type UserPresenter interface {
    PresentUser(user *entity.User) interface{}
    PresentUsers(users []*entity.User) interface{}
    PresentError(err error) interface{}
}

// EmailService defines the interface for email operations
type EmailService interface {
    SendWelcomeEmail(ctx context.Context, user *entity.User) error
    SendPasswordResetEmail(ctx context.Context, user *entity.User, token string) error
}

// IDGenerator defines the interface for generating IDs
type IDGenerator interface {
    Generate() string
}

Use Case Implementation

package usecase

import (
    "context"
    "fmt"
    "myapp/internal/entity"
)

// CreateUserInput represents input for creating a user
type CreateUserInput struct {
    Email string
    Name  string
    Age   int
}

// UpdateUserInput represents input for updating a user
type UpdateUserInput struct {
    ID    string
    Email string
    Name  string
    Age   int
}

// UserInteractor implements user use cases
type UserInteractor struct {
    repo        UserRepository
    emailService EmailService
    idGen       IDGenerator
}

// NewUserInteractor creates a new user interactor
func NewUserInteractor(
    repo UserRepository,
    emailService EmailService,
    idGen IDGenerator,
) *UserInteractor {
    return &UserInteractor{
        repo:        repo,
        emailService: emailService,
        idGen:       idGen,
    }
}

// CreateUser creates a new user (use case)
func (i *UserInteractor) CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) {
    // Use case logic: Check if user already exists
    existingUser, err := i.repo.GetByEmail(ctx, input.Email)
    if err == nil && existingUser != nil {
        return nil, entity.ErrUserAlreadyExists
    }

    // Use case logic: Create new user entity
    user, err := entity.NewUser(input.Email, input.Name, input.Age)
    if err != nil {
        return nil, fmt.Errorf("failed to create user entity: %w", err)
    }

    // Use case logic: Generate ID
    user.ID = i.idGen.Generate()

    // Use case logic: Save user
    if err := i.repo.Create(ctx, user); err != nil {
        return nil, fmt.Errorf("failed to save user: %w", err)
    }

    // Use case logic: Send welcome email (async in real system)
    if err := i.emailService.SendWelcomeEmail(ctx, user); err != nil {
        // Log error but don't fail the use case
        fmt.Printf("failed to send welcome email: %v\n", err)
    }

    return user, nil
}

// GetUser retrieves a user by ID (use case)
func (i *UserInteractor) GetUser(ctx context.Context, id string) (*entity.User, error) {
    user, err := i.repo.GetByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("failed to get user: %w", err)
    }

    if user == nil {
        return nil, entity.ErrUserNotFound
    }

    return user, nil
}

// UpdateUser updates a user (use case)
func (i *UserInteractor) UpdateUser(ctx context.Context, input UpdateUserInput) (*entity.User, error) {
    // Use case logic: Get existing user
    user, err := i.repo.GetByID(ctx, input.ID)
    if err != nil {
        return nil, entity.ErrUserNotFound
    }

    // Use case logic: Check if email is changing and if it already exists
    if user.Email != input.Email {
        existingUser, err := i.repo.GetByEmail(ctx, input.Email)
        if err == nil && existingUser != nil {
            return nil, entity.ErrUserAlreadyExists
        }
    }

    // Use case logic: Update user fields
    if err := user.UpdateEmail(input.Email); err != nil {
        return nil, err
    }

    if err := user.UpdateName(input.Name); err != nil {
        return nil, err
    }

    user.Age = input.Age
    if err := user.Validate(); err != nil {
        return nil, err
    }

    // Use case logic: Save updated user
    if err := i.repo.Update(ctx, user); err != nil {
        return nil, fmt.Errorf("failed to update user: %w", err)
    }

    return user, nil
}

// DeleteUser deletes a user (use case)
func (i *UserInteractor) DeleteUser(ctx context.Context, id string) error {
    // Use case logic: Verify user exists
    user, err := i.repo.GetByID(ctx, id)
    if err != nil {
        return entity.ErrUserNotFound
    }

    // Use case logic: Check if user can be deleted (business rule)
    if !user.CanPerformAction() {
        return entity.ErrUnauthorized
    }

    // Use case logic: Delete user
    if err := i.repo.Delete(ctx, id); err != nil {
        return fmt.Errorf("failed to delete user: %w", err)
    }

    return nil
}

// ListUsers lists users with pagination (use case)
func (i *UserInteractor) ListUsers(ctx context.Context, page, pageSize int) ([]*entity.User, error) {
    // Use case logic: Validate pagination
    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 20
    }

    offset := (page - 1) * pageSize

    // Use case logic: Get users
    users, err := i.repo.List(ctx, offset, pageSize)
    if err != nil {
        return nil, fmt.Errorf("failed to list users: %w", err)
    }

    return users, nil
}

// ActivateUser activates a user account (use case)
func (i *UserInteractor) ActivateUser(ctx context.Context, id string) error {
    user, err := i.repo.GetByID(ctx, id)
    if err != nil {
        return entity.ErrUserNotFound
    }

    user.Activate()

    if err := i.repo.Update(ctx, user); err != nil {
        return fmt.Errorf("failed to activate user: %w", err)
    }

    return nil
}

Layer 3: Interface Adapters

Repository Implementation

package repository

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    "myapp/internal/entity"
)

// PostgresUserRepository implements UserRepository interface
type PostgresUserRepository struct {
    db *sql.DB
}

// NewPostgresUserRepository creates a new Postgres user repository
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
    return &PostgresUserRepository{db: db}
}

// Create creates a new user in the database
func (r *PostgresUserRepository) Create(ctx context.Context, user *entity.User) error {
    query := `
        INSERT INTO users (id, email, name, age, status, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7)
    `

    _, err := r.db.ExecContext(
        ctx, query,
        user.ID,
        user.Email,
        user.Name,
        user.Age,
        user.Status,
        user.CreatedAt,
        user.UpdatedAt,
    )

    if err != nil {
        return fmt.Errorf("failed to create user: %w", err)
    }

    return nil
}

// GetByID retrieves a user by ID
func (r *PostgresUserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
    query := `
        SELECT id, email, name, age, status, created_at, updated_at
        FROM users
        WHERE id = $1
    `

    var user entity.User
    var status string

    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.Email,
        &user.Name,
        &user.Age,
        &status,
        &user.CreatedAt,
        &user.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, entity.ErrUserNotFound
    }
    if err != nil {
        return nil, fmt.Errorf("failed to get user: %w", err)
    }

    user.Status = entity.UserStatus(status)

    return &user, nil
}

// GetByEmail retrieves a user by email
func (r *PostgresUserRepository) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
    query := `
        SELECT id, email, name, age, status, created_at, updated_at
        FROM users
        WHERE email = $1
    `

    var user entity.User
    var status string

    err := r.db.QueryRowContext(ctx, query, email).Scan(
        &user.ID,
        &user.Email,
        &user.Name,
        &user.Age,
        &status,
        &user.CreatedAt,
        &user.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, fmt.Errorf("failed to get user: %w", err)
    }

    user.Status = entity.UserStatus(status)

    return &user, nil
}

// Update updates a user
func (r *PostgresUserRepository) Update(ctx context.Context, user *entity.User) error {
    query := `
        UPDATE users
        SET email = $1, name = $2, age = $3, status = $4, updated_at = $5
        WHERE id = $6
    `

    result, err := r.db.ExecContext(
        ctx, query,
        user.Email,
        user.Name,
        user.Age,
        user.Status,
        time.Now(),
        user.ID,
    )

    if err != nil {
        return fmt.Errorf("failed to update user: %w", err)
    }

    rows, err := result.RowsAffected()
    if err != nil {
        return fmt.Errorf("failed to get affected rows: %w", err)
    }

    if rows == 0 {
        return entity.ErrUserNotFound
    }

    return nil
}

// Delete deletes a user
func (r *PostgresUserRepository) Delete(ctx context.Context, id string) error {
    query := `DELETE FROM users WHERE id = $1`

    result, err := r.db.ExecContext(ctx, query, id)
    if err != nil {
        return fmt.Errorf("failed to delete user: %w", err)
    }

    rows, err := result.RowsAffected()
    if err != nil {
        return fmt.Errorf("failed to get affected rows: %w", err)
    }

    if rows == 0 {
        return entity.ErrUserNotFound
    }

    return nil
}

// List retrieves users with pagination
func (r *PostgresUserRepository) List(ctx context.Context, offset, limit int) ([]*entity.User, error) {
    query := `
        SELECT id, email, name, age, status, created_at, updated_at
        FROM users
        ORDER BY created_at DESC
        LIMIT $1 OFFSET $2
    `

    rows, err := r.db.QueryContext(ctx, query, limit, offset)
    if err != nil {
        return nil, fmt.Errorf("failed to list users: %w", err)
    }
    defer rows.Close()

    var users []*entity.User
    for rows.Next() {
        var user entity.User
        var status string

        err := rows.Scan(
            &user.ID,
            &user.Email,
            &user.Name,
            &user.Age,
            &status,
            &user.CreatedAt,
            &user.UpdatedAt,
        )
        if err != nil {
            return nil, fmt.Errorf("failed to scan user: %w", err)
        }

        user.Status = entity.UserStatus(status)
        users = append(users, &user)
    }

    return users, nil
}

Controller Implementation

package controller

import (
    "encoding/json"
    "net/http"
    "strconv"

    "myapp/internal/entity"
    "myapp/internal/usecase"
)

// UserController handles HTTP requests for users
type UserController struct {
    interactor *usecase.UserInteractor
    presenter  usecase.UserPresenter
}

// NewUserController creates a new user controller
func NewUserController(
    interactor *usecase.UserInteractor,
    presenter usecase.UserPresenter,
) *UserController {
    return &UserController{
        interactor: interactor,
        presenter:  presenter,
    }
}

// CreateUserRequest represents the HTTP request for creating a user
type CreateUserRequest struct {
    Email string `json:"email"`
    Name  string `json:"name"`
    Age   int    `json:"age"`
}

// CreateUser handles POST /users
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        c.respondError(w, http.StatusBadRequest, err)
        return
    }

    input := usecase.CreateUserInput{
        Email: req.Email,
        Name:  req.Name,
        Age:   req.Age,
    }

    user, err := c.interactor.CreateUser(r.Context(), input)
    if err != nil {
        switch err {
        case entity.ErrInvalidEmail, entity.ErrInvalidName, entity.ErrInvalidAge:
            c.respondError(w, http.StatusBadRequest, err)
        case entity.ErrUserAlreadyExists:
            c.respondError(w, http.StatusConflict, err)
        default:
            c.respondError(w, http.StatusInternalServerError, err)
        }
        return
    }

    c.respond(w, http.StatusCreated, c.presenter.PresentUser(user))
}

// GetUser handles GET /users/:id
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")

    user, err := c.interactor.GetUser(r.Context(), id)
    if err != nil {
        if err == entity.ErrUserNotFound {
            c.respondError(w, http.StatusNotFound, err)
        } else {
            c.respondError(w, http.StatusInternalServerError, err)
        }
        return
    }

    c.respond(w, http.StatusOK, c.presenter.PresentUser(user))
}

// UpdateUser handles PUT /users/:id
func (c *UserController) UpdateUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")

    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        c.respondError(w, http.StatusBadRequest, err)
        return
    }

    input := usecase.UpdateUserInput{
        ID:    id,
        Email: req.Email,
        Name:  req.Name,
        Age:   req.Age,
    }

    user, err := c.interactor.UpdateUser(r.Context(), input)
    if err != nil {
        switch err {
        case entity.ErrUserNotFound:
            c.respondError(w, http.StatusNotFound, err)
        case entity.ErrInvalidEmail, entity.ErrInvalidName, entity.ErrInvalidAge:
            c.respondError(w, http.StatusBadRequest, err)
        case entity.ErrUserAlreadyExists:
            c.respondError(w, http.StatusConflict, err)
        default:
            c.respondError(w, http.StatusInternalServerError, err)
        }
        return
    }

    c.respond(w, http.StatusOK, c.presenter.PresentUser(user))
}

// DeleteUser handles DELETE /users/:id
func (c *UserController) DeleteUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")

    if err := c.interactor.DeleteUser(r.Context(), id); err != nil {
        if err == entity.ErrUserNotFound {
            c.respondError(w, http.StatusNotFound, err)
        } else {
            c.respondError(w, http.StatusInternalServerError, err)
        }
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

// ListUsers handles GET /users
func (c *UserController) ListUsers(w http.ResponseWriter, r *http.Request) {
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))

    users, err := c.interactor.ListUsers(r.Context(), page, pageSize)
    if err != nil {
        c.respondError(w, http.StatusInternalServerError, err)
        return
    }

    c.respond(w, http.StatusOK, c.presenter.PresentUsers(users))
}

// Helper methods
func (c *UserController) respond(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func (c *UserController) respondError(w http.ResponseWriter, status int, err error) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(c.presenter.PresentError(err))
}

Presenter Implementation

package presenter

import (
    "myapp/internal/entity"
)

// UserJSONPresenter presents users as JSON
type UserJSONPresenter struct{}

// NewUserJSONPresenter creates a new user JSON presenter
func NewUserJSONPresenter() *UserJSONPresenter {
    return &UserJSONPresenter{}
}

// UserResponse represents the JSON response for a user
type UserResponse struct {
    ID        string `json:"id"`
    Email     string `json:"email"`
    Name      string `json:"name"`
    Age       int    `json:"age"`
    Status    string `json:"status"`
    CreatedAt string `json:"created_at"`
    UpdatedAt string `json:"updated_at"`
}

// PresentUser presents a single user
func (p *UserJSONPresenter) PresentUser(user *entity.User) interface{} {
    return UserResponse{
        ID:        user.ID,
        Email:     user.Email,
        Name:      user.Name,
        Age:       user.Age,
        Status:    string(user.Status),
        CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
        UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z"),
    }
}

// PresentUsers presents multiple users
func (p *UserJSONPresenter) PresentUsers(users []*entity.User) interface{} {
    responses := make([]UserResponse, len(users))
    for i, user := range users {
        responses[i] = UserResponse{
            ID:        user.ID,
            Email:     user.Email,
            Name:      user.Name,
            Age:       user.Age,
            Status:    string(user.Status),
            CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
            UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z"),
        }
    }
    return map[string]interface{}{
        "users": responses,
        "count": len(responses),
    }
}

// ErrorResponse represents an error response
type ErrorResponse struct {
    Error string `json:"error"`
    Code  string `json:"code,omitempty"`
}

// PresentError presents an error
func (p *UserJSONPresenter) PresentError(err error) interface{} {
    return ErrorResponse{
        Error: err.Error(),
    }
}

Layer 4: Main Application (Dependency Injection)

package main

import (
    "database/sql"
    "log"
    "net/http"

    _ "github.com/lib/pq"
    "github.com/google/uuid"

    "myapp/internal/adapter/controller"
    "myapp/internal/adapter/presenter"
    "myapp/internal/adapter/repository"
    "myapp/internal/usecase"
)

// UUIDGenerator implements IDGenerator
type UUIDGenerator struct{}

func (g *UUIDGenerator) Generate() string {
    return uuid.New().String()
}

// MockEmailService implements EmailService
type MockEmailService struct{}

func (s *MockEmailService) SendWelcomeEmail(ctx context.Context, user *entity.User) error {
    log.Printf("Sending welcome email to %s", user.Email)
    return nil
}

func (s *MockEmailService) SendPasswordResetEmail(ctx context.Context, user *entity.User, token string) error {
    log.Printf("Sending password reset email to %s with token %s", user.Email, token)
    return nil
}

func main() {
    // Initialize database
    db, err := sql.Open("postgres", "postgres://user:pass@localhost/cleanarch?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Layer 4: Initialize adapters (implementations)
    userRepo := repository.NewPostgresUserRepository(db)
    emailService := &MockEmailService{}
    idGen := &UUIDGenerator{}
    userPresenter := presenter.NewUserJSONPresenter()

    // Layer 2: Initialize use cases (with dependency injection)
    userInteractor := usecase.NewUserInteractor(userRepo, emailService, idGen)

    // Layer 3: Initialize controllers
    userController := controller.NewUserController(userInteractor, userPresenter)

    // Setup routes
    mux := http.NewServeMux()
    mux.HandleFunc("POST /users", userController.CreateUser)
    mux.HandleFunc("GET /users/{id}", userController.GetUser)
    mux.HandleFunc("PUT /users/{id}", userController.UpdateUser)
    mux.HandleFunc("DELETE /users/{id}", userController.DeleteUser)
    mux.HandleFunc("GET /users", userController.ListUsers)

    // Start server
    log.Println("Server starting on :8080")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

Testing in Clean Architecture

package usecase_test

import (
    "context"
    "testing"

    "myapp/internal/entity"
    "myapp/internal/usecase"
)

// Mock implementations
type MockUserRepository struct {
    users map[string]*entity.User
}

func NewMockUserRepository() *MockUserRepository {
    return &MockUserRepository{
        users: make(map[string]*entity.User),
    }
}

func (m *MockUserRepository) Create(ctx context.Context, user *entity.User) error {
    m.users[user.ID] = user
    return nil
}

func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
    user, exists := m.users[id]
    if !exists {
        return nil, entity.ErrUserNotFound
    }
    return user, nil
}

func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
    for _, user := range m.users {
        if user.Email == email {
            return user, nil
        }
    }
    return nil, nil
}

type MockEmailService struct {
    sentEmails []string
}

func (m *MockEmailService) SendWelcomeEmail(ctx context.Context, user *entity.User) error {
    m.sentEmails = append(m.sentEmails, user.Email)
    return nil
}

type MockIDGenerator struct {
    nextID int
}

func (m *MockIDGenerator) Generate() string {
    m.nextID++
    return fmt.Sprintf("user-%d", m.nextID)
}

func TestCreateUser(t *testing.T) {
    // Arrange
    repo := NewMockUserRepository()
    emailService := &MockEmailService{}
    idGen := &MockIDGenerator{}
    interactor := usecase.NewUserInteractor(repo, emailService, idGen)

    input := usecase.CreateUserInput{
        Email: "[email protected]",
        Name:  "Test User",
        Age:   25,
    }

    // Act
    user, err := interactor.CreateUser(context.Background(), input)

    // Assert
    if err != nil {
        t.Fatalf("expected no error, got %v", err)
    }

    if user.Email != input.Email {
        t.Errorf("expected email %s, got %s", input.Email, user.Email)
    }

    if len(emailService.sentEmails) != 1 {
        t.Errorf("expected 1 welcome email, got %d", len(emailService.sentEmails))
    }
}

Best Practices

  1. Dependency Rule: Always point dependencies inward
  2. Interface Segregation: Define minimal interfaces in use case layer
  3. Dependency Injection: Inject all dependencies explicitly
  4. Entity Purity: Keep entities free from framework dependencies
  5. Use Case Focus: Each use case should have a single responsibility
  6. Test Independence: Test each layer independently
  7. Avoid Anemic Models: Put business logic in entities

Common Pitfalls

  1. Breaking Dependency Rule: Outer layers should not be imported by inner layers
  2. Leaking Infrastructure: Database or framework details leaking into entities
  3. Fat Use Cases: Use cases doing too much or too little
  4. Ignoring Presenters: Directly returning entities from controllers
  5. Over-engineering: Applying Clean Architecture to simple CRUD apps
  6. Missing Boundaries: Not clearly defining layer boundaries

When to Use Clean Architecture

Use When:

  • Building complex business applications
  • Long-term maintainability is critical
  • Multiple interfaces (web, CLI, mobile) share the same logic
  • Testing is a high priority
  • Team size is medium to large

Avoid When:

  • Building simple CRUD applications
  • Rapid prototyping is needed
  • Team is unfamiliar with the pattern
  • Project timeline is very short
  • Application complexity doesn’t justify the overhead

Advantages vs Disadvantages

Advantages:

  • Highly testable business logic
  • Framework independence
  • Database independence
  • Clear separation of concerns
  • Easy to swap implementations
  • Domain-centric design

Disadvantages:

  • Steeper learning curve
  • More boilerplate code
  • Can be over-engineering for simple apps
  • Requires discipline to maintain
  • More files and abstractions

Clean Architecture provides a robust foundation for building maintainable, testable applications where business logic is king and external dependencies are just implementation details.


Go Architecture Patterns Series: ← Layered Architecture | Series Overview | Next: Hexagonal Architecture →