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.