How to Build Large, Maintainable Python Systems with Clean Architecture: Concepts & Real-World Examples
Why Clean Architecture?
As software grows, so does its complexity—leading to framework lock-in, untestable logic, and tangled code.
Clean Architecture keeps business rules independent of frameworks, databases, and UI.
“Your business logic should be testable and changeable without touching your framework or database.”
The Key Principle
Your business logic should be testable without binding to any framework.
If you can run your core logic—entities and use cases—in pure Python, you’re on the right track.
The Layers in Practice
Layered separation makes your codebase maintainable and testable:
- Entities: Plain business objects (e.g.
Car,Rental) - Use Cases: Application logic (e.g. "rent a car", "list all cars")
- Repositories/Interfaces: Abstractions for data access or services
- Frameworks/Drivers: Django, Flask, databases, UI code
UML Class Diagram
classDiagram
class Car {
+int car_id
+str make
+str model
+bool is_available
}
class Rental {
+int rental_id
+int car_id
+int user_id
+str start_date
+str end_date
}
class CarRepository {
+save(car)
+get_by_id(car_id)
+get_all()
+delete(car_id)
}
class RentalRepository {
+save(rental)
+get_by_id(rental_id)
+get_all()
+delete(rental_id)
}
class CarCRUDUseCase {
-car_repository
+create_car(car_id, make, model)
+get_car(car_id)
+list_cars()
+update_car(car_id, make, model, is_available)
+delete_car(car_id)
}
class RentCarUseCase {
-car_repository
-rental_repository
+execute(car_id, user_id, start_date, end_date)
}
class DjangoCarRepository {
+save(car)
+get_by_id(car_id)
+get_all()
+delete(car_id)
}
class MongoCarRepository {
+save(car)
+get_by_id(car_id)
+get_all()
+delete(car_id)
}
class DjangoRentalRepository
class MongoRentalRepository
CarCRUDUseCase --> CarRepository
RentCarUseCase --> CarRepository
RentCarUseCase --> RentalRepository
DjangoCarRepository --|> CarRepository
MongoCarRepository --|> CarRepository
DjangoRentalRepository --|> RentalRepository
MongoRentalRepository --|> RentalRepository
Flow Diagram: Class Interaction at Runtime
How does a user action (“rent a car”) flow through the layers?
sequenceDiagram
participant User
participant UI as UI/View (Django/Flask)
participant UC as RentCarUseCase
participant CarRepo as CarRepository
participant RentalRepo as RentalRepository
participant DB as DB/Framework
User->>UI: Submit rent car form (car_id, user_id, dates)
UI->>UC: execute(car_id, user_id, start_date, end_date)
UC->>CarRepo: get_by_id(car_id)
CarRepo->>DB: Query car by id
CarRepo-->>UC: Car entity
UC->>CarRepo: save(car) (mark as unavailable)
CarRepo->>DB: Update car
UC->>RentalRepo: save(rental)
RentalRepo->>DB: Insert rental record
RentalRepo-->>UC: Rental entity
UC-->>UI: Return Rental entity
UI-->>User: Render result (success page/HTML)
Sample Code: Each Layer in Python
1. Entities (Business Objects)
# entities/car.py
class Car:
def __init__(self, car_id, make, model, is_available=True):
self.car_id = car_id
self.make = make
self.model = model
self.is_available = is_available
# entities/rental.py
class Rental:
def __init__(self, rental_id, car_id, user_id, start_date, end_date):
self.rental_id = rental_id
self.car_id = car_id
self.user_id = user_id
self.start_date = start_date
self.end_date = end_date
2. Use Cases (Application Logic)
# use_cases/car_crud.py
class CarCRUDUseCase:
def __init__(self, car_repository):
self.car_repository = car_repository
def create_car(self, car_id, make, model):
car = Car(car_id=car_id, make=make, model=model, is_available=True)
self.car_repository.save(car)
return car
def get_car(self, car_id):
return self.car_repository.get_by_id(car_id)
def list_cars(self):
return self.car_repository.get_all()
def update_car(self, car_id, make, model, is_available):
car = Car(car_id=car_id, make=make, model=model, is_available=is_available)
self.car_repository.save(car)
return car
def delete_car(self, car_id):
self.car_repository.delete(car_id)
# use_cases/rent_car.py
class RentCarUseCase:
def __init__(self, car_repository, rental_repository):
self.car_repository = car_repository
self.rental_repository = rental_repository
def execute(self, car_id, user_id, start_date, end_date):
car = self.car_repository.get_by_id(car_id)
if not car or not car.is_available:
raise Exception("Car not available")
car.is_available = False
self.car_repository.save(car)
rental = Rental(
rental_id=None, car_id=car_id, user_id=user_id,
start_date=start_date, end_date=end_date
)
self.rental_repository.save(rental)
return rental
3. Repository Interfaces and Implementations
# interface_adapters/car_repository.py
class CarRepository:
def save(self, car): ...
def get_by_id(self, car_id): ...
def get_all(self): ...
def delete(self, car_id): ...
Django ORM Example:
# frameworks/django_repositories.py
from entities.car import Car
from myapp.models import CarModel
class DjangoCarRepository(CarRepository):
def save(self, car):
obj, _ = CarModel.objects.update_or_create(
pk=car.car_id,
defaults={'make': car.make, 'model': car.model, 'is_available': car.is_available}
)
return car
def get_by_id(self, car_id):
try:
obj = CarModel.objects.get(pk=car_id)
return Car(car_id=obj.pk, make=obj.make, model=obj.model, is_available=obj.is_available)
except CarModel.DoesNotExist:
return None
def get_all(self):
return [
Car(
car_id=obj.pk,
make=obj.make,
model=obj.model,
is_available=obj.is_available
)
for obj in CarModel.objects.all()
]
def delete(self, car_id):
CarModel.objects.filter(pk=car_id).delete()
MongoDB Example:
# frameworks/mongo_car_repository.py
from entities.car import Car
from interface_adapters.car_repository import CarRepository
class MongoCarRepository(CarRepository):
def __init__(self, db):
self.collection = db['cars']
def save(self, car):
self.collection.update_one(
{'car_id': car.car_id},
{'$set': {'make': car.make, 'model': car.model, 'is_available': car.is_available}},
upsert=True
)
return car
def get_by_id(self, car_id):
doc = self.collection.find_one({'car_id': car_id})
if not doc:
return None
return Car(
car_id=doc['car_id'],
make=doc['make'],
model=doc['model'],
is_available=doc.get('is_available', True)
)
def get_all(self):
return [
Car(
car_id=doc['car_id'],
make=doc['make'],
model=doc['model'],
is_available=doc.get('is_available', True)
)
for doc in self.collection.find()
]
def delete(self, car_id):
self.collection.delete_one({'car_id': car_id})
4. UI Layer Example: Django View and Template
# myapp/views.py
from django.shortcuts import render
from use_cases.car_crud import CarCRUDUseCase
from frameworks.django_repositories import DjangoCarRepository
def car_list_view(request):
use_case = CarCRUDUseCase(DjangoCarRepository())
cars = use_case.list_cars()
return render(request, "car_list.html", {"cars": cars})
Django Template (car_list.html):
<!DOCTYPE html>
<html>
<head>
<title>Car List</title>
</head>
<body>
<h1>Available Cars</h1>
<table border="1">
<tr>
<th>ID</th>
<th>Make</th>
<th>Model</th>
<th>Available</th>
</tr>
{% for car in cars %}
<tr>
<td>{{ car.car_id }}</td>
<td>{{ car.make }}</td>
<td>{{ car.model }}</td>
<td>{{ car.is_available|yesno:"Yes,No" }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
5. Flask API Example
from flask import Flask, request, jsonify
from pymongo import MongoClient
from use_cases.car_crud import CarCRUDUseCase
from frameworks.mongo_car_repository import MongoCarRepository
app = Flask(__name__)
client = MongoClient("mongodb://localhost:27017")
db = client["car_rental"]
car_repo = MongoCarRepository(db)
car_crud = CarCRUDUseCase(car_repo)
@app.route('/cars', methods=['POST'])
def create_car():
data = request.json
car = car_crud.create_car(data['car_id'], data['make'], data['model'])
return jsonify({"car_id": car.car_id, "make": car.make, "model": car.model, "is_available": car.is_available}), 201
@app.route('/cars', methods=['GET'])
def list_cars():
cars = car_crud.list_cars()
return jsonify([
{"car_id": c.car_id, "make": c.make, "model": c.model, "is_available": c.is_available}
for c in cars
])
6. Unit Testing Use Cases
import unittest
from entities.car import Car
from use_cases.car_crud import CarCRUDUseCase
class InMemoryCarRepository:
def __init__(self):
self._cars = {}
def save(self, car): self._cars[car.car_id] = car; return car
def get_by_id(self, car_id): return self._cars.get(car_id)
def get_all(self): return list(self._cars.values())
def delete(self, car_id): self._cars.pop(car_id, None)
class TestCarCRUDUseCase(unittest.TestCase):
def setUp(self):
self.repo = InMemoryCarRepository()
self.usecase = CarCRUDUseCase(self.repo)
def test_create_car(self):
car = self.usecase.create_car(1, 'Honda', 'Civic')
self.assertEqual(car.car_id, 1)
self.assertEqual(car.make, 'Honda')
def test_list_cars(self):
self.usecase.create_car(2, 'Toyota', 'Yaris')
cars = self.usecase.list_cars()
self.assertEqual(len(cars), 1)
self.assertEqual(cars[0].model, 'Yaris')
if __name__ == '__main__':
unittest.main()
Key Takeaways
- UI (Flask/Django/HTML): Just collects input, calls use cases, and presents results.
- Business logic is fully testable, reusable, and not coupled to frameworks.
- Repositories abstract data storage—you can swap MongoDB, SQL, or even in-memory for tests.
- Templates only display data—never mix business rules into your HTML.
If you can run all your core logic and tests without any web server or database, you’re building for the long run.
Want a deeper dive, form handling, or even an end-to-end repo? Let me know!
Get in Touch with us
Related Posts
- How Agentic AI and MCP Servers Work Together: The Next Step in Intelligent Automation
- DevOps in Django E-Commerce System with DRF and Docker
- How AI Can Solve Real Challenges in Agile Development
- Connecting TAK and Wazuh for Real-Time Threat Awareness
- Scaling Wazuh for Multi-Site Network Security Monitoring
- Why ERP Projects Fail — and How to Avoid It
- How to Build Strong Communities with Technology
- How AI Can Make Open Zoos More Fun, Smart, and Educational
- How to Choose the Right Recycling Factory for Industrial Scrap
- Understanding Modern Database Technologies — and How to Choose the Right One
- The Future Is at the Edge — Understanding Edge & Distributed Computing in 2025
- NVIDIA and the Two Waves: From Crypto to AI — The Art of Riding a Bubble
- From Manual Checks to AI-Powered Avionics Maintenance
- Automated Certificate Generator from XLSX Templates
- Introducing SimpliPOS (COFF POS) — A Café-Focused POS System
- Building a Local-First Web App with Alpine.js — Fast, Private, and Serverless
- Carbon Footprint Calculator (Recycling) — Measuring CO₂ Savings in Recycling Operations
- Recycle Factory Tools: A Smarter Way to Track Scrap Operations
- Running Form Coach — Cadence Metronome, Tapper, Drills, Posture Checklist
- How to Build a Carbon Credit Calculator for Your Business













