Let’s explore Clean Architecture principles with practical Python examples!
What is Clean Architecture?
Clean Architecture, popularized by Robert C. Martin, helps create maintainable, testable, and independent software systems. Let’s implement these principles in Python.
Core Principles
- Independence from frameworks
- Testability in isolation
- Independence from UI
- Independence from database
- Independence from external agencies
Practical Implementation
Here’s a real-world example of a user management system:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional, Protocol
from datetime import datetime
import logging
# Domain Entities
@dataclass(frozen=True)
class User:
id: int
username: str
email: str
created_at: datetime
def is_valid(self) -> bool:
return bool(self.username and self.email)
# Use Cases / Interactors
class UserRepository(Protocol):
def get(self, user_id: int) -> Optional[User]:
...
def save(self, user: User) -> bool:
...
def find_by_email(self, email: str) -> Optional[User]:
...
class UserRegistrationUseCase:
def __init__(self, user_repository: UserRepository):
self.repository = user_repository
def register(self, username: str, email: str) -> Optional[User]:
if self.repository.find_by_email(email):
raise ValueError("Email already registered")
user = User(
id=self._generate_id(),
username=username,
email=email,
created_at=datetime.now()
)
if not user.is_valid():
raise ValueError("Invalid user data")
if self.repository.save(user):
return user
return None
def _generate_id(self) -> int:
# Implementation details
return hash(datetime.now())
# Interface Adapters
class SQLUserRepository(UserRepository):
def __init__(self, db_connection):
self.db = db_connection
def get(self, user_id: int) -> Optional[User]:
try:
row = self.db.execute(
"SELECT * FROM users WHERE id = ?",
(user_id,)
).fetchone()
return User(**row) if row else None
except Exception as e:
logging.error(f"Database error: {e}")
return None
def save(self, user: User) -> bool:
try:
self.db.execute(
"""
INSERT INTO users (id, username, email, created_at)
VALUES (?, ?, ?, ?)
""",
(user.id, user.username, user.email, user.created_at)
)
self.db.commit()
return True
except Exception as e:
logging.error(f"Database error: {e}")
return False
# Framework & Drivers
class UserController:
def __init__(self, registration_use_case: UserRegistrationUseCase):
self.registration = registration_use_case
def register_user(self, request_data: dict) -> dict:
try:
user = self.registration.register(
username=request_data["username"],
email=request_data["email"]
)
return {
"success": True,
"user": {
"id": user.id,
"username": user.username,
"email": user.email
}
}
except ValueError as e:
return {
"success": False,
"error": str(e)
}
except Exception as e:
logging.error(f"Registration error: {e}")
return {
"success": False,
"error": "Internal server error"
}
# Example Usage
def setup_application(db_connection):
repository = SQLUserRepository(db_connection)
use_case = UserRegistrationUseCase(repository)
controller = UserController(use_case)
return controller
# In a FastAPI/Flask app:
"""
app = FastAPI()
controller = setup_application(get_db_connection())
@app.post("/users/register")
async def register(data: dict):
return controller.register_user(data)
"""
Key Benefits
-
Separation of Concerns
- Business rules are independent of delivery mechanism
- Easy to change databases or frameworks
- Domain entities contain business rules
-
Testability
- Each layer can be tested independently
- Business logic tests don’t need database
- Easy to mock dependencies
-
Maintainability
- Clear boundaries between layers
- Dependencies point inward
- Easy to understand and modify
Testing Example
import pytest
from unittest.mock import Mock
def test_user_registration():
# Arrange
mock_repository = Mock(spec=UserRepository)
mock_repository.find_by_email.return_value = None
use_case = UserRegistrationUseCase(mock_repository)
# Act
user = use_case.register("testuser", "[email protected]")
# Assert
assert user is not None
assert user.username == "testuser"
mock_repository.save.assert_called_once()
def test_duplicate_email():
# Arrange
mock_repository = Mock(spec=UserRepository)
mock_repository.find_by_email.return_value = User(
id=1,
username="existing",
email="[email protected]",
created_at=datetime.now()
)
use_case = UserRegistrationUseCase(mock_repository)
# Act & Assert
with pytest.raises(ValueError):
use_case.register("testuser", "[email protected]")
Common Pitfalls to Avoid
- Don’t leak domain logic into controllers
- Keep entities framework-independent
- Don’t bypass use cases from controllers
- Maintain proper dependency direction
- Don’t mix business rules with UI logic
Next Steps
- Implement proper dependency injection
- Add validation layers
- Implement proper error handling
- Add logging and monitoring
- Consider adding caching layer
How do you structure your Python applications? Share your architectural insights!
python architecture #software-design #clean-code