All insights
Engineering

Clean Architecture in Practice: A Guide for People Who Ship

Clean architecture beyond the concentric circles: the one rule that matters, what it looks like in a real project, the mistakes people make in both directions, and when it's honestly overkill. From a team that uses it in production.

Michele Cimmino · CEO & Academy Director, Lasting Dynamics · August 21, 2026 · 6 min read

Clean architecture is one of those ideas everyone cites and few apply with judgement. One camp ignores it and lives in dependency mud; the other applies it as dogma and turns a CRUD app into forty files of ceremony. At Lasting Dynamics we've used it on production systems for years and we teach it in our academy — this guide is how we explain it internally: the rule that matters, the boundaries that matter, and the honesty to say when not to use it.

The only rule that actually matters

Strip away the concentric-circle diagrams and clean architecture (Robert C. Martin, 2012 — with hexagonal/ports-and-adapters and onion architecture as close relatives) reduces to a single rule:

Dependencies point inward: business logic knows nothing about databases, frameworks, UI, or external services.

Everything else — entities, use cases, interface adapters — is an arrangement of that rule into layers. The core idea: the code that decides what your system does doesn't import your ORM, doesn't know HTTP exists. Technical details depend on business decisions, never the reverse.

Why is that the right rule? Because details churn and business rules endure. Across a decade of client projects we've swapped ORMs, databases, payment providers, and frontend frameworks — repeatedly. The clients' business rules outlived all of it. An architecture that couples the second to the first pays for every swap, with interest.

What it looks like in a real project

Forget the four circles for a moment; in a typical service the substance is:

src/
  domain/          ← entities and business rules. Zero external imports.
  application/     ← use cases orchestrating the domain. They DECLARE
                     interfaces for what they need (repository, mailer…)
  infrastructure/  ← concrete implementations: Postgres, Stripe, SMTP.
                     They implement application's interfaces.
  interface/       ← HTTP handlers, CLI, UI: translate the outside world
                     into use-case calls.

The mechanism holding it together is dependency inversion: a use case declares interface ApplicationRepository { save(app): ... } and doesn't care whether Postgres, a file, or a test mock sits behind it. The concrete implementation lives outside and is injected.

The payoffs are immediate and measurable:

  • Testability. Use cases test in milliseconds with no database or network. Fast suites are suites people actually write and run.
  • Swappability. Changing an ORM or provider touches infrastructure/, not the rules.
  • Readable intent. application/submit-application.ts tells you what the system does; the code becomes documentation of the business.

Where people go wrong (in both directions)

Mistake 1 — Layer dogma. Four layers, mappers between each, DTOs at every hop: for a form that inserts a row, that's 300 lines of ceremony around 10 lines of logic. Clean architecture protects business logic; if the business logic is "save it and show it", there's nothing to protect. A CRUD app is allowed to stay a CRUD app.

Mistake 2 — Fake boundaries. Layers that exist in folder names but not in dependencies: a "domain" that imports the ORM, a use case reading req.headers. That's the worst of both worlds — ceremony without benefits. The test is mechanical: what does your domain import? If the answer isn't "nothing external", your boundaries are decorative.

Mistake 3 — Pre-emptive abstraction everywhere. Interfaces for things that will never change, "just in case". Abstractions cost indirection; place them where change is plausible (storage, external services, I/O channels), not everywhere on principle.

Mistake 4 — Confusing clean architecture with microservices. They're orthogonal. A monolith with clean internal boundaries is excellent architecture — and usually the right starting point. Good internal boundaries are also precisely what makes extracting a service possible later, if it's ever needed.

When it pays off, and when it's overkill

Worth it when: there's real business logic (rules, states, calculations, policies), the system will live for years, more than one person works on it, or the technical details are unstable (more common than people admit).

Overkill when: it's a prototype, a throwaway internal tool, a thin CRUD over a database — or when the team doesn't yet have the judgement to draw the right boundaries, because wrong boundaries cost more than no boundaries.

The honest best practice: start simple, but keep the dependency rule as your compass. Even in a small project, keeping logic out of HTTP controllers costs nearly nothing and keeps every future option open.

In the AI-agent era it matters more, not less

One detail that's become central: agentic coding tools work dramatically better on codebases with clear boundaries. An agent changing a business rule in a system with an isolated domain touches one file and its tests; in spaghetti it touches twenty files and you hope. Architectural boundaries are how the AI knows where to put its hands too — as we put it in our Claude Code guide: you decide the structure, the AI fills it in.

How you actually learn this

Clean architecture isn't learned from diagrams — it's learned by drawing boundaries on real systems and having someone more experienced tell you exactly why yours is in the wrong place. That's the format of the Lasting Dynamics Academy: software architecture sits in the curriculum next to design patterns, RDBMS and testing, trained through real tasks with a weekly mentor review. Free, fully remote, selective, and everyone who completes it gets a job offer. If this guide sounded like sane engineering to you, applications are open.

FAQ

Are clean and hexagonal architecture the same thing? Close relatives: hexagonal (ports & adapters), onion and clean share the core idea — domain at the center, dependencies pointing inward, details at the edges. The differences are terminology and layer granularity. Pick one and be consistent; arguing about the names is wasted time.

Does it apply to frontend code? The principle does: separate state and business logic from UI components (which are as much a "detail" as the database). The full layer apparatus is usually too much; the dependency rule alone carries most of the value.

Where do I start on an existing codebase? With the next piece of logic you have to touch: extract it into a pure function, give it an interface for its dependencies, cover it with tests. Clean architecture on a legacy system is won one boundary at a time, not by big bang.