Clean ArchitectureΒΆ
Time to read: 10 minutes
Clean Architecture is a way of organizing code into layers with clear responsibilities and dependencies. It's the foundation of how Neuroglia structures applications.
β The Problem: "Big Ball of Mud"ΒΆ
Without architectural guidance, code becomes tangled:
# β Everything mixed together
class OrderService:
def create_order(self, data):
# UI logic
if not data.get("customer_name"):
return {"error": "Name required"}, 400
# Business logic
order = Order()
order.customer = data["customer_name"]
order.total = data["total"]
# Database access (MongoDB specific)
mongo_client.db.orders.insert_one(order.__dict__)
# Email sending
smtp.send_mail(to=data["email"], subject="Order confirmed")
# Return HTTP response
return {"order_id": order.id}, 201
Problems:
- Can't test without database and email server
- Can't switch from MongoDB to PostgreSQL
- Business rules mixed with HTTP and infrastructure
- Changes in UI requirements force changes in business logic
β The Solution: Layers with Dependency RulesΒΆ
Clean Architecture organizes code into concentric layers:
βββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure (Outer) β β Frameworks, DB, External APIs
β βββββββββββββββββββββββββββββββββββ β
β β Application (Orchestration) β β β Use Cases, Handlers
β β βββββββββββββββββββββββββββ β β
β β β Domain (Core) β β β β Business Rules, Entities
β β β β β β
β β βββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
Dependency Rule: Inner layers don't know about outer layers
Dependencies point INWARD only
The LayersΒΆ
1. Domain (Core) - The Heart
- What: Business entities, rules, and logic
- Depends on: Nothing (pure Python)
- Example:
Order,Customer,Pizzaentities with business rules
# β
Domain layer - No dependencies on framework or database
class Order:
def add_pizza(self, pizza: Pizza):
if self.status != OrderStatus.PENDING:
raise ValueError("Cannot modify confirmed orders") # Business rule
self.items.append(pizza)
2. Application (Orchestration) - The Use Cases
- What: Application-specific business logic, use cases
- Depends on: Domain only
- Example: Command handlers, query handlers, application services
# β
Application layer - Orchestrates domain operations
class PlaceOrderHandler:
def __init__(self, order_repository: IOrderRepository):
self.repository = order_repository # Interface, not implementation!
async def handle(self, command: PlaceOrderCommand):
order = Order(command.customer_id)
order.add_pizza(command.pizza)
await self.repository.save(order) # Uses interface
return order
3. Infrastructure (Outer) - The Details
- What: Frameworks, databases, external services
- Depends on: Domain and Application (implements their interfaces)
- Example: MongoDB repositories, HTTP clients, email services
# β
Infrastructure layer - Implements domain interfaces
class MongoOrderRepository(IOrderRepository):
def __init__(self, mongo_client):
self.client = mongo_client
async def save(self, order: Order):
# MongoDB-specific implementation
await self.client.db.orders.insert_one(order.to_dict())
The Dependency RuleΒΆ
Critical principle: Dependencies point INWARD only.
β
ALLOWED:
Application β Domain (handlers use entities)
Infrastructure β Domain (repositories implement domain interfaces)
Infrastructure β Application (implements handler interfaces)
β FORBIDDEN:
Domain β Application (entities don't know about handlers)
Domain β Infrastructure (entities don't know about MongoDB)
Application β Infrastructure (handlers use interfaces, not implementations)
π§ Clean Architecture in NeurogliaΒΆ
Project StructureΒΆ
Neuroglia enforces clean architecture through directory structure:
my-app/
βββ domain/ # ποΈ Domain Layer (Inner)
β βββ entities/ # Business entities
β βββ events/ # Domain events
β βββ repositories/ # Repository INTERFACES (not implementations)
β
βββ application/ # πΌ Application Layer (Middle)
β βββ commands/ # Write operations
β βββ queries/ # Read operations
β βββ events/ # Event handlers
β βββ services/ # Application services
β
βββ integration/ # π Infrastructure Layer (Outer)
β βββ repositories/ # Repository IMPLEMENTATIONS (MongoDB, etc.)
β βββ services/ # External service integrations
β
βββ api/ # π Presentation Layer (Outer)
βββ controllers/ # REST endpoints
βββ dtos/ # Data transfer objects
Dependency Flow ExampleΒΆ
# 1. Domain defines interface (no implementation)
# domain/repositories/order_repository.py
class IOrderRepository(ABC):
@abstractmethod
async def save_async(self, order: Order): pass
# 2. Application uses interface (doesn't care about implementation)
# application/commands/place_order_handler.py
class PlaceOrderHandler:
def __init__(self, repository: IOrderRepository): # Interface!
self.repository = repository
async def handle(self, cmd: PlaceOrderCommand):
order = Order(cmd.customer_id)
await self.repository.save_async(order) # Uses interface
# 3. Infrastructure implements interface
# integration/repositories/mongo_order_repository.py
class MongoOrderRepository(IOrderRepository):
async def save_async(self, order: Order):
# MongoDB-specific code here
pass
# 4. DI container wires them together at runtime
# main.py
services.add_scoped(IOrderRepository, MongoOrderRepository)
The magic: Handler never knows about MongoDB! You can swap to PostgreSQL by changing one line in main.py.
π§ͺ Testing BenefitsΒΆ
Clean Architecture makes testing easy:
# Test with in-memory repository (no database needed!)
class InMemoryOrderRepository(IOrderRepository):
def __init__(self):
self.orders = {}
async def save_async(self, order: Order):
self.orders[order.id] = order
# Test handler
async def test_place_order():
repo = InMemoryOrderRepository() # No MongoDB!
handler = PlaceOrderHandler(repo)
cmd = PlaceOrderCommand(customer_id="123", pizza="Margherita")
result = await handler.handle(cmd)
assert result.is_success
assert len(repo.orders) == 1 # Verify order was saved
β οΈ Common MistakesΒΆ
1. Domain Depending on InfrastructureΒΆ
# β WRONG: Entity knows about MongoDB
class Order:
def save(self):
mongo_client.db.orders.insert_one(self.__dict__) # NO!
# β
RIGHT: Entity is pure business logic
class Order:
def add_pizza(self, pizza):
if self.status != OrderStatus.PENDING:
raise ValueError("Cannot modify confirmed orders")
2. Application Depending on Concrete ImplementationsΒΆ
# β WRONG: Handler depends on concrete MongoDB repository
class PlaceOrderHandler:
def __init__(self):
self.repo = MongoOrderRepository() # Tight coupling!
# β
RIGHT: Handler depends on interface
class PlaceOrderHandler:
def __init__(self, repo: IOrderRepository): # Interface!
self.repo = repo
3. Putting Everything in DomainΒΆ
# β WRONG: Email sending in domain entity
class Order:
def confirm(self):
self.status = OrderStatus.CONFIRMED
EmailService.send_confirmation(self.customer) # NO!
# β
RIGHT: Domain events, infrastructure listens
class Order:
def confirm(self):
self.status = OrderStatus.CONFIRMED
self.raise_event(OrderConfirmedEvent(...)) # Yes!
# Infrastructure reacts to event
class OrderConfirmedHandler:
async def handle(self, event: OrderConfirmedEvent):
await self.email_service.send_confirmation(...)
π« When NOT to UseΒΆ
Clean Architecture has overhead. Consider simpler approaches when:
- Prototype/Throwaway Code: If you're just testing an idea
- Tiny Scripts: < 100 lines, no tests, no maintenance
- CRUD Apps: Simple database operations with no business logic
- Single Developer, Short Timeline: Clean Architecture shines in teams and long-term projects
For small apps, start simple and refactor to clean architecture when complexity grows.
π Key TakeawaysΒΆ
- Layers: Domain (core) β Application (use cases) β Infrastructure (details)
- Dependency Rule: Dependencies point INWARD only
- Interfaces: Inner layers define interfaces, outer layers implement
- Testability: Swap real implementations with test doubles
- Flexibility: Change databases/frameworks without touching business logic
π Next StepsΒΆ
- Apply it: Tutorial Part 1 sets up clean architecture
- Understand DI: Dependency Injection makes this work
- See it work: Domain-Driven Design for the domain layer
π Further ReadingΒΆ
- Robert C. Martin's "Clean Architecture" (book)
- The Clean Architecture (blog post)
- Neuroglia tutorials - see it in practice
Previous: β Core Concepts Index | Next: Dependency Injection β