Code philosophy
Against excess layers
12 October 2026 · 5 min read
The Illusion of Architectural Purity
In modern software development, layering is celebrated as an indisputable virtue: separate the HTTP router from the application service, separate the service from the domain model, separate the domain model from the repository, and separate the repository from the database driver.
While clean separation of concerns is vital, software teams often fall into the trap of dogmatic over-engineering. They introduce “empty pass-through layers” that do nothing more than wrap an existing call under a pretentious class name:
class OrderService:
def __init__(self, repo: OrderRepository):
self.repo = repo
def get_order(self, order_id: int) -> Order:
# A hollow bridge: zero business logic, zero transformation
return self.repo.get_by_id(order_id)
This is not architectural abstraction—it is meaningless indirection. It introduces mental tax, inflates file counts, complicates stack traces, and forces engineers to navigate three layers of dummy interfaces just to read a simple database row.
The Acid Test for Any Layer
To determine whether an abstraction is earning its keep, ask this single question:
“If we delete this layer and wire its caller directly to its callee, what concrete capability do we lose?”
- If the answer is merely “a clean name” or “standardized symmetry”: The layer is redundant and should be deleted immediately.
- If the answer is “a boundary between changing transport contracts, a unified caching policy, or a swappable interface for testing”: The layer is justified and should remain.
A Real-World Horror Story
On a large-scale data ingestion system I once audited, the team had enforced an uncompromising seven-layer architecture:
HTTP Handler -> Request Adapter -> Ingestion Service -> Domain Validator -> Entity Factory -> Persistence Gateway -> Database Session.
When an intermittent data corruption bug surfaced in production, tracking down where an attribute was mutated took two full weeks. The code wasn’t complex—it was simply buried across thousands of lines of pass-through boilerplate, where every layer presented a potential hiding spot for unexpected state changes.
When we flattened the pipeline to three distinct, purposeful layers (API Router -> Ingestion Pipeline -> Database Repository), the codebase shrank by 45%, debugging became straightforward, and query execution times improved due to reduced object allocations.
When Layers Are Actually Justified
A layer earns its existence when it encapsulates a genuine architectural decision:
- Separating Independent Lifecycles: Isolating web transport semantics (HTTP headers, status codes) from core domain calculations that run across CLI, workers, and API handlers.
- Centralizing Critical Policy: Enforcing strict, cross-cutting rules—such as ensuring every persistent write invalidates a Redis cache tag or emits a domain audit event.
- True Swappability: Enabling dependency substitution for in-memory integration testing (e.g., swapping a live S3 storage backend for an in-memory mock).
The golden rule: Never build a layer to satisfy an abstract pattern. Build a layer only when real domain complexity demands it.