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