The Complete Guide to Clean Architecture

Table of Contents

  1. Introduction to Clean Architecture
  2. Core Principles and Rules
  3. Layer-by-Layer Breakdown
  4. Sample Project: User Management System
  5. Complete Go Implementation
  6. Testing Strategy
  7. Common Patterns and Practices
  8. Pitfalls and How to Avoid Them
  9. Real-World Considerations

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
  • Testable: Business rules can be tested without UI, database, or external services
  • Independent of UI: You can swap UIs without changing business logic
  • Independent of 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:

// ❌ Traditional approach - tight coupling
type UserService struct {
    db *sql.DB  // Depends on concrete implementation
}

func (s *UserService) CreateUser(name string) error {
    _, err := s.db.Exec("INSERT INTO users...")  // SQL in business logic
    return err
}

Clean Architecture solution:

// ✅ Clean Architecture - dependency inversion
type UserRepository interface {
    Save(user *User) error
}

type UserService struct {
    repo UserRepository  // Depends on abstraction
}

func (s *UserService) CreateUser(name string) error {
    user := &User{Name: name}
    return s.repo.Save(user)  // No knowledge of database
}

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.

// ❌ Bad: High-level depends on low-level
type OrderProcessor struct {
    mysql *MySQLDatabase
}

// ✅ Good: Both depend on abstraction
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 {
    // Implementation
}

2. The Stable Abstractions Principle

Abstractions (interfaces) should be more stable than implementations.

// Stable interface in domain layer
type PaymentGateway interface {
    Charge(amount Money, card CreditCard) (TransactionID, error)
}

// Volatile implementation in infrastructure layer
type StripePaymentGateway struct {
    apiKey string
    client *stripe.Client
}

func (g *StripePaymentGateway) Charge(amount Money, card CreditCard) (TransactionID, error) {
    // Stripe-specific implementation
}

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
  • Use Cases: Application-specific business rules
  • Interface Adapters: 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"
)

// User represents a user entity
type User struct {
    ID        string
    Email     string
    Password  string
    CreatedAt time.Time
    UpdatedAt time.Time
}

// Validate ensures user data is valid
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
}

// IsActive checks if user account is active
func (u *User) IsActive() bool {
    // Business rule: users are active if created in last 365 days
    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"
)

// UserRepository defines data access interface
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)
}

// PasswordHasher defines password hashing interface
type PasswordHasher interface {
    Hash(password string) (string, error)
    Compare(hashedPassword, password string) error
}

// CreateUserUseCase handles user creation
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) {
    // Check if user already exists
    existing, err := uc.userRepo.FindByEmail(ctx, email)
    if err == nil && existing != nil {
        return nil, errors.New("user already exists")
    }

    // Create new user
    user := &domain.User{
        Email:     email,
        Password:  password,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }

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

    // Hash password
    hashedPassword, err := uc.hasher.Hash(password)
    if err != nil {
        return nil, err
    }
    user.Password = hashedPassword

    // Save user
    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"
)

// PostgresUserRepository implements UserRepository using PostgreSQL
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() {
    // Database connection (framework layer)
    db, err := sql.Open("postgres", "postgres://...")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Initialize repositories (adapter layer)
    userRepo := repository.NewPostgresUserRepository(db)
    hasher := security.NewBcryptHasher()

    // Initialize use cases (application layer)
    createUserUC := usecase.NewCreateUserUseCase(userRepo, hasher)

    // Initialize handlers (adapter layer)
    userHandler := handler.NewUserHandler(createUserUC)

    // Setup HTTP router (framework layer)
    router := mux.NewRouter()
    router.HandleFunc("/users", userHandler.CreateUser).Methods("POST")

    // Start server
    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                 # Application entry point
├── internal/
│   ├── domain/                     # Enterprise business rules
│   │   ├── user.go
│   │   ├── role.go
│   │   └── errors.go
│   ├── usecase/                    # Application business rules
│   │   ├── user_registration.go
│   │   ├── user_authentication.go
│   │   ├── user_profile.go
│   │   └── interfaces.go
│   ├── repository/                 # Data access adapters
│   │   ├── postgres/
│   │   │   ├── user_repository.go
│   │   │   └── role_repository.go
│   │   └── memory/
│   │       └── user_repository.go
│   ├── handler/                    # HTTP handlers
│   │   ├── http/
│   │   │   ├── user_handler.go
│   │   │   ├── auth_handler.go
│   │   │   └── middleware.go
│   │   └── dto/
│   │       ├── user_dto.go
│   │       └── auth_dto.go
│   ├── security/                   # Security adapters
│   │   ├── hasher.go
│   │   └── jwt.go
│   └── config/                     # Configuration
│       └── config.go
├── pkg/                            # Shared utilities
│   ├── validator/
│   │   └── validator.go
│   └── logger/
│       └── logger.go
├── migrations/                     # Database 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:

package domain

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

var (
    ErrInvalidEmail    = errors.New("invalid email format")
    ErrPasswordTooShort = errors.New("password must be at least 8 characters")
    ErrInvalidUserID   = errors.New("invalid user ID")
)

// User represents a user in the system
type User struct {
    ID           string
    Email        string
    PasswordHash string
    FirstName    string
    LastName     string
    RoleID       string
    IsActive     bool
    CreatedAt    time.Time
    UpdatedAt    time.Time
    LastLoginAt  *time.Time
}

// NewUser creates a new user with validation
func NewUser(email, firstName, lastName string) (*User, error) {
    user := &User{
        Email:     email,
        FirstName: firstName,
        LastName:  lastName,
        IsActive:  true,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }

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

    return user, nil
}

// Validate checks if user data is valid
func (u *User) Validate() error {
    if !isValidEmail(u.Email) {
        return ErrInvalidEmail
    }
    if u.FirstName == "" || u.LastName == "" {
        return errors.New("first name and last name are required")
    }
    return nil
}

// FullName returns the user's full name
func (u *User) FullName() string {
    return u.FirstName + " " + u.LastName
}

// RecordLogin updates the last login time
func (u *User) RecordLogin() {
    now := time.Now()
    u.LastLoginAt = &now
    u.UpdatedAt = now
}

// Deactivate marks the user as inactive
func (u *User) Deactivate() {
    u.IsActive = false
    u.UpdatedAt = time.Now()
}

// Activate marks the user as active
func (u *User) Activate() {
    u.IsActive = true
    u.UpdatedAt = time.Now()
}

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

internal/domain/role.go:

package domain

import "time"

// Permission represents a permission in the system
type Permission string

const (
    PermissionReadUsers   Permission = "users:read"
    PermissionWriteUsers  Permission = "users:write"
    PermissionDeleteUsers Permission = "users:delete"
    PermissionManageRoles Permission = "roles:manage"
)

// Role represents a user role
type Role struct {
    ID          string
    Name        string
    Description string
    Permissions []Permission
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// HasPermission checks if role has a specific permission
func (r *Role) HasPermission(permission Permission) bool {
    for _, p := range r.Permissions {
        if p == permission {
            return true
        }
    }
    return false
}

// Common roles
var (
    RoleAdmin = &Role{
        ID:   "admin",
        Name: "Administrator",
        Permissions: []Permission{
            PermissionReadUsers,
            PermissionWriteUsers,
            PermissionDeleteUsers,
            PermissionManageRoles,
        },
    }

    RoleUser = &Role{
        ID:   "user",
        Name: "User",
        Permissions: []Permission{
            PermissionReadUsers,
        },
    }
)

internal/domain/errors.go:

package domain

import "errors"

var (
    // User errors
    ErrUserNotFound      = errors.New("user not found")
    ErrUserAlreadyExists = errors.New("user already exists")
    ErrInvalidCredentials = errors.New("invalid credentials")
    ErrUserNotActive     = errors.New("user account is not active")

    // Role errors
    ErrRoleNotFound = errors.New("role not found")
    ErrUnauthorized = errors.New("unauthorized")

    // General errors
    ErrInvalidInput = errors.New("invalid input")
)

Use Case Layer

internal/usecase/interfaces.go:

package usecase

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

// UserRepository defines the interface for user data access
type UserRepository interface {
    Save(ctx context.Context, user *domain.User) error
    FindByID(ctx context.Context, id string) (*domain.User, error)
    FindByEmail(ctx context.Context, email string) (*domain.User, error)
    Update(ctx context.Context, user *domain.User) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context, limit, offset int) ([]*domain.User, error)
}

// RoleRepository defines the interface for role data access
type RoleRepository interface {
    FindByID(ctx context.Context, id string) (*domain.Role, error)
    FindByName(ctx context.Context, name string) (*domain.Role, error)
    List(ctx context.Context) ([]*domain.Role, error)
}

// PasswordHasher defines the interface for password hashing
type PasswordHasher interface {
    Hash(password string) (string, error)
    Compare(hashedPassword, password string) error
}

// TokenGenerator defines the interface for token generation
type TokenGenerator interface {
    Generate(userID string, email string) (string, error)
    Validate(token string) (userID string, err error)
}

// IDGenerator defines the interface for ID generation
type IDGenerator interface {
    Generate() string
}

internal/usecase/user_registration.go:

package usecase

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

// RegisterUserRequest represents user registration input
type RegisterUserRequest struct {
    Email     string
    Password  string
    FirstName string
    LastName  string
}

// RegisterUserResponse represents user registration output
type RegisterUserResponse struct {
    UserID string
    Email  string
}

// UserRegistrationUseCase handles user registration
type UserRegistrationUseCase struct {
    userRepo   UserRepository
    roleRepo   RoleRepository
    hasher     PasswordHasher
    idGen      IDGenerator
}

// NewUserRegistrationUseCase creates a new user registration use case
func NewUserRegistrationUseCase(
    userRepo UserRepository,
    roleRepo RoleRepository,
    hasher PasswordHasher,
    idGen IDGenerator,
) *UserRegistrationUseCase {
    return &UserRegistrationUseCase{
        userRepo: userRepo,
        roleRepo: roleRepo,
        hasher:   hasher,
        idGen:    idGen,
    }
}

// Execute registers a new user
func (uc *UserRegistrationUseCase) Execute(ctx context.Context, req RegisterUserRequest) (*RegisterUserResponse, error) {
    // Validate password length
    if len(req.Password) < 8 {
        return nil, domain.ErrPasswordTooShort
    }

    // Check if user already exists
    existingUser, err := uc.userRepo.FindByEmail(ctx, req.Email)
    if err != nil && err != domain.ErrUserNotFound {
        return nil, err
    }
    if existingUser != nil {
        return nil, domain.ErrUserAlreadyExists
    }

    // Create new user
    user, err := domain.NewUser(req.Email, req.FirstName, req.LastName)
    if err != nil {
        return nil, err
    }

    // Generate unique ID
    user.ID = uc.idGen.Generate()

    // Hash password
    hashedPassword, err := uc.hasher.Hash(req.Password)
    if err != nil {
        return nil, err
    }
    user.PasswordHash = hashedPassword

    // Assign default role
    defaultRole, err := uc.roleRepo.FindByName(ctx, "user")
    if err != nil {
        return nil, err
    }
    user.RoleID = defaultRole.ID

    // Save user
    if err := uc.userRepo.Save(ctx, user); err != nil {
        return nil, err
    }

    return &RegisterUserResponse{
        UserID: user.ID,
        Email:  user.Email,
    }, nil
}

internal/usecase/user_authentication.go:

package usecase

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

// LoginRequest represents login input
type LoginRequest struct {
    Email    string
    Password string
}

// LoginResponse represents login output
type LoginResponse struct {
    Token  string
    UserID string
    Email  string
}

// UserAuthenticationUseCase handles user authentication
type UserAuthenticationUseCase struct {
    userRepo   UserRepository
    hasher     PasswordHasher
    tokenGen   TokenGenerator
}

// NewUserAuthenticationUseCase creates a new authentication use case
func NewUserAuthenticationUseCase(
    userRepo UserRepository,
    hasher PasswordHasher,
    tokenGen TokenGenerator,
) *UserAuthenticationUseCase {
    return &UserAuthenticationUseCase{
        userRepo: userRepo,
        hasher:   hasher,
        tokenGen: tokenGen,
    }
}

// Execute authenticates a user and returns a token
func (uc *UserAuthenticationUseCase) Execute(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
    // Find user by email
    user, err := uc.userRepo.FindByEmail(ctx, req.Email)
    if err != nil {
        if err == domain.ErrUserNotFound {
            return nil, domain.ErrInvalidCredentials
        }
        return nil, err
    }

    // Check if user is active
    if !user.IsActive {
        return nil, domain.ErrUserNotActive
    }

    // Verify password
    if err := uc.hasher.Compare(user.PasswordHash, req.Password); err != nil {
        return nil, domain.ErrInvalidCredentials
    }

    // Generate token
    token, err := uc.tokenGen.Generate(user.ID, user.Email)
    if err != nil {
        return nil, err
    }

    // Record login
    user.RecordLogin()
    if err := uc.userRepo.Update(ctx, user); err != nil {
        // Log error but don't fail the login
        // This is a non-critical operation
    }

    return &LoginResponse{
        Token:  token,
        UserID: user.ID,
        Email:  user.Email,
    }, nil
}

internal/usecase/user_profile.go:

package usecase

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

// GetUserProfileRequest represents get profile input
type GetUserProfileRequest struct {
    UserID        string
    RequestUserID string // User making the request
}

// UserProfile represents user profile output
type UserProfile struct {
    ID          string
    Email       string
    FirstName   string
    LastName    string
    FullName    string
    IsActive    bool
    Role        string
    CreatedAt   string
    LastLoginAt *string
}

// UpdateUserProfileRequest represents update profile input
type UpdateUserProfileRequest struct {
    UserID    string
    FirstName string
    LastName  string
}

// UserProfileUseCase handles user profile operations
type UserProfileUseCase struct {
    userRepo UserRepository
    roleRepo RoleRepository
}

// NewUserProfileUseCase creates a new user profile use case
func NewUserProfileUseCase(userRepo UserRepository, roleRepo RoleRepository) *UserProfileUseCase {
    return &UserProfileUseCase{
        userRepo: userRepo,
        roleRepo: roleRepo,
    }
}

// GetProfile retrieves a user's profile
func (uc *UserProfileUseCase) GetProfile(ctx context.Context, req GetUserProfileRequest) (*UserProfile, error) {
    // Find user
    user, err := uc.userRepo.FindByID(ctx, req.UserID)
    if err != nil {
        return nil, err
    }

    // Get role information
    role, err := uc.roleRepo.FindByID(ctx, user.RoleID)
    if err != nil {
        return nil, err
    }

    // Build profile
    profile := &UserProfile{
        ID:        user.ID,
        Email:     user.Email,
        FirstName: user.FirstName,
        LastName:  user.LastName,
        FullName:  user.FullName(),
        IsActive:  user.IsActive,
        Role:      role.Name,
        CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
    }

    if user.LastLoginAt != nil {
        lastLogin := user.LastLoginAt.Format("2006-01-02T15:04:05Z")
        profile.LastLoginAt = &lastLogin
    }

    return profile, nil
}

// UpdateProfile updates a user's profile
func (uc *UserProfileUseCase) UpdateProfile(ctx context.Context, req UpdateUserProfileRequest) error {
    // Find user
    user, err := uc.userRepo.FindByID(ctx, req.UserID)
    if err != nil {
        return err
    }

    // Update fields
    if req.FirstName != "" {
        user.FirstName = req.FirstName
    }
    if req.LastName != "" {
        user.LastName = req.LastName
    }

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

    // Save changes
    return uc.userRepo.Update(ctx, user)
}

Repository Layer (Interface Adapters)

internal/repository/postgres/user_repository.go:

package postgres

import (
    "context"
    "database/sql"
    "myapp/internal/domain"
    "time"
)

// UserRepository implements the UserRepository interface using PostgreSQL
type UserRepository struct {
    db *sql.DB
}

// NewUserRepository creates a new PostgreSQL user repository
func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{db: db}
}

// Save creates a new user in the database
func (r *UserRepository) Save(ctx context.Context, user *domain.User) error {
    query := `
        INSERT INTO users (id, email, password_hash, first_name, last_name, role_id, is_active, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
    `

    _, err := r.db.ExecContext(ctx, query,
        user.ID,
        user.Email,
        user.PasswordHash,
        user.FirstName,
        user.LastName,
        user.RoleID,
        user.IsActive,
        user.CreatedAt,
        user.UpdatedAt,
    )

    return err
}

// FindByID retrieves a user by ID
func (r *UserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
    query := `
        SELECT id, email, password_hash, first_name, last_name, role_id, is_active, 
               created_at, updated_at, last_login_at
        FROM users
        WHERE id = $1
    `

    user := &domain.User{}
    var lastLoginAt sql.NullTime

    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.Email,
        &user.PasswordHash,
        &user.FirstName,
        &user.LastName,
        &user.RoleID,
        &user.IsActive,
        &user.CreatedAt,
        &user.UpdatedAt,
        &lastLoginAt,
    )

    if err == sql.ErrNoRows {
        return nil, domain.ErrUserNotFound
    }
    if err != nil {
        return nil, err
    }

    if lastLoginAt.Valid {
        user.LastLoginAt = &lastLoginAt.Time
    }

    return user, nil
}

// FindByEmail retrieves a user by email
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
    query := `
        SELECT id, email, password_hash, first_name, last_name, role_id, is_active, 
               created_at, updated_at, last_login_at
        FROM users
        WHERE email = $1
    `

    user := &domain.User{}
    var lastLoginAt sql.NullTime

    err := r.db.QueryRowContext(ctx, query, email).Scan(
        &user.ID,
        &user.Email,
        &user.PasswordHash,
        &user.FirstName,
        &user.LastName,
        &user.RoleID,
        &user.IsActive,
        &user.CreatedAt,
        &user.UpdatedAt,
        &lastLoginAt,
    )

    if err == sql.ErrNoRows {
        return nil, domain.ErrUserNotFound
    }
    if err != nil {
        return nil, err
    }

    if lastLoginAt.Valid {
        user.LastLoginAt = &lastLoginAt.Time
    }

    return user, nil
}

// Update updates an existing user
func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
    query := `
        UPDATE users
        SET email = $2, password_hash = $3, first_name = $4, last_name = $5,
            role_id = $6, is_active = $7, updated_at = $8, last_login_at = $9
        WHERE id = $1
    `

    user.UpdatedAt = time.Now()

    _, err := r.db.ExecContext(ctx, query,
        user.ID,
        user.Email,
        user.PasswordHash,
        user.FirstName,
        user.LastName,
        user.RoleID,
        user.IsActive,
        user.UpdatedAt,
        user.LastLoginAt,
    )

    return err
}

// Delete removes a user from the database
func (r *UserRepository) Delete(ctx context.Context, id string) error {
    query := `DELETE FROM users WHERE id = $1`
    _, err := r.db.ExecContext(ctx, query, id)
    return err
}

// List retrieves a paginated list of users
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]*domain.User, error) {
    query := `
        SELECT id, email, password_hash, first_name, last_name, role_id, is_active,
               created_at, updated_at, last_login_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, err
    }
    defer rows.Close()

    var users []*domain.User
    for rows.Next() {
        user := &domain.User{}
        var lastLoginAt sql.NullTime

        err := rows.Scan(
            &user.ID,
            &user.Email,
            &user.PasswordHash,
            &user.FirstName,
            &user.LastName,
            &user.RoleID,
            &user.IsActive,
            &user.CreatedAt,
            &user.UpdatedAt,
            &lastLoginAt,
        )
        if err != nil {
            return nil, err
        }

        if lastLoginAt.Valid {
            user.LastLoginAt = &lastLoginAt.Time
        }

        users = append(users, user)
    }

    return users, rows.Err()
}

internal/repository/postgres/role_repository.go:

package postgres

import (
    "context"
    "database/sql"
    "encoding/json"
    "myapp/internal/domain"
)

// RoleRepository implements the RoleRepository interface using PostgreSQL
type RoleRepository struct {
    db *sql.DB
}

// NewRoleRepository creates a new PostgreSQL role repository
func NewRoleRepository(db *sql.DB) *RoleRepository {
    return &RoleRepository{db: db}
}

// FindByID retrieves a role by ID
func (r *RoleRepository) FindByID(ctx context.Context, id string) (*domain.Role, error) {
    query := `
        SELECT id, name, description, permissions, created_at, updated_at
        FROM roles
        WHERE id = $1
    `

    role := &domain.Role{}
    var permissionsJSON []byte

    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &role.ID,
        &role.Name,
        &role.Description,
        &permissionsJSON,
        &role.CreatedAt,
        &role.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, domain.ErrRoleNotFound
    }
    if err != nil {
        return nil, err
    }

    if err := json.Unmarshal(permissionsJSON, &role.Permissions); err != nil {
        return nil, err
    }

    return role, nil
}

// FindByName retrieves a role by name
func (r *RoleRepository) FindByName(ctx context.Context, name string) (*domain.Role, error) {
    query := `
        SELECT id, name, description, permissions, created_at, updated_at
        FROM roles
        WHERE name = $1
    `

    role := &domain.Role{}
    var permissionsJSON []byte

    err := r.db.QueryRowContext(ctx, query, name).Scan(
        &role.ID,
        &role.Name,
        &role.Description,
        &permissionsJSON,
        &role.CreatedAt,
        &role.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, domain.ErrRoleNotFound
    }
    if err != nil {
        return nil, err
    }

    if err := json.Unmarshal(permissionsJSON, &role.Permissions); err != nil {
        return nil, err
    }

    return role, nil
}

// List retrieves all roles
func (r *RoleRepository) List(ctx context.Context) ([]*domain.Role, error) {
    query := `
        SELECT id, name, description, permissions, created_at, updated_at
        FROM roles
        ORDER BY name
    `

    rows, err := r.db.QueryContext(ctx, query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var roles []*domain.Role
    for rows.Next() {
        role := &domain.Role{}
        var permissionsJSON []byte

        err := rows.Scan(
            &role.ID,
            &role.Name,
            &role.Description,
            &permissionsJSON,
            &role.CreatedAt,
            &role.UpdatedAt,
        )
        if err != nil {
            return nil, err
        }

        if err := json.Unmarshal(permissionsJSON, &role.Permissions); err != nil {
            return nil, err
        }

        roles = append(roles, role)
    }

    return roles, rows.Err()
}

Security Layer (Interface Adapters)

internal/security/hasher.go:

package security

import (
    "golang.org/x/crypto/bcrypt"
)

// BcryptHasher implements password hashing using bcrypt
type BcryptHasher struct {
    cost int
}

// NewBcryptHasher creates a new bcrypt hasher
func NewBcryptHasher() *BcryptHasher {
    return &BcryptHasher{
        cost: bcrypt.DefaultCost,
    }
}

// Hash generates a bcrypt hash of the password
func (h *BcryptHasher) Hash(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), h.cost)
    return string(bytes), err
}

// Compare compares a password with a hash
func (h *BcryptHasher) Compare(hashedPassword, password string) error {
    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

internal/security/jwt.go:

package security

import (
    "errors"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

var (
    ErrInvalidToken = errors.New("invalid token")
    ErrExpiredToken = errors.New("token has expired")
)

// Claims represents JWT claims
type Claims struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
    jwt.RegisteredClaims
}

// JWTGenerator implements token generation using JWT
type JWTGenerator struct {
    secretKey     []byte
    tokenDuration time.Duration
}

// NewJWTGenerator creates a new JWT generator
func NewJWTGenerator(secretKey string, tokenDuration time.Duration) *JWTGenerator {
    return &JWTGenerator{
        secretKey:     []byte(secretKey),
        tokenDuration: tokenDuration,
    }
}

// Generate creates a new JWT token
func (g *JWTGenerator) Generate(userID, email string) (string, error) {
    claims := &Claims{
        UserID: userID,
        Email:  email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(g.tokenDuration)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(g.secretKey)
}

// Validate validates a JWT token and returns the user ID
func (g *JWTGenerator) Validate(tokenString string) (string, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return g.secretKey, nil
    })

    if err != nil {
        if errors.Is(err, jwt.ErrTokenExpired) {
            return "", ErrExpiredToken
        }
        return "", ErrInvalidToken
    }

    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims.UserID, nil
    }

    return "", ErrInvalidToken
}

internal/security/id_generator.go:

package security

import "github.com/google/uuid"

// UUIDGenerator implements ID generation using UUID
type UUIDGenerator struct{}

// NewUUIDGenerator creates a new UUID generator
func NewUUIDGenerator() *UUIDGenerator {
    return &UUIDGenerator{}
}

// Generate creates a new UUID
func (g *UUIDGenerator) Generate() string {
    return uuid.New().String()
}

HTTP Handler Layer (Interface Adapters)

internal/handler/dto/user_dto.go:

package dto

// RegisterUserRequest represents user registration HTTP request
type RegisterUserRequest struct {
    Email     string `json:"email" validate:"required,email"`
    Password  string `json:"password" validate:"required,min=8"`
    FirstName string `json:"first_name" validate:"required"`
    LastName  string `json:"last_name" validate:"required"`
}

// RegisterUserResponse represents user registration HTTP response
type RegisterUserResponse struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
}

// LoginRequest represents login HTTP request
type LoginRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

// LoginResponse represents login HTTP response
type LoginResponse struct {
    Token  string `json:"token"`
    UserID string `json:"user_id"`
    Email  string `json:"email"`
}

// UpdateProfileRequest represents update profile HTTP request
type UpdateProfileRequest struct {
    FirstName string `json:"first_name,omitempty"`
    LastName  string `json:"last_name,omitempty"`
}

// UserProfileResponse represents user profile HTTP response
type UserProfileResponse struct {
    ID          string  `json:"id"`
    Email       string  `json:"email"`
    FirstName   string  `json:"first_name"`
    LastName    string  `json:"last_name"`
    FullName    string  `json:"full_name"`
    IsActive    bool    `json:"is_active"`
    Role        string  `json:"role"`
    CreatedAt   string  `json:"created_at"`
    LastLoginAt *string `json:"last_login_at,omitempty"`
}

// ErrorResponse represents an error HTTP response
type ErrorResponse struct {
    Error   string `json:"error"`
    Message string `json:"message"`
}

internal/handler/http/user_handler.go:

package http

import (
    "encoding/json"
    "myapp/internal/handler/dto"
    "myapp/internal/usecase"
    "net/http"

    "github.com/go-playground/validator/v10"
)

// UserHandler handles user-related HTTP requests
type UserHandler struct {
    registerUC *usecase.UserRegistrationUseCase
    loginUC    *usecase.UserAuthenticationUseCase
    profileUC  *usecase.UserProfileUseCase
    validator  *validator.Validate
}

// NewUserHandler creates a new user handler
func NewUserHandler(
    registerUC *usecase.UserRegistrationUseCase,
    loginUC *usecase.UserAuthenticationUseCase,
    profileUC *usecase.UserProfileUseCase,
) *UserHandler {
    return &UserHandler{
        registerUC: registerUC,
        loginUC:    loginUC,
        profileUC:  profileUC,
        validator:  validator.New(),
    }
}

// Register handles user registration
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
    var req dto.RegisterUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request body", err)
        return
    }

    if err := h.validator.Struct(req); err != nil {
        respondError(w, http.StatusBadRequest, "Validation failed", err)
        return
    }

    ucReq := usecase.RegisterUserRequest{
        Email:     req.Email,
        Password:  req.Password,
        FirstName: req.FirstName,
        LastName:  req.LastName,
    }

    result, err := h.registerUC.Execute(r.Context(), ucReq)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Registration failed", err)
        return
    }

    response := dto.RegisterUserResponse{
        UserID: result.UserID,
        Email:  result.Email,
    }

    respondJSON(w, http.StatusCreated, response)
}

// Login handles user login
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
    var req dto.LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request body", err)
        return
    }

    if err := h.validator.Struct(req); err != nil {
        respondError(w, http.StatusBadRequest, "Validation failed", err)
        return
    }

    ucReq := usecase.LoginRequest{
        Email:    req.Email,
        Password: req.Password,
    }

    result, err := h.loginUC.Execute(r.Context(), ucReq)
    if err != nil {
        respondError(w, http.StatusUnauthorized, "Login failed", err)
        return
    }

    response := dto.LoginResponse{
        Token:  result.Token,
        UserID: result.UserID,
        Email:  result.Email,
    }

    respondJSON(w, http.StatusOK, response)
}

// GetProfile handles retrieving user profile
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value("user_id").(string)

    ucReq := usecase.GetUserProfileRequest{
        UserID:        userID,
        RequestUserID: userID,
    }

    profile, err := h.profileUC.GetProfile(r.Context(), ucReq)
    if err != nil {
        respondError(w, http.StatusNotFound, "Profile not found", err)
        return
    }

    response := dto.UserProfileResponse{
        ID:          profile.ID,
        Email:       profile.Email,
        FirstName:   profile.FirstName,
        LastName:    profile.LastName,
        FullName:    profile.FullName,
        IsActive:    profile.IsActive,
        Role:        profile.Role,
        CreatedAt:   profile.CreatedAt,
        LastLoginAt: profile.LastLoginAt,
    }

    respondJSON(w, http.StatusOK, response)
}

// UpdateProfile handles updating user profile
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value("user_id").(string)

    var req dto.UpdateProfileRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request body", err)
        return
    }

    ucReq := usecase.UpdateUserProfileRequest{
        UserID:    userID,
        FirstName: req.FirstName,
        LastName:  req.LastName,
    }

    if err := h.profileUC.UpdateProfile(r.Context(), ucReq); err != nil {
        respondError(w, http.StatusBadRequest, "Update failed", err)
        return
    }

    respondJSON(w, http.StatusOK, map[string]string{"message": "Profile updated successfully"})
}

// Helper functions
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func respondError(w http.ResponseWriter, status int, message string, err error) {
    response := dto.ErrorResponse{
        Error:   message,
        Message: err.Error(),
    }
    respondJSON(w, status, response)
}

internal/handler/http/middleware.go:

package http

import (
    "context"
    "myapp/internal/usecase"
    "net/http"
    "strings"
)

// AuthMiddleware creates an authentication middleware
func AuthMiddleware(tokenGen usecase.TokenGenerator) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                respondError(w, http.StatusUnauthorized, "Missing authorization header", nil)
                return
            }

            parts := strings.Split(authHeader, " ")
            if len(parts) != 2 || parts[0] != "Bearer" {
                respondError(w, http.StatusUnauthorized, "Invalid authorization header", nil)
                return
            }

            token := parts[1]
            userID, err := tokenGen.Validate(token)
            if err != nil {
                respondError(w, http.StatusUnauthorized, "Invalid token", err)
                return
            }

            // Add user ID to context
            ctx := context.WithValue(r.Context(), "user_id", userID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// CORSMiddleware adds CORS headers
func CORSMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(w, r)
    })
}

// LoggingMiddleware logs HTTP requests
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Simple logging - in production, use a proper logger
        println(r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

Main Application (Frameworks & Drivers)

cmd/api/main.go:

package main

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

    "myapp/internal/handler/http"
    "myapp/internal/repository/postgres"
    "myapp/internal/security"
    "myapp/internal/usecase"

    "github.com/gorilla/mux"
    _ "github.com/lib/pq"
)

func main() {
    // Load configuration
    dbURL := os.Getenv("DATABASE_URL")
    if dbURL == "" {
        dbURL = "postgres://user:password@localhost:5432/userdb?sslmode=disable"
    }

    jwtSecret := os.Getenv("JWT_SECRET")
    if jwtSecret == "" {
        jwtSecret = "your-secret-key-change-in-production"
    }

    // Initialize database
    db, err := sql.Open("postgres", dbURL)
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }
    defer db.Close()

    if err := db.Ping(); err != nil {
        log.Fatal("Failed to ping database:", err)
    }

    // Initialize repositories
    userRepo := postgres.NewUserRepository(db)
    roleRepo := postgres.NewRoleRepository(db)

    // Initialize security services
    hasher := security.NewBcryptHasher()
    tokenGen := security.NewJWTGenerator(jwtSecret, 24*time.Hour)
    idGen := security.NewUUIDGenerator()

    // Initialize use cases
    registerUC := usecase.NewUserRegistrationUseCase(userRepo, roleRepo, hasher, idGen)
    loginUC := usecase.NewUserAuthenticationUseCase(userRepo, hasher, tokenGen)
    profileUC := usecase.NewUserProfileUseCase(userRepo, roleRepo)

    // Initialize handlers
    userHandler := httpHandler.NewUserHandler(registerUC, loginUC, profileUC)

    // Setup router
    router := mux.NewRouter()

    // Public routes
    router.HandleFunc("/api/register", userHandler.Register).Methods("POST")
    router.HandleFunc("/api/login", userHandler.Login).Methods("POST")

    // Protected routes
    protected := router.PathPrefix("/api").Subrouter()
    protected.Use(httpHandler.AuthMiddleware(tokenGen))
    protected.HandleFunc("/profile", userHandler.GetProfile).Methods("GET")
    protected.HandleFunc("/profile", userHandler.UpdateProfile).Methods("PUT")

    // Apply global middleware
    router.Use(httpHandler.CORSMiddleware)
    router.Use(httpHandler.LoggingMiddleware)

    // Start server
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    log.Printf("Server starting on port %s", port)
    if err := http.ListenAndServe(":"+port, router); err != nil {
        log.Fatal("Server failed to start:", err)
    }
}

Database Migrations

migrations/001_create_users_table.sql:

CREATE TABLE IF NOT EXISTS users (
    id VARCHAR(36) PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    role_id VARCHAR(36) NOT NULL,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    last_login_at TIMESTAMP,
    
    INDEX idx_email (email),
    INDEX idx_role_id (role_id)
);

migrations/002_create_roles_table.sql:

CREATE TABLE IF NOT EXISTS roles (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL,
    description TEXT,
    permissions JSONB NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Insert default roles
INSERT INTO roles (id, name, description, permissions) VALUES
('admin', 'Administrator', 'Full system access', '["users:read", "users:write", "users:delete", "roles:manage"]'),
('user', 'User', 'Basic user access', '["users:read"]')
ON CONFLICT (id) DO NOTHING;

Testing Strategy {#testing}

Unit Testing

Domain layer tests (test only business logic):

package domain_test

import (
    "myapp/internal/domain"
    "testing"
)

func TestUser_Validate(t *testing.T) {
    tests := []struct {
        name    string
        user    *domain.User
        wantErr error
    }{
        {
            name: "valid user",
            user: &domain.User{
                Email:     "[email protected]",
                FirstName: "John",
                LastName:  "Doe",
            },
            wantErr: nil,
        },
        {
            name: "invalid email",
            user: &domain.User{
                Email:     "invalid-email",
                FirstName: "John",
                LastName:  "Doe",
            },
            wantErr: domain.ErrInvalidEmail,
        },
        {
            name: "missing first name",
            user: &domain.User{
                Email:    "[email protected]",
                LastName: "Doe",
            },
            wantErr: errors.New("first name and last name are required"),
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.user.Validate()
            if err != nil && tt.wantErr == nil {
                t.Errorf("expected no error, got %v", err)
            }
            if err == nil && tt.wantErr != nil {
                t.Errorf("expected error %v, got nil", tt.wantErr)
            }
        })
    }
}

func TestUser_FullName(t *testing.T) {
    user := &domain.User{
        FirstName: "John",
        LastName:  "Doe",
    }

    expected := "John Doe"
    if got := user.FullName(); got != expected {
        t.Errorf("FullName() = %v, want %v", got, expected)
    }
}

Use case tests with mocks:

package usecase_test

import (
    "context"
    "myapp/internal/domain"
    "myapp/internal/usecase"
    "testing"
)

// Mock repository
type mockUserRepository struct {
    saveFn        func(ctx context.Context, user *domain.User) error
    findByEmailFn func(ctx context.Context, email string) (*domain.User, error)
}

func (m *mockUserRepository) Save(ctx context.Context, user *domain.User) error {
    return m.saveFn(ctx, user)
}

func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
    return m.findByEmailFn(ctx, email)
}

func (m *mockUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
    return nil, nil
}

func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) error {
    return nil
}

func (m *mockUserRepository) Delete(ctx context.Context, id string) error {
    return nil
}

func (m *mockUserRepository) List(ctx context.Context, limit, offset int) ([]*domain.User, error) {
    return nil, nil
}

// Mock role repository
type mockRoleRepository struct {
    findByNameFn func(ctx context.Context, name string) (*domain.Role, error)
}

func (m *mockRoleRepository) FindByName(ctx context.Context, name string) (*domain.Role, error) {
    return m.findByNameFn(ctx, name)
}

func (m *mockRoleRepository) FindByID(ctx context.Context, id string) (*domain.Role, error) {
    return nil, nil
}

func (m *mockRoleRepository) List(ctx context.Context) ([]*domain.Role, error) {
    return nil, nil
}

// Mock hasher
type mockHasher struct {
    hashFn func(password string) (string, error)
}

func (m *mockHasher) Hash(password string) (string, error) {
    return m.hashFn(password)
}

func (m *mockHasher) Compare(hashedPassword, password string) error {
    return nil
}

// Mock ID generator
type mockIDGenerator struct{}

func (m *mockIDGenerator) Generate() string {
    return "test-id-123"
}

func TestUserRegistrationUseCase_Execute(t *testing.T) {
    ctx := context.Background()

    t.Run("successful registration", func(t *testing.T) {
        userRepo := &mockUserRepository{
            findByEmailFn: func(ctx context.Context, email string) (*domain.User, error) {
                return nil, domain.ErrUserNotFound
            },
            saveFn: func(ctx context.Context, user *domain.User) error {
                return nil
            },
        }

        roleRepo := &mockRoleRepository{
            findByNameFn: func(ctx context.Context, name string) (*domain.Role, error) {
                return domain.RoleUser, nil
            },
        }

        hasher := &mockHasher{
            hashFn: func(password string) (string, error) {
                return "hashed_" + password, nil
            },
        }

        idGen := &mockIDGenerator{}

        uc := usecase.NewUserRegistrationUseCase(userRepo, roleRepo, hasher, idGen)

        req := usecase.RegisterUserRequest{
            Email:     "[email protected]",
            Password:  "password123",
            FirstName: "John",
            LastName:  "Doe",
        }

        result, err := uc.Execute(ctx, req)
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }

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

    t.Run("user already exists", func(t *testing.T) {
        userRepo := &mockUserRepository{
            findByEmailFn: func(ctx context.Context, email string) (*domain.User, error) {
                return &domain.User{Email: email}, nil
            },
        }

        roleRepo := &mockRoleRepository{}
        hasher := &mockHasher{}
        idGen := &mockIDGenerator{}

        uc := usecase.NewUserRegistrationUseCase(userRepo, roleRepo, hasher, idGen)

        req := usecase.RegisterUserRequest{
            Email:     "[email protected]",
            Password:  "password123",
            FirstName: "John",
            LastName:  "Doe",
        }

        _, err := uc.Execute(ctx, req)
        if err != domain.ErrUserAlreadyExists {
            t.Errorf("expected ErrUserAlreadyExists, got %v", err)
        }
    })
}

Integration Testing

package integration_test

import (
    "context"
    "database/sql"
    "myapp/internal/domain"
    "myapp/internal/repository/postgres"
    "testing"

    _ "github.com/lib/pq"
)

func setupTestDB(t *testing.T) *sql.DB {
    db, err := sql.Open("postgres", "postgres://test:test@localhost:5432/testdb?sslmode=disable")
    if err != nil {
        t.Fatal(err)
    }

    // Run migrations
    _, err = db.Exec(`
        CREATE TABLE IF NOT EXISTS users (
            id VARCHAR(36) PRIMARY KEY,
            email VARCHAR(255) UNIQUE NOT NULL,
            password_hash VARCHAR(255) NOT NULL,
            first_name VARCHAR(100) NOT NULL,
            last_name VARCHAR(100) NOT NULL,
            role_id VARCHAR(36) NOT NULL,
            is_active BOOLEAN DEFAULT true,
            created_at TIMESTAMP NOT NULL,
            updated_at TIMESTAMP NOT NULL,
            last_login_at TIMESTAMP
        )
    `)
    if err != nil {
        t.Fatal(err)
    }

    return db
}

func teardownTestDB(t *testing.T, db *sql.DB) {
    _, err := db.Exec("DROP TABLE IF EXISTS users")
    if err != nil {
        t.Fatal(err)
    }
    db.Close()
}

func TestPostgresUserRepository_Integration(t *testing.T) {
    db := setupTestDB(t)
    defer teardownTestDB(t, db)

    repo := postgres.NewUserRepository(db)
    ctx := context.Background()

    t.Run("save and find user", func(t *testing.T) {
        user := &domain.User{
            ID:           "test-123",
            Email:        "[email protected]",
            PasswordHash: "hashed",
            FirstName:    "John",
            LastName:     "Doe",
            RoleID:       "user",
            IsActive:     true,
            CreatedAt:    time.Now(),
            UpdatedAt:    time.Now(),
        }

        err := repo.Save(ctx, user)
        if err != nil {
            t.Fatalf("failed to save user: %v", err)
        }

        found, err := repo.FindByEmail(ctx, user.Email)
        if err != nil {
            t.Fatalf("failed to find user: %v", err)
        }

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

End-to-End Testing

package e2e_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "myapp/internal/handler/dto"
)

func TestUserRegistrationFlow(t *testing.T) {
    // Setup test server
    server := setupTestServer(t)
    defer server.Close()

    // Register user
    regReq := dto.RegisterUserRequest{
        Email:     "[email protected]",
        Password:  "password123",
        FirstName: "John",
        LastName:  "Doe",
    }

    regBody, _ := json.Marshal(regReq)
    resp, err := http.Post(server.URL+"/api/register", "application/json", bytes.NewBuffer(regBody))
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        t.Errorf("expected status 201, got %d", resp.StatusCode)
    }

    var regResp dto.RegisterUserResponse
    json.NewDecoder(resp.Body).Decode(&regResp)

    // Login
    loginReq := dto.LoginRequest{
        Email:    "[email protected]",
        Password: "password123",
    }

    loginBody, _ := json.Marshal(loginReq)
    resp2, err := http.Post(server.URL+"/api/login", "application/json", bytes.NewBuffer(loginBody))
    if err != nil {
        t.Fatal(err)
    }
    defer resp2.Body.Close()

    if resp2.StatusCode != http.StatusOK {
        t.Errorf("expected status 200, got %d", resp2.StatusCode)
    }

    var loginResp dto.LoginResponse
    json.NewDecoder(resp2.Body).Decode(&loginResp)

    if loginResp.Token == "" {
        t.Error("expected token, got empty string")
    }

    // Get profile with token
    req, _ := http.NewRequest("GET", server.URL+"/api/profile", nil)
    req.Header.Set("Authorization", "Bearer "+loginResp.Token)

    resp3, err := http.DefaultClient.Do(req)
    if err != nil {
        t.Fatal(err)
    }
    defer resp3.Body.Close()

    if resp3.StatusCode != http.StatusOK {
        t.Errorf("expected status 200, got %d", resp3.StatusCode)
    }
}

Common Patterns and Practices {#patterns}

1. Repository Pattern

Already demonstrated above. Key points:

  • Define interface in use case layer
  • Implement in repository layer
  • Use dependency injection

2. Factory Pattern

package factory

import (
    "myapp/internal/domain"
    "myapp/internal/security"
)

// UserFactory creates users with defaults
type UserFactory struct {
    idGen      security.IDGenerator
    defaultRole string
}

func NewUserFactory(idGen security.IDGenerator) *UserFactory {
    return &UserFactory{
        idGen:      idGen,
        defaultRole: "user",
    }
}

func (f *UserFactory) CreateUser(email, firstName, lastName string) (*domain.User, error) {
    user, err := domain.NewUser(email, firstName, lastName)
    if err != nil {
        return nil, err
    }

    user.ID = f.idGen.Generate()
    user.RoleID = f.defaultRole
    user.IsActive = true

    return user, nil
}

3. Specification Pattern

package specification

import "myapp/internal/domain"

// UserSpecification defines a specification for users
type UserSpecification interface {
    IsSatisfiedBy(user *domain.User) bool
}

// ActiveUserSpec checks if user is active
type ActiveUserSpec struct{}

func (s *ActiveUserSpec) IsSatisfiedBy(user *domain.User) bool {
    return user.IsActive
}

// EmailDomainSpec checks if user email has specific domain
type EmailDomainSpec struct {
    domain string
}

func (s *EmailDomainSpec) IsSatisfiedBy(user *domain.User) bool {
    return strings.HasSuffix(user.Email, "@"+s.domain)
}

// AndSpec combines multiple specifications
type AndSpec struct {
    specs []UserSpecification
}

func (s *AndSpec) IsSatisfiedBy(user *domain.User) bool {
    for _, spec := range s.specs {
        if !spec.IsSatisfiedBy(user) {
            return false
        }
    }
    return true
}

4. Event-Driven Architecture

package events

import "myapp/internal/domain"

// Event represents a domain event
type Event interface {
    EventName() string
}

// UserRegisteredEvent is emitted when a user registers
type UserRegisteredEvent struct {
    User *domain.User
}

func (e *UserRegisteredEvent) EventName() string {
    return "user.registered"
}

// EventPublisher publishes events
type EventPublisher interface {
    Publish(event Event) error
}

// EventHandler handles events
type EventHandler interface {
    Handle(event Event) error
}

// EventBus manages event publishing and handling
type EventBus struct {
    handlers map[string][]EventHandler
}

func NewEventBus() *EventBus {
    return &EventBus{
        handlers: make(map[string][]EventHandler),
    }
}

func (b *EventBus) Subscribe(eventName string, handler EventHandler) {
    b.handlers[eventName] = append(b.handlers[eventName], handler)
}

func (b *EventBus) Publish(event Event) error {
    handlers, ok := b.handlers[event.EventName()]
    if !ok {
        return nil
    }

    for _, handler := range handlers {
        if err := handler.Handle(event); err != nil {
            return err
        }
    }

    return nil
}

5. CQRS (Command Query Responsibility Segregation)

// Command side
type CreateUserCommand struct {
    Email     string
    Password  string
    FirstName string
    LastName  string
}

type CreateUserCommandHandler struct {
    userRepo UserRepository
    hasher   PasswordHasher
}

func (h *CreateUserCommandHandler) Handle(ctx context.Context, cmd CreateUserCommand) error {
    // Handle command
    return nil
}

// Query side
type GetUserQuery struct {
    UserID string
}

type GetUserQueryHandler struct {
    userRepo UserRepository
}

func (h *GetUserQueryHandler) Handle(ctx context.Context, query GetUserQuery) (*domain.User, error) {
    return h.userRepo.FindByID(ctx, query.UserID)
}

Pitfalls and How to Avoid Them {#pitfalls}

1. Leaking Domain Logic to Handlers

❌ Bad:

func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
    // Validation in handler
    if len(req.Password) < 8 {
        respondError(w, 400, "Password too short")
        return
    }

    // Business logic in handler
    user := &domain.User{
        Email:    req.Email,
        Password: hashPassword(req.Password),
    }
    h.db.Save(user)
}

✅ Good:

func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
    // Handler only translates HTTP to domain
    ucReq := usecase.RegisterUserRequest{
        Email:    req.Email,
        Password: req.Password,
    }
    result, err := h.registerUC.Execute(r.Context(), ucReq)
    // Handle result
}

2. Use Cases Depending on Frameworks

❌ Bad:

type CreateUserUseCase struct {
    db *gorm.DB  // Framework dependency
}

✅ Good:

type CreateUserUseCase struct {
    repo UserRepository  // Interface dependency
}

3. Fat Entities

❌ Bad:

type User struct {
    // ... many fields
    OrderHistory    []Order
    PaymentMethods  []PaymentMethod
    Preferences     UserPreferences
    // Becomes god object
}

func (u *User) PlaceOrder() { /* complex logic */ }
func (u *User) ProcessPayment() { /* complex logic */ }

✅ Good:

// Keep entities focused
type User struct {
    ID        string
    Email     string
    // Only user-specific fields
}

// Separate concerns into different aggregates
type Order struct { /* ... */ }
type Payment struct { /* ... */ }

// Use services for cross-aggregate operations
type OrderService struct {
    userRepo  UserRepository
    orderRepo OrderRepository
}

4. Anemic Domain Model

❌ Bad:

type User struct {
    ID    string
    Email string
    // No behavior, just data
}

// All logic in services
type UserService struct {
    repo UserRepository
}

func (s *UserService) ValidateUser(user *User) error {
    // Should be in domain
}

✅ Good:

type User struct {
    ID    string
    Email string
}

// Behavior in domain
func (u *User) Validate() error {
    // Business rules here
}

func (u *User) IsActive() bool {
    // Business logic here
}

5. Circular Dependencies

❌ Bad:

domain → usecase → domain  # Circular!

✅ Good:

handlers → usecase → domain

         repository

6. Testing Without Interfaces

❌ Bad:

type UserService struct {
    repo *PostgresUserRepository  // Concrete type
}

✅ Good:

type UserService struct {
    repo UserRepository  // Interface - easy to mock
}

Real-World Considerations {#real-world}

Performance Optimization

Caching layer:

type CachedUserRepository struct {
    repo  UserRepository
    cache Cache
}

func (r *CachedUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
    // Try cache first
    if user, ok := r.cache.Get("user:" + id); ok {
        return user.(*domain.User), nil
    }

    // Fall back to repository
    user, err := r.repo.FindByID(ctx, id)
    if err != nil {
        return nil, err
    }

    // Cache result
    r.cache.Set("user:"+id, user, 5*time.Minute)
    return user, nil
}

Error Handling

Structured errors:

package errors

type AppError struct {
    Code    string
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return e.Message + ": " + e.Err.Error()
    }
    return e.Message
}

func NewValidationError(msg string) *AppError {
    return &AppError{Code: "VALIDATION_ERROR", Message: msg}
}

func NewNotFoundError(msg string) *AppError {
    return &AppError{Code: "NOT_FOUND", Message: msg}
}

Logging and Observability

type LoggingUserRepository struct {
    repo   UserRepository
    logger Logger
}

func (r *LoggingUserRepository) Save(ctx context.Context, user *domain.User) error {
    r.logger.Info("Saving user", "email", user.Email)
    start := time.Now()
    
    err := r.repo.Save(ctx, user)
    
    duration := time.Since(start)
    if err != nil {
        r.logger.Error("Failed to save user", "error", err, "duration", duration)
        return err
    }
    
    r.logger.Info("User saved successfully", "duration", duration)
    return nil
}

Configuration Management

package config

import "os"

type Config struct {
    Database DatabaseConfig
    Server   ServerConfig
    JWT      JWTConfig
}

type DatabaseConfig struct {
    URL             string
    MaxConnections  int
    ConnMaxLifetime time.Duration
}

type ServerConfig struct {
    Port         string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

type JWTConfig struct {
    Secret   string
    Duration time.Duration
}

func Load() (*Config, error) {
    return &Config{
        Database: DatabaseConfig{
            URL:             os.Getenv("DATABASE_URL"),
            MaxConnections:  25,
            ConnMaxLifetime: 5 * time.Minute,
        },
        Server: ServerConfig{
            Port:         os.Getenv("PORT"),
            ReadTimeout:  10 * time.Second,
            WriteTimeout: 10 * time.Second,
        },
        JWT: JWTConfig{
            Secret:   os.Getenv("JWT_SECRET"),
            Duration: 24 * time.Hour,
        },
    }, nil
}

Graceful Shutdown

func main() {
    // ... setup code ...

    server := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    // Start server in goroutine
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    log.Println("Server exited")
}

Conclusion

Clean Architecture in Go provides:

  • Testability: Every layer can be tested independently
  • Maintainability: Changes are isolated to specific layers
  • Flexibility: Easy to swap implementations
  • Scalability: Clear boundaries support team growth
  • Longevity: Business logic survives framework changes

Key Takeaways:

  1. Dependencies point inward (Dependency Rule)
  2. Entities contain business logic, not frameworks
  3. Use cases orchestrate business workflows
  4. Interfaces define contracts, implementations are details
  5. Keep frameworks and databases at the edges

Remember: Clean Architecture is about creating sustainable systems. The initial overhead pays dividends as your project grows and evolves.


Further Reading:

  • Clean Architecture by Robert C. Martin
  • Domain-Driven Design by Eric Evans
  • Implementing Domain-Driven Design by Vaughn Vernon
  • Go best practices: https://golang.org/doc/effective_go

Get the Complete Guide: The Complete Guide to Clean Architecture

Prefer to read offline? Get the complete PDF, ePub, and source code bundle.

Buy Now for $19

Includes PDF, ePub, and full source code bundle. Payments securely processed via Gumroad.