Clean Architecture Patterns

Production engineering guide for clean architecture patterns covering patterns, implementation strategies, and operational best practices.

Clean architecture is a structural approach that separates business logic from technical implementation details, enabling systems to remain maintainable and adaptable as requirements evolve. This pattern becomes essential when building systems where business rules must outlast technical decisions, or when teams need to swap components without disrupting core logic.

Organize Code by Layer and Responsibility

Define Core Business Logic in Isolation

Business entities should contain only domain-specific data and behavior, with no dependencies on external frameworks or databases. The following example shows an entity that encapsulates business rules without technical concerns:

type User struct {
    ID       int
    Name     string
    Email    string
    IsActive bool
}

func (u *User) Validate() error {
    if u.Name == "" {
        return errors.New("name is required")
    }
    if u.Email == "" {
        return errors.New("email is required")
    }
    return nil
}

Create Interfaces for External Dependencies

Dependencies like databases or external services must be abstracted through interfaces defined in the core layer. These interfaces are implemented by infrastructure code:

type UserRepository interface {
    Save(user *User) error
    FindByID(id int) (*User, error)
    FindByEmail(email string) (*User, error)
}

Implement Infrastructure Details in Separate Layers

Infrastructure code depends on the core interfaces but provides concrete implementations. This layer handles database connections, HTTP clients, file systems, or external API integrations:

type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) Save(user *User) error {
    _, err := r.db.Exec("INSERT INTO users (name, email, is_active) VALUES ($1, $2, $3)", 
        user.Name, user.Email, user.IsActive)
    return err
}

Separate Business Rules from Technical Implementation

Use Dependency Injection to Connect Layers

The application layer orchestrates business operations using core entities and interfaces, without knowing how concrete implementations are provided:

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) CreateUser(name, email string) (*User, error) {
    user := &User{Name: name, Email: email, IsActive: true}
    if err := user.Validate(); err != nil {
        return nil, err
    }
    if err := s.repo.Save(user); err != nil {
        return nil, err
    }
    return user, nil
}

Maintain Layered Dependency Direction

Dependencies flow inward from outer layers to inner layers. The core layer cannot depend on infrastructure or presentation:

Presentation Layer (HTTP handlers)
      ↓
Application Layer (Use cases, services)
      ↓
Core Layer (Entities, interfaces, business rules)
      ↓
Infrastructure Layer (Database drivers, external clients)

Implement Input/Output Boundaries

Define Clear Data Transfer Objects

Data passed between layers should be explicitly defined to avoid coupling through implementation details:

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserResponse struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    IsActive bool   `json:"is_active"`
}

Control How Data Flows Through the System

Avoid passing domain entities directly between layers. Convert them to appropriate DTOs:

func (s *UserService) HandleCreateUser(req CreateUserRequest) (*CreateUserResponse, error) {
    user := &User{Name: req.Name, Email: req.Email, IsActive: true}
    if err := user.Validate(); err != nil {
        return nil, err
    }
    
    if err := s.repo.Save(user); err != nil {
        return nil, err
    }
    
    return &CreateUserResponse{
        ID:       user.ID,
        Name:     user.Name,
        Email:    user.Email,
        IsActive: user.IsActive,
    }, nil
}

Handle Cross-Cutting Concerns Properly

Implement Logging and Metrics at Appropriate Levels

Logging should not be part of business logic. Instead, use interfaces that can be implemented in infrastructure:

type Logger interface {
    Info(message string, fields map[string]interface{})
    Error(message string, err error, fields map[string]interface{})
}

Avoid Technical Dependencies in Business Entities

Entities must remain free from technical concerns like database schema definitions or HTTP request objects:

// ❌ Wrong - contains database-specific field
type User struct {
    ID       int `db:"id"`
    Name     string
    Email    string
    IsActive bool
}

// ✅ Correct - clean business entity
type User struct {
    ID       int
    Name     string
    Email    string
    IsActive bool
}

Identify Common Pitfalls

Mixing Business Logic with Technical Implementation

A common error occurs when business rules depend on database connection details or framework-specific features:

// ❌ Wrong - business logic depends on SQL
func (u *User) UpdateStatus(db *sql.DB, status bool) error {
    _, err := db.Exec("UPDATE users SET is_active = ? WHERE id = ?", 
        status, u.ID)
    return err
}

Forgetting to Define Interfaces for Dependencies

When infrastructure code directly references concrete types instead of interfaces:

// ❌ Wrong - dependency on concrete type
type UserService struct {
    db *sql.DB  // Direct dependency on database driver
}

// ✅ Correct - dependency on interface
type UserService struct {
    repo UserRepository  // Dependency on interface
}

Confusing Layer Responsibilities

Sometimes developers place infrastructure concerns in the core layer:

// ❌ Wrong - database-specific code in core
func (u *User) SaveToDatabase() error {
    db, err := sql.Open("postgres", "connection_string")
    if err != nil {
        return err
    }
    _, err = db.Exec("INSERT INTO users (...) VALUES (...)")
    return err
}

// ✅ Correct - core logic only
func (u *User) Validate() error {
    // Validation logic only
}

Manage Layer Boundaries Strictly

Use Dependency Inversion Principle

The dependency inversion principle ensures that abstractions do not depend on concrete implementations:

// Interface defined in core layer
type PaymentProcessor interface {
    Process(amount float64) (string, error)
}

// Implementation in infrastructure layer
type StripePaymentProcessor struct{}

func (p *StripePaymentProcessor) Process(amount float64) (string, error) {
    // Stripe-specific implementation
    return "payment_id", nil
}

Avoid Circular Dependencies

Ensure that layers depend only on inner layers:

// ❌ Wrong - circular dependency
type User struct {
    NotificationService *NotificationService  // Depends on presentation layer
}

// ✅ Correct - no circular dependencies
type User struct {
    ID       int
    Name     string
    Email    string
}

Keep Infrastructure Details Out of Core Entities

Core entities should not contain methods that directly interact with external systems:

// ❌ Wrong - entity knows about HTTP
func (u *User) SendEmail() error {
    client := &http.Client{}
    req, _ := http.NewRequest("POST", "https://api.example.com/send", 
        strings.NewReader("email content"))
    _, err := client.Do(req)
    return err
}

// ✅ Correct - entity stays pure
func (u *User) GetEmail() string {
    return u.Email
}

Recognize Production Issues

Silent Data Loss from Improper DTO Usage

When domain entities are used directly in API responses without proper conversion, business logic may be silently bypassed:

// ❌ Risky - direct entity usage
func (s *UserService) GetUser(id int) (*User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    return user, nil  // Returns domain entity directly
}

// ✅ Safer - explicit conversion
func (s *UserService) GetUser(id int) (*UserResponse, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    
    return &UserResponse{
        ID:       user.ID,
        Name:     user.Name,
        Email:    user.Email,
        IsActive: user.IsActive,
    }, nil
}

Hidden Coupling from Interface Implementation

When interfaces are implemented incorrectly or incompletely, systems may silently fail:

// ❌ Missing method implementation
type UserRepository interface {
    Save(user *User) error
    FindByID(id int) (*User, error)
    // Missing FindByEmail method
}

// ✅ Complete interface
type UserRepository interface {
    Save(user *User) error
    FindByID(id int) (*User, error)
    FindByEmail(email string) (*User, error)
}

Layer Violation Through Indirect Dependencies

Sometimes dependencies appear to be in the correct layer but are actually indirect:

// ❌ Indirect dependency - uses framework-specific type
func (s *UserService) ProcessPayment(req PaymentRequest) error {
    // PaymentRequest contains framework-specific types
    // This makes the service dependent on HTTP framework
    return nil
}

Testing Boundary Issues

When tests directly access infrastructure instead of using interfaces, they lose their ability to verify core business logic:

// ❌ Test depends on concrete implementation
func TestUserService(t *testing.T) {
    repo := &PostgresUserRepository{}  // Direct dependency
    service := NewUserService(repo)
    // Test is now tied to database implementation
}

// ✅ Test uses interface
func TestUserService(t *testing.T) {
    repo := &MockUserRepository{}  // Interface-based mock
    service := NewUserService(repo)
    // Test can verify business logic without infrastructure
}

Validate Architecture Integrity

Check Layer Dependencies Regularly

Use static analysis or build tools to ensure that outer layers do not depend on inner layers. Tools like go vet or custom linters can help enforce these boundaries.

Monitor for Interface Implementation Gaps

Regularly review interface implementations to ensure all required methods are properly implemented and tested.

Maintain Clear Separation of Concerns

Each layer should have a single, well-defined responsibility. The core layer handles business logic, the application layer orchestrates operations, and infrastructure handles technical concerns.

The clean architecture pattern provides a reliable foundation for building systems that can evolve over time without requiring complete rewrites of business logic. The key is maintaining strict boundaries between layers and ensuring that dependencies flow in the correct direction.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.