Skip to content
9 min read·The TechKis team

Clean Architecture in real projects — what survives contact with a deadline

Clean Architecture looks elegant on a diagram. In production it gets messy. Here's what the layers actually buy you, where the theory breaks down, and the pragmatic version we apply on real client work.

  • Architecture
  • Clean Architecture
  • Backend
  • Software Design

Clean Architecture has a reputation problem. Half the engineers who've tried it say it saved their codebase. The other half say it buried a simple feature under six layers of interfaces and made onboarding a nightmare. Both groups are right — about different implementations.

The theory is sound. The diagrams are clean. The problem is that the gap between the diagram and a real project with a real deadline is where most implementations go wrong. This is what we've learned about what actually survives that gap.

What Clean Architecture is actually trying to do

Before the layers, the dependency rule, the use cases — the core idea is simple: your business logic should not know about your infrastructure.

Your domain model shouldn't know whether data comes from Postgres or MongoDB. Your use cases shouldn't know whether they're called from an HTTP handler or a CLI command or a message queue consumer. Your business rules shouldn't import your ORM.

That's it. Everything else — the concentric circles, the interface adapters, the dependency inversion — is in service of that one idea.

When you keep that idea in mind, the architecture becomes a tool. When you lose sight of it and start following the diagram mechanically, it becomes ceremony.

The dependency rule in practical Clean ArchitectureA dependency direction diagram where HTTP, database and payment infrastructure point into use cases, and use cases point into the domain model.Use casesDomainHTTPPostgresStripeBUSINESS RULES DO NOT IMPORT INFRASTRUCTURE
Figure. The dependency rule in practice — HTTP, database and payment infrastructure point inward toward use cases and domain logic, never the other way around.

The layers, and what they actually do

Clean Architecture layers with dependencies pointing inwardConcentric layers showing domain at the center, use cases around it, interface adapters outside, and infrastructure at the edge with arrows pointing inward.DomainRulesUse casesInterface adaptersInfrastructureFrameworks, DB, UIDependencies point inward
Figure. Clean Architecture's concentric circles — domain at the centre, use cases around it, interface adapters outside that, infrastructure at the edge. Dependencies only point inward.

Domain (Entities): your core business objects and rules. An Order with its validation logic. A User with its invariants. No framework imports, no database imports, no HTTP imports. Pure business logic that could run in any context.

Use Cases (Application layer): the things your system can do. PlaceOrder, CancelSubscription, GenerateInvoice. Each use case orchestrates domain objects and calls out to interfaces (repositories, notification services, payment gateways) — but only through abstractions it defines, not concrete implementations.

Interface Adapters: the translation layer. HTTP controllers that parse a request and call a use case. Repository implementations that translate between your domain model and your ORM. Event handlers that map an incoming message to a use case call. This layer knows about both the use case and the infrastructure; it's the glue.

Infrastructure: the actual implementations. Postgres via TypeORM. Redis. Stripe SDK. SendGrid. S3. These are the things that change when you switch providers — and in Clean Architecture, they're the only things that change.

Where the theory breaks down in practice

The repository abstraction tax. The theory says: define a UserRepository interface in the domain layer, implement it in infrastructure. In practice, for a CRUD-heavy app, you end up writing an interface, an implementation, and a test double for every entity — three files where one would do. The abstraction earns its keep when you genuinely need to swap implementations (e.g., in-memory for tests, Postgres for prod). It doesn't earn its keep when you have one implementation and will always have one implementation.

Where Clean Architecture ceremony is worth payingA trade-off diagram contrasting useful boundaries like payment gateways and repositories with avoidable ceremony like interfaces for every tiny class.WORTH THE BOUNDARYPayment gatewayRepository with test doubleHTTP request/response DTOUSUALLY CEREMONYInterface for every classDTO at every hopOne class per tiny action
Figure. Clean Architecture trade-offs — pay the abstraction cost for real provider boundaries and test doubles, but avoid interfaces and DTOs that only add ceremony.

Our rule: write the interface when you have two implementations or when the implementation is hard to test without it. Don't write it speculatively.

Use case explosion. A strict reading of Clean Architecture gives you one use case class per operation. For a product with 200 operations, that's 200 classes. The overhead of creating, naming, and navigating 200 files often outweighs the benefit of the separation. We group related use cases into services (OrderService with place, cancel, refund methods) and keep the dependency rule — the service still only calls interfaces, not concrete infrastructure.

The DTO proliferation. Clean Architecture purists pass DTOs (Data Transfer Objects) across every layer boundary — request DTO in, response DTO out, domain object in the middle. For complex domains with real transformation logic, this is correct. For simple CRUD, it's three classes where one would do. We use DTOs at the HTTP boundary (request/response shapes) and at the infrastructure boundary (ORM entities vs domain objects). We skip them at the use case boundary unless the transformation is non-trivial.

Framework coupling is often fine. The theory says your use cases shouldn't know about your framework. In practice, if you're building a Next.js app and you're never going to run your use cases outside of Next.js, the abstraction that hides Next.js from your use cases is pure overhead. We apply the dependency rule where it buys testability or replaceability. We don't apply it where it's just ceremony.

The pragmatic version we actually apply

The version that survives real projects:

Keep the dependency rule for the things that matter. Business logic doesn't import infrastructure. Domain objects don't import ORMs. Use cases don't import HTTP frameworks. This is non-negotiable and costs almost nothing to enforce.

Be selective about interfaces. Write them when you have multiple implementations or when the implementation is hard to test. Skip them when you have one implementation and it's easy to test with a real database in CI.

Group use cases into services. One service per domain concept (OrderService, BillingService, NotificationService), not one class per operation. Keep the services thin — they orchestrate, they don't contain business logic.

Use a real domain model. The biggest win from Clean Architecture isn't the layers — it's having a domain model that contains business logic instead of pushing it into controllers or database queries. An Order that knows how to validate itself, a Subscription that knows its own renewal rules. This is where the architecture pays for itself.

Test at the use case boundary. Unit tests for domain logic, integration tests that call use cases with real infrastructure (or a fast in-memory substitute). Skip the layer of mocks that test nothing but the wiring.

A concrete example of what this looks like

A billing module in a SaaS product:

billing/
  domain/
    subscription.ts        # Subscription entity with renewal/cancellation logic
    invoice.ts             # Invoice entity with line-item and tax logic
    billing-repository.ts  # Interface: findById, save, findDueForRenewal
  application/
    billing-service.ts     # renewSubscription(), cancelSubscription(), generateInvoice()
  infrastructure/
    postgres-billing-repository.ts  # TypeORM implementation of BillingRepository
    stripe-payment-gateway.ts       # Stripe SDK wrapped behind a PaymentGateway interface
  http/
    billing-controller.ts  # Express/Next.js handler → calls BillingService

The domain layer has zero imports from outside itself. The application layer imports from domain and from interfaces it defines. The infrastructure layer imports from application (to implement the interfaces) and from the actual SDKs. The HTTP layer imports from application and from the framework.

When Stripe changes their API, only stripe-payment-gateway.ts changes. When you add a new billing operation, you add a method to BillingService and a test. When you need to test renewSubscription without hitting Stripe, you swap in an in-memory PaymentGateway implementation.

That's the architecture working. Not because you followed the diagram, but because the dependency rule kept the business logic isolated from the things that change.

What we wish someone had told us

  • The diagram is a goal, not a checklist. Apply the dependency rule everywhere. Apply the full layer structure where it earns its keep.
  • The domain model is the most important part. If your domain objects are just data bags with no logic, you've missed the point of the architecture regardless of how many layers you have.
  • Testability is the best proxy for correctness. If a use case is hard to test, the dependencies are wrong. Fix the dependencies, not the test.
  • Don't let the architecture slow down the first version. Get the dependency rule right from day one. Add the full layer structure as the complexity justifies it.

TL;DR

Clean Architecture's core idea — business logic doesn't know about infrastructure — is worth applying on every project. The full ceremony of interfaces, DTOs, and strict layer separation is worth applying selectively, where it buys testability or replaceability.

The teams that get the most out of it treat it as a set of principles, not a diagram to reproduce. The teams that get burned by it treat it as a checklist.

If you're designing the architecture for a new product or refactoring an existing one and want a second opinion, let's talk.

Back to all insights