Skip to main content

Sign in to CleanKata

Track your progress, earn XP, and unlock every lesson.

By signing in you agree to our Terms of Use and Privacy Policy.

Clean Architecture70 XP7 min

The Devil Is in the Implementation Details

The best architecture fails without disciplined use of access modifiers — the compiler is your strongest ally in enforcing Clean Architecture rules across the team.

Why this matters

Simon Brown identifies four levels at which architecture can be implemented. Understanding which level your system is at — and which level it needs to be at — is a key architectural judgment:

  • 1.Architecture as an idea — diagrams on a wiki, boxes and arrows. No code reflects the diagram. Any developer can violate the diagram at any time. Zero enforcement.
  • 2.Architecture in packages — the directory structure reflects the diagram. But all classes are public. Any class can import any other class. Enforcement is entirely social — "please don't bypass the service."
  • 3.Architecture enforced by access modifiers — package-private internals, only public API is exposed. The compiler makes violations impossible. This is the minimum for a real architectural boundary.
  • 4.Architecture enforced by deployment units — separate JARs, packages, or services. The runtime makes violations impossible. Used when teams or scaling require it.

Most codebases are at level 2 and believe they are at level 3. The difference between levels 2 and 3 is the difference between aspirational architecture and actual architecture. Team size and discipline determine which level is appropriate — but no team is disciplined enough to permanently resist the convenience of a direct import.

✗The problem

Perfect architectural diagram. In practice, every class is public. A developer imports OrderRepository directly into a background task — unnoticed, no compiler complaint.

✗ Bad — Level 2: Architecture in packages only

# orders/order_repository.py  ← should be internal, but is public
class OrderRepository:
    def find_by_id(self, order_id: str) -> dict:
        return {"id": order_id, "total": 99.0, "status": "pending"}

    def mark_shipped(self, order_id: str) -> None:
        pass

# orders/order_service.py  ← intended public API
class OrderService:
    def get_order(self, order_id: str) -> dict:
        return OrderRepository().find_by_id(order_id)

# tasks/shipping_task.py  ← Celery worker — bypasses OrderService!
from orders.order_repository import OrderRepository  # ← no compiler warning

from celery import Celery
app = Celery("tasks")

@app.task
def process_shipment(order_id: str):
    repo  = OrderRepository()
    order = repo.find_by_id(order_id)   # bypasses all business validation
    repo.mark_shipped(order_id)         # side effects without domain rules

# Architecture diagram: correct. Code: level 2. Violation: undetected.
// orders/OrderRepository.ts  ← should be internal, but exported
export class OrderRepository {
  findById(orderId: string): { id: string; total: number; status: string } {
    return { id: orderId, total: 99.0, status: "pending" };
  }
  markShipped(orderId: string): void { /* database write */ }
}

// orders/OrderService.ts  ← intended public API
export class OrderService {
  getOrder(orderId: string) { return new OrderRepository().findById(orderId); }
}

// workers/ShippingWorker.ts  ← background job — bypasses OrderService!
import { OrderRepository } from "../orders/OrderRepository"; // ← no compiler warning

export class ShippingWorker {
  process(orderId: string): void {
    const repo = new OrderRepository();
    repo.markShipped(orderId); // no business validation, no domain rules
  }
}

// Architecture diagram: correct. Code: level 2. Violation: undetected.

✓The solution

__all__ (Python) or barrel index.ts (TypeScript) makes OrderRepository invisible to the rest of the system. The architecture is code, not a diagram.

✓ Good — Level 3: Architecture enforced by access modifiers

# orders/__init__.py  ← enforced public API
from orders._order_service import OrderService

__all__ = ["OrderService"]  # only OrderService is the public contract

# orders/_order_repository.py  ← private (underscore + not in __all__)
class _OrderRepository:
    def find_by_id(self, order_id: str) -> dict:
        return {"id": order_id, "total": 99.0, "status": "pending"}

    def mark_shipped(self, order_id: str) -> None:
        pass

# orders/_order_service.py  ← private implementation, exported via __init__
from orders._order_repository import _OrderRepository

class OrderService:
    def ship_order(self, order_id: str) -> None:
        order = _OrderRepository().find_by_id(order_id)
        if order["status"] != "pending":
            raise ValueError("Cannot ship an order that is not pending")
        _OrderRepository().mark_shipped(order_id)

# tasks/shipping_task.py  ← must use the public API
from orders import OrderService  # _OrderRepository is not in __all__

from celery import Celery
app = Celery("tasks")

@app.task
def process_shipment(order_id: str):
    OrderService().ship_order(order_id)  # goes through business validation

# Architecture is enforced by __all__ + linting rules (flake8-import-order).
# A violation raises a linting error at development time, not production.
// orders/OrderRepository.ts  ← NOT exported from barrel (internal only)
class OrderRepository {
  findById(orderId: string): { id: string; total: number; status: string } {
    return { id: orderId, total: 99.0, status: "pending" };
  }
  markShipped(orderId: string): void { /* database write */ }
}
export { OrderRepository }; // visible inside orders/ module only

// orders/OrderService.ts
import { OrderRepository } from "./OrderRepository";

export class OrderService {
  shipOrder(orderId: string): void {
    const order = new OrderRepository().findById(orderId);
    if (order.status !== "pending") {
      throw new Error("Cannot ship an order that is not pending");
    }
    new OrderRepository().markShipped(orderId);
  }
}

// orders/index.ts  ← barrel: only public surface
export { OrderService } from "./OrderService";
// OrderRepository is NOT re-exported — invisible to the rest of the system

// workers/ShippingWorker.ts  ← must use the barrel
import { OrderService } from "../orders"; // barrel import only

export class ShippingWorker {
  process(orderId: string): void {
    new OrderService().shipOrder(orderId); // goes through business validation
  }
}

// ESLint: "import/no-internal-modules" rule.
// Attempting to import "../orders/OrderRepository" directly → lint error
// at development time, not production. Architecture is code, not a diagram.

💡Key takeaway

An architecture that can only be enforced by asking developers to be careful is not an architecture — it is a suggestion. Use the compiler. In Python, use __all__ and underscore prefixes. In TypeScript, use barrel index.ts files and ESLint's import/no-internal-modules rule. The difference between a diagram and an architecture is enforcement.

🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.

Hint: An architecture that can only be enforced by asking developers to be careful is not an architecture — it's a suggestion. Use the compiler.

✗ Your version

The Devil Is in the Implementation Details — CleanKata — CleanKata