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:

  1. Entities: Plain business objects (e.g. Car, Rental)
  2. Use Cases: Application logic (e.g. "rent a car", "list all cars")
  3. Repositories/Interfaces: Abstractions for data access or services
  4. 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

Chat with Us on LINE

iiitum1984

Speak to Us or Whatsapp

(+66) 83001 0222

Related Posts

Our Products