---
title: 3-Layered Architecture
description: Separate an application into presentation, business logic, and data access layers to achieve modularity and maintainability.
tokens: ~1173
---

# 3-Layered Architecture

**3-layered architecture** divides an application into three horizontal layers — presentation, business logic, and data access — each with a distinct responsibility. The goal is isolation: changes to how data is stored should not ripple into how HTTP requests are handled, and vice versa.

## How It Works

```
┌──────────────────────────────────────┐
│          Presentation                │  ← Controllers, routes, UI
├──────────────────────────────────────┤
│          Business Logic              │  ← Domain rules, validation, orchestration
├──────────────────────────────────────┤
│          Data Access                 │  ← Repositories, ORM, external APIs
└──────────────────────────────────────┘
         ▲ each layer calls only the one below
```

Each layer only knows about the layer directly below it, never above. The presentation layer calls business logic, business logic calls data access, but data access never reaches up into business logic, and business logic never imports anything from presentation. This one-way dependency chain keeps each layer replaceable without disturbing the others.

## Presentation Layer

The **presentation layer** handles all interaction with the outside world — HTTP requests, CLI commands, UI rendering. It receives input, delegates work to the business logic layer, and formats the result for the caller. No domain rules live here.

```typescript
import { OrderService } from "../services/OrderService";

async function createOrderHandler(req: Request): Promise<Response> {
  const { customerId, items } = await req.json();
  const order = await OrderService.create(customerId, items);
  return new Response(JSON.stringify(order), { status: 201 });
}
```

The handler knows how to read a request and write a response, but the decision of whether the order is valid belongs entirely to `OrderService`.

## Business Logic Layer

The **business logic layer** contains the domain rules, validation, and orchestration that make the application do what it actually does. It has no knowledge of HTTP, HTML, or SQL — it works with plain objects and calls the data access layer to persist results.

```typescript
import { OrderRepository } from "../repositories/OrderRepository";

type OrderItem = { productId: string; quantity: number; unitPrice: number };

export class OrderService {
  static async create(customerId: string, items: OrderItem[]) {
    if (items.length === 0) {
      throw new Error("Order must contain at least one item");
    }

    const total = items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0);
    const order = { customerId, items, total, status: "created" as const };

    return OrderRepository.save(order);
  }
}
```

In stricter setups, `OrderService` depends on a repository _interface_ rather than the concrete `OrderRepository` class. This inverts the dependency so the service can be tested with an in-memory stub instead of a real database.

## Data Access Layer

The **data access layer** owns all persistence and external service communication — SQL queries, ORM calls, third-party API requests. It exposes simple read/write operations that the business logic layer consumes without knowing the underlying storage mechanism.

```typescript
import { db } from "../db";

type Order = {
  customerId: string;
  items: { productId: string; quantity: number; unitPrice: number }[];
  total: number;
  status: "created";
};

export class OrderRepository {
  static async save(order: Order) {
    const [row] = await db.query(
      `INSERT INTO orders (customer_id, items, total, status)
       VALUES ($1, $2, $3, $4)
       RETURNING *`,
      [order.customerId, JSON.stringify(order.items), order.total, order.status],
    );
    return row;
  }

  static async findById(id: string) {
    const [row] = await db.query(`SELECT * FROM orders WHERE id = $1`, [id]);
    return row ?? null;
  }
}
```

## Trade-offs

| Benefit                                    | Cost                                                    |
| ------------------------------------------ | ------------------------------------------------------- |
| Clear separation makes code navigable      | Adds indirection for simple CRUD                        |
| Each layer testable in isolation            | Interface boilerplate between layers                    |
| Swap infrastructure without touching logic  | Risk of "pass-through" layers that add no value         |
| Teams can work on layers independently      | Cross-cutting concerns (auth, logging) don't fit neatly |

The 3-layered architecture is the foundation that more sophisticated patterns — hexagonal architecture, clean architecture, DDD's four-layer model — build on.