← All posts

Building Scalable APIs with Clean Architecture

How separating concerns across well-defined layers keeps your API maintainable as requirements evolve — and the concrete tradeoffs you'll make along the way.

Every API starts simple. A handful of endpoints, a database, maybe a service class or two. Then requirements arrive. A new client needs a slightly different response shape. Another team needs to call the same logic from a background job. A compliance requirement demands audit logs on certain mutations.

Without structure, each of these additions becomes a negotiation with the existing mess. With Clean Architecture — or any layered approach that enforces a clear dependency direction — they become routine.

The core idea

Clean Architecture organizes code into concentric layers. The innermost layer contains your domain: entities, value objects, and the rules that govern them. No framework dependencies, no database references, no HTTP types. Just logic.

The next layer out contains your application use cases. These orchestrate the domain to fulfill specific business operations. They depend on the domain inward and on abstractions outward — interfaces for repositories, notification services, event buses.

The outermost layer holds your infrastructure: the concrete implementations of those interfaces. The actual Entity Framework DbContext. The actual SMTP client. The actual HTTP controller that maps a request into a use case command and a result into a response.

The dependency rule is absolute: inner layers never reference outer layers. A domain entity has no idea what ASP.NET is. A use case has no idea which database you’re using.

What this buys you

Testability. Your use cases can be unit-tested by injecting in-memory implementations of their dependencies. No database, no HTTP stack, no network. Tests run in milliseconds.

Swappability. When you switch from SQL Server to PostgreSQL, you rewrite the repository implementations. The use cases and domain are untouched. When you add a gRPC interface alongside your REST API, the same use cases serve both.

Clarity. A new developer reading the codebase can look at a use case class and understand what the operation does without navigating through framework plumbing.

The tradeoffs

This structure is not free. It introduces indirection. Where a procedural approach might put database access directly inside a controller action, Clean Architecture routes the call through an interface, an implementation, and a registration in your IoC container.

For a simple CRUD API with no real domain logic, this overhead buys very little. Use the simplest thing that works. Where Clean Architecture pays off is in systems with non-trivial business rules, multiple consumers, or a lifespan long enough that the requirements will change.

In practice

When we set up a new API project, we create three projects in the solution:

  • Domain — entities, value objects, domain events, repository interfaces
  • Application — use cases (commands, queries, handlers), application service interfaces
  • Infrastructure — repository implementations, external service adapters, migrations

A fourth project, Api, contains controllers and program startup. It references all three.

We enforce the dependency rule with a build-time architecture test using NetArchTest. If anyone accidentally imports Infrastructure from Domain, CI fails. The structure isn’t just a convention — it’s a constraint the tooling enforces.


This post is part of an ongoing series on the engineering practices we use at Constelutions when building backend systems for our clients.