Skip to content
Architecture

Clean Architecture Meets a Real Deadline

The one load-bearing idea under all the concentric circles, the interface-with-one-implementation tax, and the three-folder dose that keeps domains pure without nine-file one-line changes.

4 min read Updated Sep 3, 2026
Clean Architecture Meets a Real Deadline

I own the books. Clean Architecture, Hexagonal, Onion — I've read them, drawn the concentric circles on whiteboards, and shipped systems arranged both ways. So take this as friendly fire, not ignorance: most codebases applying "clean architecture" are paying enterprise prices for a startup problem, and a smaller, better-aimed dose gets you nearly all the value. Here's the dose.

Domain, application, infrastructure and delivery layers — with dependencies pointing inward

The one idea that's actually load-bearing

Strip the diagrams and every one of these architectures says the same sentence: business logic should not depend on delivery mechanisms or infrastructure details. Your pricing rules shouldn't import an HTTP request. Your order lifecycle shouldn't care whether storage is MySQL or Postgres. Dependencies point inward, toward the domain.

That idea is genuinely valuable — it's what makes logic testable without booting a framework, and portable when infrastructure changes. Everything else — the four ring names, the interface-for-everything reflex, the DTO relay race between layers — is technique in service of that idea, and techniques are optional per situation. The trouble starts when teams enforce the techniques and forget the idea.

Where the ceremony goes wrong

The pathology has a recognizable shape. To add one field to one screen you touch: the entity, the repository interface, the repository implementation, two DTOs, a mapper, a use-case class, a presenter, and a view model — nine files, zero business decisions. Meanwhile the actual domain logic is four lines inside a "use case" that's 90% object-shoveling. Abstractions multiplied faster than the reasons for them.

My favorite tell: the interface with exactly one implementation, forever. IOrderRepository with OrderRepository right next to it, never a second implementation, never swapped in tests (the tests use the real database anyway — as they often should). That interface costs navigation, indirection and file count, and buys a hypothetical that never arrived. Interfaces earn their keep at genuine seams: payment providers you actually swap (e-invoice integrators, PSPs, notification channels), clocks and randomness in test-sensitive code, boundaries between modules. Behind those seams? Concrete classes are fine. Your framework's container can bind concretions.

The dose that works

Here's the layering I actually ship, in Laravel or .NET, and it's three folders, not four rings:

Modules/Billing/
├── Domain/          # entities, value objects, domain services — ZERO framework imports
│   ├── Invoice.php
│   ├── Money.php
│   └── InvoiceNumberSequence.php
├── Application/     # use cases / actions — orchestrate domain + ports
│   ├── IssueInvoice.php
│   └── Ports/PaymentGateway.php   # interface — a REAL seam (Stripe today, adyen tomorrow)
└── Infrastructure/  # Eloquent models, gateway implementations, mail, queues
    ├── EloquentInvoiceRepository.php
    └── StripePaymentGateway.php
# Controllers/routes live in the framework's normal spots — delivery is thin anyway

Three rules govern it, and they're checkable by architecture tests, not vigilance:

  1. Domain imports nothing from the framework. This is the hill worth dying on. If Invoice::applyLateFee() can be unit-tested with new Invoice(...) and no container, you've won the war that matters.
  2. Application classes are verbs. IssueInvoice, CancelSubscription — one public method, orchestrating domain objects and ports. These are your transaction boundaries and your audit log's vocabulary.
  3. Infrastructure is allowed to be boring and framework-native. Eloquent models with all their magic, HTTP clients, queue jobs — no purity contest here. The contamination rule is one-directional: infra may know domain; domain may never know infra.

Notice what's missing: no mandatory repository interface per entity (use them at module seams and genuinely swappable stores), no DTO between every layer (map at the edges — HTTP in, HTTP out — and pass domain objects inside), no presenter/view-model theater for JSON APIs that serialize a resource class just fine.

Calibrating to the codebase in front of you

  • CRUD-heavy admin panels: skip nearly all of it. Controller → Eloquent → view is the correct architecture for a screen that saves what it shows. Ceremony here is negative-value.
  • A real domain core (pricing, billing, scheduling, risk): full dose for that module — pure domain objects, explicit use cases, ports at the seams. This is also where CQRS tends to pay off, and the two compose beautifully.
  • Integrations-heavy edges (PSPs, e-invoice, push providers): ports and adapters earn their keep precisely here, because the second implementation genuinely arrives.

Uneven architecture — strict where the business lives, relaxed where it doesn't — isn't inconsistency. It's judgment. The concentric circles were always a map, and the map was never the territory; the territory is a team shipping correct business logic fast, for years, without fearing their own codebase. Aim at that, and let the rings serve you instead of the reverse.

If your team is nine files deep into every one-line change — or zero files deep into a domain that deserves better — an architecture review can find your dose.

Keep reading

Related articles