Prerequisites
- Basic understanding of programming concepts 📝
- Python installation (3.8+) 🐍
- VS Code or preferred IDE 💻
What you'll learn
- Understand Clean Architecture fundamentals 🎯
- Apply Clean Architecture in real projects 🏗️
- Debug common architecture issues 🐛
- Write clean, maintainable Python code ✨
🎯 Introduction
Welcome to this exciting tutorial on Clean Architecture in Python! 🎉 In this guide, we’ll explore how to build maintainable, testable, and scalable applications using Uncle Bob’s Clean Architecture principles.
You’ll discover how Clean Architecture can transform your Python development experience. Whether you’re building web applications 🌐, microservices 🔧, or complex enterprise systems 🏢, understanding Clean Architecture is essential for writing robust, maintainable code that stands the test of time.
By the end of this tutorial, you’ll feel confident architecting Python applications that are flexible, testable, and easy to maintain! Let’s dive in! 🏊♂️
📚 Understanding Clean Architecture
🤔 What is Clean Architecture?
Clean Architecture is like building a house with separate, well-defined rooms 🏠. Think of it as organizing your code into concentric circles, where each circle has specific responsibilities and can only depend on inner circles, never outer ones.
In Python terms, Clean Architecture separates your business logic from external concerns like databases, web frameworks, and UI. This means you can:
- ✨ Change frameworks without rewriting business logic
- 🚀 Test business rules in isolation
- 🛡️ Protect your core logic from external changes
- 🎯 Keep your code organized and maintainable
💡 Why Use Clean Architecture?
Here’s why developers love Clean Architecture:
- Testability 🧪: Test business logic without databases or frameworks
- Independence 🔓: Framework-agnostic core business logic
- Flexibility 🔄: Easy to swap databases, APIs, or UI layers
- Maintainability 🛠️: Clear separation of concerns
Real-world example: Imagine building an e-commerce system 🛒. With Clean Architecture, you can switch from PostgreSQL to MongoDB, or from Flask to FastAPI, without touching your order processing logic!
🔧 Basic Syntax and Usage
📝 The Four Layers
Let’s start with the fundamental layers:
# 🎯 1. Entities (Enterprise Business Rules)
class Product:
"""Core business entity - no dependencies! 🎯"""
def __init__(self, id: str, name: str, price: float):
self.id = id
self.name = name
self.price = price
def apply_discount(self, percentage: float) -> float:
"""Business rule: calculate discounted price 💰"""
return self.price * (1 - percentage / 100)
# 🔧 2. Use Cases (Application Business Rules)
class CreateOrderUseCase:
"""Orchestrates business operations 🎼"""
def __init__(self, order_repository):
self.order_repository = order_repository
def execute(self, products: list[Product], customer_id: str):
# 📝 Business logic here!
total = sum(p.price for p in products)
order = Order(customer_id=customer_id, products=products, total=total)
self.order_repository.save(order)
return order
# 🌐 3. Interface Adapters
class OrderController:
"""Adapts HTTP requests to use cases 🔌"""
def __init__(self, create_order_use_case):
self.create_order_use_case = create_order_use_case
def create_order(self, request_data: dict):
# 🔄 Convert external data to domain objects
products = [Product(**p) for p in request_data['products']]
order = self.create_order_use_case.execute(
products,
request_data['customer_id']
)
return {"order_id": order.id, "total": order.total}
# 💾 4. Frameworks & Drivers
class SQLOrderRepository:
"""Database implementation - outermost layer 🗄️"""
def save(self, order: Order):
# SQL-specific implementation
pass
💡 Explanation: Notice how dependencies flow inward! The use case doesn’t know about SQL or HTTP - it only knows about business rules! 🎯
🎯 The Dependency Rule
The golden rule of Clean Architecture:
# ✅ Correct - Inner layers don't know about outer layers
class OrderService: # Use Case layer
def __init__(self, repository): # Interface, not implementation!
self.repository = repository
def process_order(self, order_data):
# Pure business logic here! 🎯
pass
# ❌ Wrong - Business logic coupled to framework
class BadOrderService:
def __init__(self):
import flask # 💥 Framework dependency in business logic!
self.app = flask.current_app
💡 Practical Examples
🛒 Example 1: E-Commerce System
Let’s build a real e-commerce order system:
# 🎯 Domain Layer (Entities)
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import List
import uuid
@dataclass
class OrderItem:
"""Order item entity 📦"""
product_id: str
name: str
price: float
quantity: int
def subtotal(self) -> float:
"""Calculate item subtotal 💰"""
return self.price * self.quantity
class Order:
"""Order aggregate root 🛒"""
def __init__(self, customer_id: str):
self.id = str(uuid.uuid4())
self.customer_id = customer_id
self.items: List[OrderItem] = []
self.created_at = datetime.now()
self.status = "pending"
def add_item(self, item: OrderItem):
"""Add item to order 📦➕"""
self.items.append(item)
def total(self) -> float:
"""Calculate order total 💵"""
return sum(item.subtotal() for item in self.items)
def can_be_cancelled(self) -> bool:
"""Business rule: only pending orders can be cancelled 🚫"""
return self.status == "pending"
# 🔧 Use Case Layer
class OrderRepository(ABC):
"""Repository interface - no implementation details! 🎯"""
@abstractmethod
def save(self, order: Order) -> None:
pass
@abstractmethod
def find_by_id(self, order_id: str) -> Order:
pass
class CreateOrderUseCase:
"""Create order use case 🎬"""
def __init__(self, order_repo: OrderRepository, inventory_service):
self.order_repo = order_repo
self.inventory_service = inventory_service
def execute(self, customer_id: str, items_data: List[dict]) -> dict:
# 🏗️ Create order
order = Order(customer_id)
# 📦 Add items and check inventory
for item_data in items_data:
if not self.inventory_service.is_available(
item_data['product_id'],
item_data['quantity']
):
raise ValueError(f"Product {item_data['product_id']} not available! 😢")
item = OrderItem(**item_data)
order.add_item(item)
# 💾 Save order
self.order_repo.save(order)
# 📤 Return result
return {
"order_id": order.id,
"total": order.total(),
"status": order.status,
"message": "Order created successfully! 🎉"
}
# 🌐 Interface Adapters Layer
class FlaskOrderController:
"""Flask controller adapter 🔌"""
def __init__(self, create_order_use_case: CreateOrderUseCase):
self.create_order_use_case = create_order_use_case
def create_order(self, request):
try:
# 🔄 Adapt request to use case
result = self.create_order_use_case.execute(
customer_id=request.json['customer_id'],
items_data=request.json['items']
)
return {"success": True, "data": result}, 201
except ValueError as e:
return {"success": False, "error": str(e)}, 400
# 💾 Infrastructure Layer
class PostgreSQLOrderRepository(OrderRepository):
"""PostgreSQL implementation 🐘"""
def __init__(self, connection):
self.connection = connection
def save(self, order: Order) -> None:
# 💾 SQL implementation details
cursor = self.connection.cursor()
cursor.execute(
"INSERT INTO orders (id, customer_id, total, status) VALUES (%s, %s, %s, %s)",
(order.id, order.customer_id, order.total(), order.status)
)
# Save order items too...
def find_by_id(self, order_id: str) -> Order:
# 🔍 Query implementation
pass
# 🎮 Putting it all together!
def create_app():
# 🏗️ Wire up dependencies (dependency injection)
db_connection = create_database_connection()
order_repo = PostgreSQLOrderRepository(db_connection)
inventory_service = InventoryService()
create_order_use_case = CreateOrderUseCase(order_repo, inventory_service)
order_controller = FlaskOrderController(create_order_use_case)
# 🚀 Ready to handle requests!
return order_controller
🎯 Try it yourself: Add a CancelOrderUseCase
that respects the business rules!
🏦 Example 2: Banking System
Let’s create a clean banking system:
# 🎯 Domain Layer
@dataclass
class Money:
"""Value object for money 💰"""
amount: float
currency: str = "USD"
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Cannot add different currencies! 💱")
return Money(self.amount + other.amount, self.currency)
class Account:
"""Bank account entity 🏦"""
def __init__(self, account_number: str, owner: str):
self.account_number = account_number
self.owner = owner
self.balance = Money(0.0)
self.transactions = []
def deposit(self, amount: Money):
"""Deposit money 💵➕"""
self.balance = self.balance + amount
self.transactions.append({
"type": "deposit",
"amount": amount.amount,
"timestamp": datetime.now()
})
def withdraw(self, amount: Money) -> bool:
"""Withdraw money with validation 💵➖"""
if amount.amount > self.balance.amount:
return False # Insufficient funds! 😢
self.balance.amount -= amount.amount
self.transactions.append({
"type": "withdrawal",
"amount": amount.amount,
"timestamp": datetime.now()
})
return True
# 🔧 Use Cases
class TransferMoneyUseCase:
"""Transfer money between accounts 💸"""
def __init__(self, account_repo, notification_service):
self.account_repo = account_repo
self.notification_service = notification_service
def execute(self, from_account_id: str, to_account_id: str, amount: float):
# 🔍 Get accounts
from_account = self.account_repo.find_by_id(from_account_id)
to_account = self.account_repo.find_by_id(to_account_id)
# 💰 Create money object
transfer_amount = Money(amount)
# 💸 Perform transfer
if not from_account.withdraw(transfer_amount):
raise ValueError("Insufficient funds! 😢")
to_account.deposit(transfer_amount)
# 💾 Save changes
self.account_repo.save(from_account)
self.account_repo.save(to_account)
# 📧 Send notifications
self.notification_service.notify_transfer(
from_account.owner,
to_account.owner,
amount
)
return {
"success": True,
"message": f"Transferred ${amount} successfully! 🎉",
"from_balance": from_account.balance.amount,
"to_balance": to_account.balance.amount
}
🚀 Advanced Concepts
🧙♂️ Dependency Injection Container
When you’re ready to level up, use a DI container:
# 🎯 Advanced DI Container
class DIContainer:
"""Dependency injection container 💉"""
def __init__(self):
self._services = {}
self._singletons = {}
def register(self, interface, implementation, singleton=False):
"""Register a service 📝"""
self._services[interface] = (implementation, singleton)
def resolve(self, interface):
"""Resolve a dependency 🔍"""
if interface not in self._services:
raise ValueError(f"Service {interface} not registered! 😱")
implementation, is_singleton = self._services[interface]
if is_singleton:
if interface not in self._singletons:
self._singletons[interface] = implementation()
return self._singletons[interface]
return implementation()
# 🏗️ Application setup
def setup_application():
container = DIContainer()
# 📝 Register services
container.register(OrderRepository, PostgreSQLOrderRepository, singleton=True)
container.register(NotificationService, EmailNotificationService)
container.register(CreateOrderUseCase,
lambda: CreateOrderUseCase(
container.resolve(OrderRepository),
container.resolve(NotificationService)
)
)
return container
🏗️ Event-Driven Clean Architecture
For the brave developers - combine with events:
# 🚀 Domain Events
@dataclass
class DomainEvent:
"""Base domain event 📢"""
aggregate_id: str
occurred_at: datetime = datetime.now()
@dataclass
class OrderCreatedEvent(DomainEvent):
"""Order created event 🛒✨"""
customer_id: str
total: float
class EventBus:
"""Event bus for decoupling 🚌"""
def __init__(self):
self.handlers = {}
def subscribe(self, event_type, handler):
"""Subscribe to events 📮"""
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event: DomainEvent):
"""Publish events 📢"""
event_type = type(event)
if event_type in self.handlers:
for handler in self.handlers[event_type]:
handler(event)
# 🎯 Event-sourced aggregate
class EventSourcedOrder(Order):
"""Order with event sourcing 📚"""
def __init__(self, customer_id: str, event_bus: EventBus):
super().__init__(customer_id)
self.event_bus = event_bus
self.uncommitted_events = []
def add_item(self, item: OrderItem):
super().add_item(item)
# 📢 Raise domain event
event = OrderItemAddedEvent(
aggregate_id=self.id,
product_id=item.product_id,
quantity=item.quantity
)
self.uncommitted_events.append(event)
self.event_bus.publish(event)
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Framework Invasion
# ❌ Wrong - Framework in domain layer!
class Order:
def __init__(self):
self.id = models.AutoField(primary_key=True) # 💥 Django in domain!
self.created_at = models.DateTimeField()
# ✅ Correct - Pure Python domain
class Order:
def __init__(self):
self.id = str(uuid.uuid4()) # ✨ No framework dependency!
self.created_at = datetime.now()
🤯 Pitfall 2: Anemic Domain Model
# ❌ Wrong - No behavior, just data!
class Product:
def __init__(self, price):
self.price = price
class PriceCalculator: # 😢 Logic separated from data
def calculate_discount(self, product, percentage):
return product.price * (1 - percentage / 100)
# ✅ Correct - Rich domain model!
class Product:
def __init__(self, price):
self.price = price
def apply_discount(self, percentage): # 🎯 Behavior with data!
"""Business logic where it belongs!"""
if percentage > 50:
raise ValueError("Discount too high! 🚫")
return self.price * (1 - percentage / 100)
🛠️ Best Practices
- 🎯 Keep Business Logic Pure: No framework dependencies in domain layer!
- 📝 Use Interfaces: Define contracts between layers
- 🛡️ Respect the Dependency Rule: Dependencies point inward only
- 🎨 Rich Domain Models: Put behavior with data
- ✨ Test the Use Cases: They contain your valuable business logic
🧪 Hands-On Exercise
🎯 Challenge: Build a Library Management System
Create a clean architecture library system:
📋 Requirements:
- ✅ Books with ISBN, title, author, and availability status
- 🏷️ Members who can borrow and return books
- 👤 Borrowing rules (max 3 books, 14-day limit)
- 📅 Late fee calculation ($0.50 per day)
- 🎨 Each book category needs an emoji!
🚀 Bonus Points:
- Add reservation system
- Implement notification for due dates
- Create book recommendation use case
💡 Solution
🔍 Click to see solution
# 🎯 Domain Layer
from datetime import datetime, timedelta
from typing import List, Optional
import uuid
@dataclass
class Book:
"""Book entity 📚"""
isbn: str
title: str
author: str
category: str
emoji: str = "📖"
is_available: bool = True
@dataclass
class Loan:
"""Loan value object 📋"""
book_isbn: str
member_id: str
borrowed_date: datetime
due_date: datetime
returned_date: Optional[datetime] = None
def is_overdue(self) -> bool:
"""Check if loan is overdue 📅"""
if self.returned_date:
return False
return datetime.now() > self.due_date
def calculate_fine(self) -> float:
"""Calculate late fine 💰"""
if not self.is_overdue():
return 0.0
days_overdue = (datetime.now() - self.due_date).days
return days_overdue * 0.50
class Member:
"""Library member aggregate 👤"""
def __init__(self, member_id: str, name: str):
self.id = member_id
self.name = name
self.active_loans: List[Loan] = []
def can_borrow(self) -> bool:
"""Business rule: max 3 books 📚"""
return len(self.active_loans) < 3
def borrow_book(self, book: Book) -> Loan:
"""Borrow a book 📖➕"""
if not self.can_borrow():
raise ValueError("Maximum books borrowed! 🚫")
if not book.is_available:
raise ValueError("Book not available! 😢")
loan = Loan(
book_isbn=book.isbn,
member_id=self.id,
borrowed_date=datetime.now(),
due_date=datetime.now() + timedelta(days=14)
)
self.active_loans.append(loan)
book.is_available = False
return loan
# 🔧 Use Cases
class BorrowBookUseCase:
"""Borrow book use case 📚"""
def __init__(self, member_repo, book_repo, notification_service):
self.member_repo = member_repo
self.book_repo = book_repo
self.notification_service = notification_service
def execute(self, member_id: str, isbn: str) -> dict:
# 🔍 Get entities
member = self.member_repo.find_by_id(member_id)
book = self.book_repo.find_by_isbn(isbn)
# 📖 Borrow book
loan = member.borrow_book(book)
# 💾 Save changes
self.member_repo.save(member)
self.book_repo.save(book)
# 📧 Send notification
self.notification_service.send_borrow_confirmation(
member.name,
book.title,
loan.due_date
)
return {
"success": True,
"message": f"Borrowed '{book.title}' {book.emoji} successfully! 🎉",
"due_date": loan.due_date.isoformat(),
"books_remaining": 3 - len(member.active_loans)
}
class ReturnBookUseCase:
"""Return book use case 📚"""
def __init__(self, member_repo, book_repo):
self.member_repo = member_repo
self.book_repo = book_repo
def execute(self, member_id: str, isbn: str) -> dict:
# 🔍 Get entities
member = self.member_repo.find_by_id(member_id)
book = self.book_repo.find_by_isbn(isbn)
# 📋 Find loan
loan = next(
(l for l in member.active_loans if l.book_isbn == isbn),
None
)
if not loan:
raise ValueError("No active loan found! 🤔")
# 💰 Calculate fine
fine = loan.calculate_fine()
# 📖 Return book
loan.returned_date = datetime.now()
member.active_loans.remove(loan)
book.is_available = True
# 💾 Save changes
self.member_repo.save(member)
self.book_repo.save(book)
result = {
"success": True,
"message": f"Returned '{book.title}' successfully! ✅",
}
if fine > 0:
result["fine"] = f"${fine:.2f}"
result["message"] += f" Late fine: ${fine:.2f} 💸"
return result
# 🌐 Interface Adapters
class LibraryController:
"""Library API controller 🏛️"""
def __init__(self, borrow_use_case, return_use_case):
self.borrow_use_case = borrow_use_case
self.return_use_case = return_use_case
def borrow_book(self, request):
"""Handle borrow request 📥"""
try:
result = self.borrow_use_case.execute(
member_id=request.json['member_id'],
isbn=request.json['isbn']
)
return {"status": "success", **result}, 200
except ValueError as e:
return {"status": "error", "message": str(e)}, 400
def return_book(self, request):
"""Handle return request 📤"""
try:
result = self.return_use_case.execute(
member_id=request.json['member_id'],
isbn=request.json['isbn']
)
return {"status": "success", **result}, 200
except ValueError as e:
return {"status": "error", "message": str(e)}, 400
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Design Clean Architecture systems with confidence 💪
- ✅ Separate concerns properly between layers 🛡️
- ✅ Write testable business logic 🎯
- ✅ Build flexible systems that adapt to change 🐛
- ✅ Create maintainable Python applications! 🚀
Remember: Clean Architecture is about protecting your business logic from the chaos of the outside world! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered Clean Architecture in Python!
Here’s what to do next:
- 💻 Practice with the library system exercise above
- 🏗️ Refactor an existing project to use Clean Architecture
- 📚 Move on to our next tutorial: Hexagonal Architecture
- 🌟 Share your clean architecture journey with others!
Remember: Every great architect started with their first clean design. Keep building, keep learning, and most importantly, have fun creating maintainable systems! 🚀
Happy coding! 🎉🚀✨