# Clean Architecture in Practice: How to Structure Large Backend Systems

> Clean Architecture keeps business logic at the center of a backend, so frameworks and databases stay replaceable. Here is how to apply it in practice.

- Author: Tai Pham Phu
- Published: 14 Mar 2026
- Updated: 9 Oct 2026
- Tags: Backend, Architecture
- URL: https://phamphutai.com/2026/03/14/clean-architecture-in-practice-how-to-structure-large-backend-systems/

As backend systems grow, their codebases often become harder to maintain. Business logic gets mixed with framework code, database queries spread across services, and simple changes start breaking unrelated parts of the system.

To solve this, many teams adopt **Clean Architecture**, a design approach popularized by Robert C. Martin. The goal is to build systems that are **maintainable, testable, and independent of frameworks and databases**.

In this article, we'll look at what Clean Architecture is and how to apply it when structuring a large backend system.

## What is Clean Architecture?

Clean Architecture organizes code into **layers**. The most important business rules sit at the center, and external concerns (frameworks, databases, UI) stay on the outside.

The key idea:

> **Dependencies always point inward, toward the business logic.**

This means your core logic should **not depend on frameworks, databases, or external services**.

## The Core Layers of Clean Architecture

A typical Clean Architecture backend has four main layers:

1.  Entities
2.  Use Cases
3.  Interface Adapters
4.  Frameworks & Drivers

Let's look at each one.

## 1\. Entities (Enterprise Business Rules)

Entities represent the **core business models** and rules of the system. They should be completely independent of frameworks and infrastructure.

Example:

```ts
class Order {
  constructor(public items: Item[], public total: number) {}

  calculateTotal() {
    return this.items.reduce((sum, item) => sum + item.price, 0)
  }
}
```

Key characteristics:

-   Pure business logic
-   No database logic
-   No framework dependencies
-   Highly reusable

Entities rarely change, because they represent **fundamental business rules**.

## 2\. Use Cases (Application Business Rules)

Use cases define **what the system does**. They orchestrate entities and enforce application-specific rules.

Example:

```ts
class CreateOrder {
  constructor(private orderRepository: OrderRepository) {}

  execute(orderData) {
    const order = new Order(orderData.items, 0)
    order.total = order.calculateTotal()
    this.orderRepository.save(order)
    return order
  }
}
```

Responsibilities:

-   Coordinate domain entities
-   Implement application workflows
-   Define input/output boundaries
-   Stay free of framework dependencies

Use cases are often called **interactors** or **application services**.

## 3\. Interface Adapters

This layer converts data between **external systems and the internal application**.

Typical components:

-   Controllers
-   Presenters
-   Repositories
-   DTO mappers

Example controller:

```ts
class OrderController {
  constructor(private createOrder: CreateOrder) {}

  handle(request) {
    return this.createOrder.execute(request.body)
  }
}
```

This layer acts as a **translator** between:

-   HTTP requests
-   databases
-   external APIs

and your internal business logic.

## 4\. Frameworks & Drivers

This is the **outermost layer**. It contains:

-   web frameworks
-   databases
-   message queues
-   external APIs

Examples of frameworks that live here:

-   Express.js
-   NestJS
-   Spring Framework

Example repository implementation:

```ts
class PostgresOrderRepository implements OrderRepository {
  save(order: Order) {
    // database logic
  }
}
```

These components **plug into the inner layers**, but the core application does not depend on them.

## The Dependency Rule

The most important rule in Clean Architecture is:

> **Source code dependencies must point inward.**

Outer layers can depend on inner layers, but inner layers should never depend on outer layers.

For example:

```
Frameworks → Controllers → Use Cases → Entities
```

Never the other way around.

This keeps your business logic **independent and portable**. It is the same idea as the Dependency Inversion Principle, one of the five ideas I cover in [Writing Better Code with SOLID Principles](https://phamphutai.com/2026/03/14/writing-better-code-with-solid-principles/), applied at the level of a whole system instead of a single class.

## Example Project Structure

A large backend project might look like this:

```
src
 ├── domain
 │   ├── entities
 │   └── repositories
 │
 ├── application
 │   └── usecases
 │
 ├── infrastructure
 │   ├── database
 │   └── external-services
 │
 └── interfaces
     ├── controllers
     └── routes
```

Benefits of this structure:

-   Clear separation of concerns
-   Easier testing
-   Room to grow
-   Framework independence

## Why Clean Architecture Works for Large Systems

Large backend systems often suffer from problems like:

-   tightly coupled modules
-   code that is hard to test
-   fragile dependencies
-   infrastructure that is hard to replace

Clean Architecture addresses these by making sure that:

-   business logic stays isolated
-   frameworks become replaceable
-   testing becomes easier

For example, you could switch from Express.js to Fastify without rewriting your core business logic.

## Testing Becomes Much Easier

Because use cases depend on **interfaces instead of implementations**, you can test them with mocks.

Example:

```ts
const mockRepo = {
  save: jest.fn()
}

const usecase = new CreateOrder(mockRepo)
```

This lets you test business logic **without running a database or a server**.

## Common Mistakes When Implementing Clean Architecture

#### Overengineering small projects

Clean Architecture adds extra layers. For a small application, that can be unnecessary complexity.

#### Mixing responsibilities

Controllers should not contain business logic.

Bad:

```
Controller → database queries
```

Good:

```
Controller → Use Case → Repository
```

#### Ignoring domain modeling

Clean Architecture works best when your **domain entities are well designed**. Layers can't rescue a model that doesn't reflect the business.

## When Should You Use Clean Architecture?

Clean Architecture works best for:

-   large backend systems
-   enterprise applications
-   long-lived codebases
-   systems with many integrations

For quick prototypes or small scripts, a simpler structure is usually more practical.

## When This Matters for a Startup

A small team rarely needs all four layers on day one. Early on, the bigger risk is building something nobody wants, and every extra interface is code you have to move around when you change direction.

What does pay off early is the core of the idea: keep business rules out of controllers and away from vendor SDKs. That costs very little, and it protects the part of the product customers actually rely on. At [Santara](https://phamphutai.com/projects/santara/), for example, booking data arrives from Airbnb, Booking.com and WhatsApp, and each of those integrations will change on its own schedule. Keeping that messy outside world at the edges, with the briefing logic in the middle, is exactly the kind of boundary worth drawing.

A reasonable rule: draw a boundary when you have a second integration, a real testing pain, or a part of the system that must be correct every time. Correctness tends to matter more than extra features, which is a theme I come back to in [Boring Businesses Make the Best Businesses](https://phamphutai.com/2026/10/07/boring-businesses-make-the-best-businesses/). Until then, keep it simple and refactor when the pain is real.

* * *

## Final Thoughts

Clean Architecture is not just about folder structure. It's about **protecting your business logic from external complexity**.

When applied well, it leads to systems that are:

-   easier to maintain
-   easier to test
-   easier to scale
-   easier to evolve

By keeping business rules at the center and isolating infrastructure concerns, teams can build backend systems that stay manageable even as they grow.
