Skip to main content
Clean Architecture 70 XP · 7 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.

Showing
Ad (728×90)

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.

Done with this lesson?

Mark it complete to earn XP and track your progress.