Clean Architecture Pattern

Clean Architecture (by Robert C. Martin) organizes code into layers with clear dependencies flowing inward. The inner layers contain business logic and are independent of frameworks, UI, and databases.

Use Case

Use Clean Architecture when you need to:

  • Build maintainable, testable systems
  • Keep business logic independent of frameworks
  • Support multiple interfaces (web, CLI, API)
  • Enable easy technology changes

Architecture Layers

Dependency Rule: Dependencies point inward (outer → inner). Inner layers know nothing about outer layers.

Code

Example 1: Go Implementation

  1// Domain Layer (Entities)
  2package domain
  3
  4type User struct {
  5    ID       string
  6    Email    string
  7    Name     string
  8    Password string
  9}
 10
 11type UserRepository interface {
 12    Save(user *User) error
 13    FindByID(id string) (*User, error)
 14    FindByEmail(email string) (*User, error)
 15}
 16
 17// Use Case Layer
 18package usecase
 19
 20type CreateUserUseCase struct {
 21    repo domain.UserRepository
 22}
 23
 24func NewCreateUserUseCase(repo domain.UserRepository) *CreateUserUseCase {
 25    return &CreateUserUseCase{repo: repo}
 26}
 27
 28func (uc *CreateUserUseCase) Execute(email, name, password string) (*domain.User, error) {
 29    // Business logic
 30    existing, _ := uc.repo.FindByEmail(email)
 31    if existing != nil {
 32        return nil, errors.New("user already exists")
 33    }
 34    
 35    user := &domain.User{
 36        ID:       generateID(),
 37        Email:    email,
 38        Name:     name,
 39        Password: hashPassword(password),
 40    }
 41    
 42    if err := uc.repo.Save(user); err != nil {
 43        return nil, err
 44    }
 45    
 46    return user, nil
 47}
 48
 49// Interface Adapter Layer (Repository Implementation)
 50package repository
 51
 52type PostgresUserRepository struct {
 53    db *sql.DB
 54}
 55
 56func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
 57    return &PostgresUserRepository{db: db}
 58}
 59
 60func (r *PostgresUserRepository) Save(user *domain.User) error {
 61    query := "INSERT INTO users (id, email, name, password) VALUES ($1, $2, $3, $4)"
 62    _, err := r.db.Exec(query, user.ID, user.Email, user.Name, user.Password)
 63    return err
 64}
 65
 66func (r *PostgresUserRepository) FindByID(id string) (*domain.User, error) {
 67    // Implementation
 68}
 69
 70// Interface Adapter Layer (Controller)
 71package handler
 72
 73type UserHandler struct {
 74    createUser *usecase.CreateUserUseCase
 75}
 76
 77func NewUserHandler(createUser *usecase.CreateUserUseCase) *UserHandler {
 78    return &UserHandler{createUser: createUser}
 79}
 80
 81func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
 82    var req struct {
 83        Email    string `json:"email"`
 84        Name     string `json:"name"`
 85        Password string `json:"password"`
 86    }
 87    
 88    json.NewDecoder(r.Body).Decode(&req)
 89    
 90    user, err := h.createUser.Execute(req.Email, req.Name, req.Password)
 91    if err != nil {
 92        http.Error(w, err.Error(), http.StatusBadRequest)
 93        return
 94    }
 95    
 96    json.NewEncoder(w).Encode(user)
 97}
 98
 99// Framework Layer (Main)
100package main
101
102func main() {
103    // Setup database
104    db, _ := sql.Open("postgres", "connection-string")
105    
106    // Wire dependencies (Dependency Injection)
107    userRepo := repository.NewPostgresUserRepository(db)
108    createUserUC := usecase.NewCreateUserUseCase(userRepo)
109    userHandler := handler.NewUserHandler(createUserUC)
110    
111    // Setup routes
112    http.HandleFunc("/users", userHandler.CreateUser)
113    http.ListenAndServe(":8080", nil)
114}

Example 2: Python Implementation

 1# Domain Layer (Entities)
 2from dataclasses import dataclass
 3from abc import ABC, abstractmethod
 4
 5@dataclass
 6class User:
 7    id: str
 8    email: str
 9    name: str
10    password: str
11
12class UserRepository(ABC):
13    @abstractmethod
14    def save(self, user: User) -> None:
15        pass
16    
17    @abstractmethod
18    def find_by_id(self, id: str) -> User:
19        pass
20
21# Use Case Layer
22class CreateUserUseCase:
23    def __init__(self, repo: UserRepository):
24        self.repo = repo
25    
26    def execute(self, email: str, name: str, password: str) -> User:
27        # Business logic
28        existing = self.repo.find_by_email(email)
29        if existing:
30            raise ValueError("User already exists")
31        
32        user = User(
33            id=generate_id(),
34            email=email,
35            name=name,
36            password=hash_password(password)
37        )
38        
39        self.repo.save(user)
40        return user
41
42# Interface Adapter Layer (Repository)
43class PostgresUserRepository(UserRepository):
44    def __init__(self, db_connection):
45        self.db = db_connection
46    
47    def save(self, user: User) -> None:
48        query = "INSERT INTO users (id, email, name, password) VALUES (%s, %s, %s, %s)"
49        self.db.execute(query, (user.id, user.email, user.name, user.password))
50
51# Interface Adapter Layer (Controller)
52from flask import Flask, request, jsonify
53
54class UserController:
55    def __init__(self, create_user_uc: CreateUserUseCase):
56        self.create_user = create_user_uc
57    
58    def create_user_endpoint(self):
59        data = request.get_json()
60        try:
61            user = self.create_user.execute(
62                email=data['email'],
63                name=data['name'],
64                password=data['password']
65            )
66            return jsonify(user.__dict__), 201
67        except ValueError as e:
68            return jsonify({'error': str(e)}), 400
69
70# Framework Layer (Main)
71def main():
72    # Setup
73    db = create_db_connection()
74    
75    # Wire dependencies
76    user_repo = PostgresUserRepository(db)
77    create_user_uc = CreateUserUseCase(user_repo)
78    user_controller = UserController(create_user_uc)
79    
80    # Setup Flask
81    app = Flask(__name__)
82    app.route('/users', methods=['POST'])(user_controller.create_user_endpoint)
83    
84    app.run(port=8080)

Key Principles

1. Dependency Inversion

1High-level modules should not depend on low-level modules.
2Both should depend on abstractions (interfaces).

2. Single Responsibility

1Each layer has one reason to change:
2- Entities: Business rules change
3- Use Cases: Application workflow changes
4- Controllers: API contract changes
5- Repositories: Data storage changes

3. Testability

 1// Easy to test - mock the repository
 2func TestCreateUser(t *testing.T) {
 3    mockRepo := &MockUserRepository{}
 4    useCase := NewCreateUserUseCase(mockRepo)
 5    
 6    user, err := useCase.Execute("test@example.com", "Test", "pass")
 7    
 8    assert.NoError(t, err)
 9    assert.NotNil(t, user)
10}

Benefits

  • Testable: Business logic can be tested without frameworks
  • Independent: Core logic doesn't depend on UI, DB, or frameworks
  • Flexible: Easy to swap implementations (e.g., Postgres → MongoDB)
  • Maintainable: Clear separation of concerns
  • Scalable: Can grow without becoming tangled

Trade-offs

  • ⚠️ More code: More interfaces and layers than simple approaches
  • ⚠️ Learning curve: Team needs to understand the pattern
  • ⚠️ Overkill for simple apps: Not needed for CRUD applications
  • ⚠️ Boilerplate: Requires discipline to maintain structure

Notes

  • Start simple, add layers as needed
  • Use dependency injection for wiring
  • Keep entities pure (no framework dependencies)
  • Use cases should be thin - just orchestration
  • Test each layer independently

Gotchas/Warnings

  • ⚠️ Over-abstraction: Don't create interfaces for everything
  • ⚠️ Anemic domain: Entities should have behavior, not just data
  • ⚠️ Leaky abstractions: Be careful not to leak implementation details
  • ⚠️ Premature optimization: Don't use this for simple projects
comments powered by Disqus