26 Clean Architecture
26.1 Core Concept: Dependency Inversion
Clean Architecture is fundamentally about reversing the traditional dependency direction. Instead of business logic depending on infrastructure, infrastructure depends on business logic.
26.1.1 Traditional Architecture (❌ Problems)
┌─────────────────────────────────────┐
│ User Interface │
└──────────────┬──────────────────────┘
│ depends on
▼
┌─────────────────────────────────────┐
│ Business Logic │
│ • Calls database directly │
│ • Depends on specific DB (SQL) │
│ • Tightly coupled to infrastructure│
└──────────────┬──────────────────────┘
│ depends on
▼
┌─────────────────────────────────────┐
│ Data Access / Infrastructure │
│ • SQL Server specific code │
│ • File system operations │
│ • External API calls │
└─────────────────────────────────────┘
Problem: Business logic knows about database!
Can't test without database
Can't swap databases easily
26.1.2 Clean Architecture (✅ Solution)
┌─────────────────────────────────────┐
│ User Interface │
└──────────────┬──────────────────────┘
│ depends on
▼
┌─────────────────────────────────────┐
│ Application Core (Business Logic) │
│ • Defines INTERFACES │
│ • Contains domain models │
│ • Pure business rules │
│ • NO infrastructure knowledge │
└──────────────┬──────────────────────┘
│ defines interfaces
▼
┌─────────────────────────────────────┐
│ Infrastructure │
│ • IMPLEMENTS those interfaces │
│ • SQL/PostgreSQL/MongoDB │
│ • File system │
│ • External APIs │
└─────────────────────────────────────┘
▲
│ implements interfaces from
└─ Dependency points INWARD!
Benefit: Business logic is independent!
Easy to test (mock interfaces)
Easy to swap implementations
26.2 The Onion Architecture View
From your first image, let’s break down each layer:
┌─────────────────────────────────────────┐
│ OUTER LAYER (Gray) │
│ │
│ ┌─────────────────────────┐ │
│ │ User Interface │ │
│ │ • Controllers │ │
│ │ • View Models │ │
│ │ • API Endpoints │ │
│ └──────────┬──────────────┘ │
│ │ │
│ │ uses │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ MIDDLE LAYER (Orange) │ │
│ │ Application Core │ │
│ │ │ │
│ │ Domain Services │ │
│ │ Business Rules │ │
│ │ │ │
│ │ ┌────────────────────┐ │ │
│ │ │ INNER CORE │ │ │
│ │ │ • Entities │ │ │
│ │ │ • Interfaces │ │ │
│ │ └────────────────────┘ │ │
│ └──────────────────────────────┘ │
│ ▲ │
│ │ implements │
│ ┌──────────┴──────────────┐ │
│ │ Infrastructure │ │
│ │ • Repositories │ │
│ │ • Impl. Services │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
▲
│ connects to
┌─────────────┴────────────────┐
│ External Dependencies │
│ • SQL Database │
│ • Cloud Storage │
│ • Email Service │
└──────────────────────────────┘
Key Rule: Dependencies flow INWARD →
Inner layers know nothing about outer layers
26.3 The Three Core Layers
26.3.1 Layer 1: Application Core (The Heart) 🧡
What it contains:
Application Core/
├── Entities/
│ ├── Patient.py
│ ├── CTScan.py
│ └── StrokeResult.py
├── Interfaces/
│ ├── IDicomRepository.py
│ ├── IStrokeDetector.py
│ └── INotificationService.py
└── Services/
├── AspectsCalculator.py
└── ClinicalRecommendation.py
Characteristics:
- NO dependencies on external libraries (except maybe core Python)
- NO knowledge of databases, APIs, UI frameworks
- Contains pure business logic
- Defines interfaces (contracts) for what it needs
Example - Patient Entity:
# entities/patient.py
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Patient:
"""Pure domain entity - no database, no infrastructure"""
patient_id: str
name: str
date_of_birth: datetime
hn: str # Hospital Number
def get_age(self) -> int:
"""Pure business logic"""
today = datetime.now()
return today.year - self.date_of_birth.yearExample - Interface Definition:
# interfaces/idicom_repository.py
from abc import ABC, abstractmethod
from entities.ct_scan import CTScan
class IDicomRepository(ABC):
"""Interface defined in Application Core"""
@abstractmethod
def get_scan_by_id(self, scan_id: str) -> CTScan:
"""Get CT scan - HOW is not specified here!"""
pass
@abstractmethod
def save_scan(self, scan: CTScan) -> None:
"""Save CT scan - Could be SQL, MongoDB, file system!"""
passThe core doesn’t care HOW, just WHAT!
26.3.2 Layer 2: Infrastructure (The Implementation) 🟡
What it contains:
Infrastructure/
├── Repositories/
│ ├── SqlDicomRepository.py ← Implements IDicomRepository
│ ├── PacsDicomRepository.py ← Also implements IDicomRepository
│ └── FileDicomRepository.py ← Another implementation!
├── Services/
│ ├── EmailNotificationService.py ← Implements INotificationService
│ └── SmsNotificationService.py ← Alternative implementation
└── ExternalApis/
└── PacsConnector.py
Characteristics: - Implements interfaces from Application Core - Contains all infrastructure code: databases, files, APIs, etc. - Depends on Application Core - Can be swapped without changing business logic
Example - SQL Implementation:
# infrastructure/repositories/sql_dicom_repository.py
from sqlalchemy.orm import Session
from interfaces.idicom_repository import IDicomRepository # ← From Core!
from entities.ct_scan import CTScan # ← From Core!
class SqlDicomRepository(IDicomRepository):
"""SQL implementation of the interface"""
def __init__(self, db_session: Session):
self.db = db_session
def get_scan_by_id(self, scan_id: str) -> CTScan:
"""Implementation uses SQL"""
result = self.db.query(CTScanModel).filter_by(id=scan_id).first()
return self._map_to_entity(result)
def save_scan(self, scan: CTScan) -> None:
"""Implementation uses SQL"""
model = self._map_to_model(scan)
self.db.add(model)
self.db.commit()Example - PACS Implementation:
# infrastructure/repositories/pacs_dicom_repository.py
from pynetdicom import AE
from interfaces.idicom_repository import IDicomRepository # ← Same interface!
from entities.ct_scan import CTScan
class PacsDicomRepository(IDicomRepository):
"""PACS implementation of the SAME interface"""
def __init__(self, pacs_host: str, pacs_port: int):
self.ae = AE()
self.host = pacs_host
self.port = pacs_port
def get_scan_by_id(self, scan_id: str) -> CTScan:
"""Implementation uses DICOM network protocol"""
# Query PACS using DIMSE
dataset = self._query_pacs(scan_id)
return self._convert_to_entity(dataset)
def save_scan(self, scan: CTScan) -> None:
"""Implementation sends to PACS"""
dataset = self._convert_to_dicom(scan)
self._store_to_pacs(dataset)Same interface, completely different implementation!
26.3.3 Layer 3: User Interface (The Presentation) 🔵
What it contains:
UserInterface/
├── Api/
│ ├── Controllers/
│ │ ├── StrokeDetectionController.py
│ │ └── PatientController.py
│ └── ViewModels/
│ ├── StrokeResultViewModel.py
│ └── PatientViewModel.py
└── Web/
├── templates/
└── static/
Characteristics: - Handles user interaction (HTTP requests, CLI, GUI) - Depends on Application Core - Converts between user format and domain format - NO business logic here!
Example - API Controller:
# api/controllers/stroke_detection_controller.py
from fastapi import APIRouter, Depends
from interfaces.idicom_repository import IDicomRepository
from services.aspects_calculator import AspectsCalculator
router = APIRouter()
@router.post("/detect-stroke")
def detect_stroke(
study_id: str,
# Dependency injection - we don't care which implementation!
dicom_repo: IDicomRepository = Depends(get_dicom_repository)
):
"""UI layer - just coordinates, no business logic"""
# Get data using interface
ct_scan = dicom_repo.get_scan_by_id(study_id)
# Use business logic from Application Core
calculator = AspectsCalculator()
aspects_score = calculator.calculate(ct_scan)
# Return as JSON (UI concern)
return {
"study_id": study_id,
"aspects_score": aspects_score,
"status": "completed"
}26.4 Dependency Flow Visualization
From your second image, here’s the dependency direction:
┌───────────────────────────────────────────────┐
│ User Interface (Blue) │
│ • FastAPI routes │
│ • React frontend │
└──────────────────┬────────────────────────────┘
│
│ Solid arrow = Compile-time dependency
▼
┌──────────────────────────────────────────┐
│ Application Core (Red) │
│ • Business Logic │
│ • Entities │
│ • Interfaces (contracts) │
└──────────────────────────────────────────┘
▲ ▲
│ │
┌─────────┴──────┐ ┌──────┴────────┐
│ │ │ │
│ Implements │ │ Uses (test) │
│ │ │ │
┌────────┴────────┐ ┌──┴───┴──────┐
│ Infrastructure │ │ Tests │
│ (Yellow) │ │ (Purple) │
└─────────────────┘ └─────────────┘
Key insight: Infrastructure knows about Core
Core doesn't know about Infrastructure!
26.5 Real-World Radiology AI Example
26.5.1 Scenario: CT Stroke Detection System
Application Core (Business Logic):
# core/entities/ct_scan.py
@dataclass
class CTScan:
scan_id: str
patient_id: str
acquisition_date: datetime
modality: str
image_data: np.ndarray # Pure data, no storage concern
# core/interfaces/iscan_repository.py
class IScanRepository(ABC):
@abstractmethod
def get_scan(self, scan_id: str) -> CTScan:
pass
# core/services/stroke_detector.py
class StrokeDetector:
"""Pure business logic - no infrastructure!"""
def __init__(self, model_path: str):
self.model = self._load_model(model_path)
def calculate_aspects(self, scan: CTScan) -> int:
"""Business rule: ASPECTS scoring"""
predictions = self.model.predict(scan.image_data)
score = self._calculate_score(predictions)
return score
def generate_recommendation(self, aspects_score: int) -> str:
"""Business rule: Clinical recommendation"""
if aspects_score >= 8:
return "Consider thrombolysis if within window"
elif aspects_score >= 6:
return "Consult stroke team for case discussion"
else:
return "Large infarct - thrombolysis not recommended"Infrastructure (Multiple Implementations):
# infrastructure/repositories/pacs_repository.py
class PacsRepository(IScanRepository):
"""Production: Get from hospital PACS"""
def get_scan(self, scan_id: str) -> CTScan:
# Complex DICOM network code
pass
# infrastructure/repositories/file_repository.py
class FileRepository(IScanRepository):
"""Development: Get from local files"""
def get_scan(self, scan_id: str) -> CTScan:
# Simple file reading
pass
# infrastructure/repositories/mock_repository.py
class MockRepository(IScanRepository):
"""Testing: Return fake data"""
def get_scan(self, scan_id: str) -> CTScan:
return CTScan(
scan_id="TEST001",
patient_id="P001",
acquisition_date=datetime.now(),
modality="CT",
image_data=np.zeros((512, 512)) # Fake image
)User Interface (API):
# api/main.py
from fastapi import FastAPI, Depends
app = FastAPI()
def get_repository() -> IScanRepository:
"""Dependency injection - choose implementation"""
if settings.ENV == "production":
return PacsRepository()
elif settings.ENV == "development":
return FileRepository()
else:
return MockRepository()
@app.post("/analyze")
def analyze_stroke(
scan_id: str,
repo: IScanRepository = Depends(get_repository)
):
# Get scan (don't care from where!)
scan = repo.get_scan(scan_id)
# Run business logic
detector = StrokeDetector(model_path="model.h5")
aspects_score = detector.calculate_aspects(scan)
recommendation = detector.generate_recommendation(aspects_score)
return {
"aspects_score": aspects_score,
"recommendation": recommendation
}26.6 Benefits of Clean Architecture
26.6.1 1. Easy Testing
# tests/test_stroke_detector.py
def test_aspects_calculation():
# Use mock repository - no real PACS needed!
mock_repo = MockRepository()
scan = mock_repo.get_scan("TEST001")
detector = StrokeDetector(model_path="test_model.h5")
score = detector.calculate_aspects(scan)
assert score >= 0 and score <= 10No database, no PACS, no network - just pure logic testing!
26.6.2 2. Easy Swapping
# Swap from PACS to cloud storage:
# Before:
repository = PacsRepository(host="pacs.hospital.local")
# After (just change ONE line):
repository = CloudRepository(bucket="ct-scans-asia")
# Business logic stays EXACTLY the same!
detector = StrokeDetector(model_path="model.h5")
scan = repository.get_scan(scan_id) # ← Same interface
result = detector.calculate_aspects(scan) # ← Same logic26.6.3 3. Independent Development
Team 1: Application Core Team 2: Infrastructure
┌─────────────────────┐ ┌──────────────────────┐
│ Define interfaces │ ──────> │ Implement interfaces │
│ Write business │ │ Build PACS connector │
│ logic │ │ Set up database │
│ │ │ │
│ Can work with │ │ Can work separately │
│ mock data │ │ as long as interface │
│ │ │ is followed │
└─────────────────────┘ └──────────────────────┘
Both teams work independently!
Integration is just wiring up interfaces
26.6.4 4. Framework Independence
Your Application Core doesn’t depend on: - FastAPI - SQLAlchemy
- pydicom - Any external library!
Today: FastAPI + PostgreSQL + PACS
Tomorrow: Django + MongoDB + Cloud Storage
Next year: GraphQL + DynamoDB + S3
Application Core stays the same! ✨
26.7 Project Structure Example
ct-stroke-ai/
├── core/ ← Application Core
│ ├── entities/
│ │ ├── patient.py
│ │ ├── ct_scan.py
│ │ └── stroke_result.py
│ ├── interfaces/
│ │ ├── iscan_repository.py
│ │ ├── imodel_service.py
│ │ └── inotification_service.py
│ └── services/
│ ├── stroke_detector.py
│ ├── aspects_calculator.py
│ └── clinical_advisor.py
│
├── infrastructure/ ← Infrastructure
│ ├── repositories/
│ │ ├── pacs_repository.py
│ │ ├── sql_repository.py
│ │ └── file_repository.py
│ ├── services/
│ │ ├── tensorflow_model_service.py
│ │ └── email_notification_service.py
│ └── database/
│ └── postgres_context.py
│
├── api/ ← User Interface
│ ├── controllers/
│ │ └── stroke_controller.py
│ ├── viewmodels/
│ │ └── stroke_result_vm.py
│ └── main.py
│
└── tests/ ← Tests
├── unit/
│ └── test_stroke_detector.py
└── integration/
└── test_api.py
26.8 Key Principles Summary
26.8.1 1. Dependency Inversion Principle
❌ WRONG:
Core → depends on → Infrastructure
✅ RIGHT:
Core ← depended on by ← Infrastructure
26.8.2 2. Interface Segregation
Core defines interfaces:
interface IScanRepository {
get_scan()
save_scan()
}
Infrastructure implements:
class PacsRepository implements IScanRepository
class FileRepository implements IScanRepository
class CloudRepository implements IScanRepository
26.8.3 3. Single Responsibility
✅ Application Core: Business rules ONLY
✅ Infrastructure: Technical implementation ONLY
✅ UI: User interaction ONLY
26.9 When to Use Clean Architecture?
✅ Use when:
- Complex business logic
- Long-term project (years)
- Multiple developers/teams
- Need flexibility to change tech stack
- High testability requirements
- Your radiology AI systems! 🏥
❌ Overkill for:
- Simple CRUD apps
- Prototypes
- Short-lived projects
- Solo weekend projects
26.10 Summary
Clean Architecture = Onion Architecture = Hexagonal Architecture
Core idea: Business logic at the center, everything else depends on it.
┌─────────────────────────────────────┐
│ The Dependency Rule: │
│ │
│ Source code dependencies │
│ point INWARD only │
│ │
│ Outer circles → Inner circles ✅ │
│ Inner circles → Outer circles ❌ │
└─────────────────────────────────────┘
Result:
- Testable business logic
- Flexible infrastructure
- Independent of frameworks
- Independent of databases
- Independent of UI

