The Problem with "It Just Works" Code

Every Python project starts simple. A single file, a few functions, maybe a class or two. Then requirements grow: you add a database, an API, a background job queue. Before long, your "simple" script is a tangled mess where business logic lives alongside SQL queries and HTTP response formatting.

Clean Architecture is a set of principles, popularized by Robert C. Martin ("Uncle Bob"), designed to keep your core business logic independent from frameworks, databases, and external services.

Core Principle: The Dependency Rule

The fundamental rule is: source code dependencies must point inward. Inner layers know nothing about outer layers.

  • Entities — Pure business objects and rules (innermost)
  • Use Cases — Application-specific business logic
  • Interface Adapters — Controllers, presenters, gateways
  • Frameworks & Drivers — Django, Flask, SQLAlchemy, etc. (outermost)

Your database framework depends on your use cases. Your use cases do not depend on your database framework.

A Practical Python Example

Consider a simple user registration feature. Without clean architecture:

# Tightly coupled — hard to test, hard to change
def register_user(username, email):
    db.execute("INSERT INTO users ...", username, email)
    requests.post("https://email-service.com/send", {...})
    return {"status": "ok"}

With clean architecture, we separate concerns:

# entities/user.py — pure Python, no dependencies
from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str

# use_cases/register_user.py — depends only on abstractions
class RegisterUserUseCase:
    def __init__(self, user_repo, email_service):
        self.user_repo = user_repo
        self.email_service = email_service

    def execute(self, username: str, email: str) -> User:
        user = User(username=username, email=email)
        self.user_repo.save(user)
        self.email_service.send_welcome(user)
        return user

Dependency Injection: The Glue

Clean architecture relies heavily on dependency injection — passing dependencies in rather than creating them inside functions. This makes testing trivial:

def test_register_user():
    mock_repo = MockUserRepository()
    mock_email = MockEmailService()
    use_case = RegisterUserUseCase(mock_repo, mock_email)
    
    user = use_case.execute("alice", "alice@example.com")
    
    assert user.username == "alice"
    assert mock_repo.saved_user == user
    assert mock_email.welcome_sent is True

Recommended Project Structure

my_app/
├── entities/          # Business objects
│   └── user.py
├── use_cases/         # Business logic
│   └── register_user.py
├── adapters/          # Interfaces to external world
│   ├── repositories/
│   └── services/
├── infrastructure/    # Concrete implementations
│   ├── db/            # SQLAlchemy models
│   └── email/         # SMTP or API client
└── entrypoints/       # FastAPI, Flask, CLI
    └── api/

When to Apply Clean Architecture

ScenarioRecommendation
Simple script or one-off toolSkip it — overkill
Small web app with limited logicPartial separation is fine
Complex domain with many business rulesStrongly recommended
App likely to change frameworks or DBsEssential
Team project with multiple developersStrongly recommended

Key Takeaways

  • Your business logic should have zero imports from Django, SQLAlchemy, or any framework.
  • Depend on abstractions (protocols/ABCs), not concrete implementations.
  • Use dependency injection to wire things together at the edges.
  • Start simpler and refactor toward clean architecture as complexity grows.