Software Architecture

Clean Architecture từ A đến Z

Admin · Cập nhật 19/07/2026

Tìm hiểu Clean Architecture từ cơ bản đến nâng cao: 4 tầng kiến trúc, Dependency Rule, cấu trúc thư mục và ví dụ thực tế với Next.js + Prisma, kèm ưu nhược điểm và checklist áp dụng.

Chuyên mục: Software Architecture
Mức độ: Cơ bản → Nâng cao

Mục lục

  • Giới thiệu
  • Clean Architecture là gì?
  • Vì sao cần?
  • 4 tầng kiến trúc
  • Dependency Rule
  • Cấu trúc thư mục
  • Ví dụ với Next.js + Prisma
  • So sánh trước và sau khi áp dụng
  • Ưu điểm
  • Nhược điểm
  • Khi nào nên dùng?
  • Best Practices
  • Sai lầm thường gặp
  • Checklist
  • FAQ
  • Kết luận

Giới thiệu

Khi một dự án phát triển từ vài nghìn lên hàng trăm nghìn dòng mã, việc quản lý sự phụ thuộc giữa các thành phần trở nên khó khăn. Clean Architecture giúp tách biệt nghiệp vụ với framework, cơ sở dữ liệu và giao diện người dùng để hệ thống dễ bảo trì, kiểm thử và mở rộng.

Clean Architecture là gì?

Clean Architecture là mô hình kiến trúc do Robert C. Martin (Uncle Bob) đề xuất. Mục tiêu là đảm bảo business logic không phụ thuộc vào framework, database hay UI.

Vì sao cần?

  • Dễ bảo trì.
  • Dễ kiểm thử.
  • Có thể thay đổi cơ sở dữ liệu hoặc framework với ít ảnh hưởng.
  • Giảm coupling, tăng khả năng mở rộng.

Bốn tầng chính

Hãy hình dung bốn tầng như bốn vòng tròn đồng tâm. Vòng trong cùng là quy tắc nghiệp vụ ổn định nhất, ít thay đổi nhất; càng ra ngoài càng "dễ thay" (framework, database, giao diện). Mục tiêu là bảo vệ vòng trong khỏi sự biến động của vòng ngoài.

1. Domain

Chứa Entity, Value Object và các quy tắc nghiệp vụ cốt lõi. Đây là tầng không được import bất kỳ framework nào (không Prisma, không Express, không Next.js). Một Entity chuẩn tự bảo vệ tính hợp lệ của nó ngay trong constructor:

// domain/entities/user.ts
export class User {
  private constructor(
    public readonly id: string,
    public readonly email: string,
    private _isActive: boolean
  ) {}

  static create(id: string, email: string): User {
    if (!email.includes("@")) {
      throw new Error("Email không hợp lệ");
    }
    return new User(id, email, true);
  }

  deactivate(): void {
    if (!this._isActive) throw new Error("Tài khoản đã bị khóa");
    this._isActive = false;
  }

  get isActive() {
    return this._isActive;
  }
}

Lưu ý: Entity không biết nó được lưu ở đâu. Nó chỉ mô tả nghiệp vụ ("email phải hợp lệ", "không thể khóa hai lần").

2. Application

Chứa Use Case, Service và Interface (port). Use Case điều phối luồng nghiệp vụ nhưng phụ thuộc vào abstraction, không phụ thuộc chi tiết:

// application/use-cases/deactivate-user.ts
import type { UserRepository } from "../ports/user-repository";

export class DeactivateUserUseCase {
  constructor(private readonly users: UserRepository) {}

  async execute(userId: string): Promise<void> {
    const user = await this.users.findById(userId);
    if (!user) throw new Error("Không tìm thấy người dùng");
    user.deactivate();      // quy tắc nghiệp vụ nằm ở Domain
    await this.users.save(user);
  }
}

3. Infrastructure

Triển khai Repository, Database, Redis, Email, Queue... Đây là nơi duy nhất "biết" Prisma tồn tại. Nó implement interface do tầng Application định nghĩa:

// infrastructure/repositories/prisma-user-repository.ts
import { PrismaClient } from "@prisma/client";
import type { UserRepository } from "@/application/ports/user-repository";
import { User } from "@/domain/entities/user";

export class PrismaUserRepository implements UserRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findById(id: string): Promise<User | null> {
    const row = await this.prisma.user.findUnique({ where: { id } });
    return row ? User.create(row.id, row.email) : null;
  }

  async save(user: User): Promise<void> {
    await this.prisma.user.update({
      where: { id: user.id },
      data: { isActive: user.isActive },
    });
  }
}

4. Presentation

API, Web UI, CLI hoặc Mobile. Tầng này chỉ nhận request, gọi Use Case và trả response — không chứa nghiệp vụ.

Dependency Rule

Nguyên tắc quan trọng nhất:

Mọi dependency chỉ được hướng vào trong (inward).

Presentation → Infrastructure → Application → Domain

Ví dụ cấu trúc thư mục

src/
├── domain/
├── application/
├── infrastructure/
└── presentation/

Ví dụ Repository Interface

export interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

Infrastructure sẽ triển khai interface này bằng Prisma hoặc ORM khác.

Ví dụ thực tế với Next.js + Prisma

Hãy ghép các mảnh trên vào một Route Handler thật của Next.js App Router. Điểm mấu chốt: Route Handler (Presentation) không hề nhìn thấy Prisma — nó chỉ khởi tạo dependency và gọi Use Case. Đây chính là nơi "lắp ráp" (composition root) diễn ra.

// app/api/users/[id]/deactivate/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { PrismaUserRepository } from "@/infrastructure/repositories/prisma-user-repository";
import { DeactivateUserUseCase } from "@/application/use-cases/deactivate-user";

export async function POST(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  // Composition root: nơi duy nhất biết cả Prisma lẫn Use Case
  const repository = new PrismaUserRepository(prisma);
  const useCase = new DeactivateUserUseCase(repository);

  try {
    await useCase.execute(id);
    return NextResponse.json({ message: "Đã khóa tài khoản" });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Lỗi server";
    return NextResponse.json({ message }, { status: 400 });
  }
}

Lợi ích thực tế: khi viết test cho DeactivateUserUseCase, bạn chỉ cần truyền một UserRepository giả (in-memory), không cần chạy database. Khi muốn đổi từ PostgreSQL sang MongoDB, bạn chỉ viết một MongoUserRepository mới mà không đụng đến Use Case hay Domain.

So sánh: trước và sau khi áp dụng Clean Architecture

Tiêu chí Không áp dụng (logic nằm trong Route) Có Clean Architecture
Vị trí nghiệp vụ Rải rác trong API route, component Tập trung ở Domain + Use Case
Viết Unit Test Phải mock cả Next.js request và Prisma Chỉ mock Repository interface
Đổi database Sửa khắp nơi có prisma.* Viết 1 Repository mới
Tốc độ code ban đầu Nhanh Chậm hơn (nhiều lớp)
Chi phí bảo trì dài hạn Tăng nhanh khi hệ thống lớn Ổn định, dễ mở rộng
Onboarding người mới Khó dò luồng Rõ ranh giới trách nhiệm

Ưu điểm

  • Dễ test
  • Dễ mở rộng
  • Thay thế công nghệ ít ảnh hưởng
  • Tăng tuổi thọ dự án

Nhược điểm

  • Ban đầu nhiều lớp hơn.
  • Không phù hợp với dự án rất nhỏ.
  • Đòi hỏi đội ngũ thống nhất quy ước.

Khi nào nên dùng?

Nên dùng khi:

  • Dự án dài hạn.
  • Có nhiều lập trình viên.
  • Có nhiều nghiệp vụ.

Không nên dùng khi:

  • MVP rất nhỏ.
  • Script ngắn.

Best Practices

  • Không để Domain phụ thuộc Prisma hoặc Express.
  • Repository chỉ là abstraction trong Application/Domain.
  • Viết Unit Test cho Use Case.
  • Mỗi Use Case chỉ thực hiện một mục tiêu.

Sai lầm thường gặp

  • Domain "rò rỉ" phụ thuộc framework. Import PrismaClient, NextRequest hay decorator của ORM vào Entity là vi phạm nghiêm trọng nhất — Domain lập tức mất khả năng test độc lập.
  • Anemic Domain Model (Entity rỗng ruột). Entity chỉ toàn getter/setter, còn toàn bộ nghiệp vụ dồn hết vào Service. Kết quả là bạn có Clean Architecture "hình thức" nhưng bản chất vẫn là code thủ tục.
  • Repository trả về Prisma model thay vì Domain Entity. Khi đó kiểu dữ liệu của Prisma lan ngược vào Use Case, và bạn không còn tách được tầng nữa.
  • Nhồi nghiệp vụ vào Route Handler. Việc validate, tính toán, gọi nhiều bảng ngay trong route.ts khiến Presentation phình to và không thể tái sử dụng cho CLI hay job nền.
  • Áp dụng đủ 4 tầng cho một CRUD tí hon. Over-engineering cũng tai hại như under-engineering: một form liên hệ đơn giản không cần Domain, Use Case và Repository riêng.
  • Dependency đi sai chiều. Ví dụ để Application import trực tiếp class Infrastructure (thay vì import interface) — điều này phá vỡ Dependency Rule dù code vẫn chạy.

Checklist

  • Domain không import framework
  • Business logic nằm trong Use Case
  • Repository là interface
  • Infrastructure triển khai interface
  • Presentation chỉ gọi Use Case

FAQ

Clean Architecture có bắt buộc dùng Repository Pattern không?

Không bắt buộc, nhưng thường được kết hợp để tách biệt truy cập dữ liệu.

Có phù hợp với Next.js?

Có. Có thể áp dụng cho API Route, Route Handler hoặc Server Actions. Bạn đặt Domain và Application trong thư mục độc lập (ví dụ src/core/), còn app/ chỉ đóng vai trò Presentation gọi vào Use Case.

Clean Architecture khác gì với mô hình MVC nhiều tầng?

MVC truyền thống thường để Controller phụ thuộc trực tiếp vào Model/ORM, nên chiều phụ thuộc hướng ra ngoài (vào database). Clean Architecture đảo chiều: mọi thứ hướng vào Domain thông qua interface, nhờ đó nghiệp vụ được cô lập khỏi hạ tầng.

Có nhất thiết phải tách đủ 4 tầng ngay từ đầu không?

Không. Bạn có thể bắt đầu với Domain + Application gộp chung, rồi tách dần khi độ phức tạp tăng. Điều quan trọng là giữ đúng chiều phụ thuộc, còn số lượng thư mục có thể điều chỉnh theo quy mô.

Dùng Dependency Injection container có bắt buộc không?

Không bắt buộc. Với dự án nhỏ và vừa, việc "new" thủ công tại composition root (như ví dụ Route Handler ở trên) là đủ và dễ đọc. Chỉ nên cân nhắc DI container (như tsyringe, InversifyJS) khi số lượng dependency lớn và lồng nhau nhiều tầng.

Làm sao test một Use Case mà không cần database?

Tạo một lớp implement Repository interface bằng mảng/Map trong bộ nhớ (in-memory repository), truyền vào Use Case rồi kiểm tra kết quả. Vì Use Case chỉ phụ thuộc interface, nó không phân biệt được đó là Prisma hay in-memory.

Kết luận

Clean Architecture không phải mục tiêu cuối cùng mà là công cụ giúp dự án lớn dễ phát triển lâu dài. Hãy áp dụng linh hoạt theo quy mô hệ thống thay vì máy móc.