Hướng dẫn thiết kế kiến trúc phần mềm Clean Architecture từ đầu
Clean Architecture là cách tổ chức code sao cho phần nghiệp vụ cốt lõi không phụ thuộc vào framework, database, giao diện hay dịch vụ bên ngoài. Quy tắc trung tâm chỉ có một câu: phụ thuộc trong mã nguồn chỉ được hướng vào trong, từ chi tiết kỹ thuật về phía nghiệp vụ. Nhờ vậy bạn có thể đổi database, đổi framework web hay viết test cho nghiệp vụ mà không phải dựng cả hệ thống. Bài này hướng dẫn thiết kế từ đầu cho một dự án mới, kèm ví dụ cụ thể và những chỗ nên linh hoạt.
Nguồn gốc và ý tưởng chính
Robert C. Martin (thường gọi Uncle Bob) giới thiệu Clean Architecture trên blog năm 2012 và sau đó trong cuốn sách cùng tên. Nó tổng hợp ý tưởng từ các kiến trúc trước như Hexagonal Architecture (Ports and Adapters) của Alistair Cockburn và Onion Architecture của Jeffrey Palermo. Điểm chung: đặt mô hình nghiệp vụ ở giữa, mọi thứ kỹ thuật ở vòng ngoài và giao tiếp qua interface.
Sơ đồ quen thuộc gồm bốn vòng tròn đồng tâm:
- Entities: quy tắc nghiệp vụ cốt lõi của doanh nghiệp, ví dụ một đơn hàng không được có tổng tiền âm, một tài khoản không được rút quá số dư.
- Use Cases: quy tắc nghiệp vụ của ứng dụng, điều phối entity để hoàn thành một tác vụ như "đặt hàng", "hủy đơn", "đăng ký tài khoản".
- Interface Adapters: chuyển đổi dữ liệu giữa use case và thế giới bên ngoài: controller, presenter, repository cài đặt cụ thể.
- Frameworks và Drivers: web framework, ORM, database, message broker, SDK bên thứ ba.
Quy tắc phụ thuộc trong thực tế
Vòng trong không được biết tên bất kỳ thứ gì ở vòng ngoài. Use case đặt hàng không import thư viện ORM, không biết đang chạy trên HTTP hay CLI. Khi use case cần lưu dữ liệu, nó định nghĩa một interface, ví dụ OrderRepository với phương thức save(order), và lớp hạ tầng ở vòng ngoài cài đặt interface đó bằng PostgreSQL. Đây là nguyên lý đảo ngược phụ thuộc (Dependency Inversion) trong SOLID.
Dữ liệu đi qua ranh giới cũng nên ở dạng đơn giản: use case nhận một đối tượng input (command) và trả về output, không nhận thẳng request object của framework hay trả về model ORM.
Cấu trúc thư mục mẫu
Một cách sắp xếp phổ biến cho service backend, tổ chức theo tính năng (module) rồi mới chia lớp bên trong:
- ordering/domain: entity Order, OrderLine, value object Money, quy tắc nghiệp vụ, domain event.
- ordering/application: use case PlaceOrder, CancelOrder; interface OrderRepository, PaymentGateway, Clock.
- ordering/infrastructure: PostgresOrderRepository, cài đặt PaymentGateway gọi cổng thanh toán, ánh xạ ORM.
- ordering/presentation: HTTP controller, DTO request/response, validator đầu vào.
- main hoặc bootstrap: nơi duy nhất nối mọi thứ lại (dependency injection), đọc cấu hình.
Tổ chức theo tính năng trước giúp khi mở thư mục, người đọc thấy ngay hệ thống làm gì (đặt hàng, kho, thanh toán) thay vì chỉ thấy "controllers, services, models". Uncle Bob gọi đây là kiến trúc "biết kêu lên" mục đích của nó (screaming architecture).
Ví dụ: use case đặt hàng
Luồng xử lý khi khách bấm "Đặt hàng":
- Controller nhận HTTP request, kiểm tra định dạng (thiếu trường, sai kiểu), chuyển thành PlaceOrderCommand gồm mã khách, danh sách sản phẩm và số lượng.
- Use case PlaceOrder lấy thông tin sản phẩm qua interface ProductCatalog, tạo entity Order. Entity tự kiểm tra quy tắc: số lượng dương, không vượt giới hạn mỗi đơn.
- Use case gọi PaymentGateway để tạo yêu cầu thanh toán, rồi OrderRepository để lưu.
- Use case trả về PlaceOrderResult (mã đơn, số tiền, đường dẫn thanh toán). Presenter hoặc controller chuyển nó thành JSON.
Khi viết unit test cho PlaceOrder, bạn truyền vào bản giả lập của ProductCatalog, PaymentGateway và OrderRepository chạy trong bộ nhớ. Test chạy trong mili giây, không cần database hay mạng. Bài hướng dẫn viết unit test và integration test giải thích khi nào nên dùng fake thay cho mock trong tình huống này.
So sánh với kiến trúc ba lớp truyền thống
| Khía cạnh | Ba lớp (Controller – Service – Repository) | Clean Architecture |
|---|---|---|
| Hướng phụ thuộc | Service phụ thuộc trực tiếp vào lớp truy cập dữ liệu | Hạ tầng phụ thuộc vào interface do nghiệp vụ định nghĩa |
| Model | Thường dùng chung model ORM xuyên suốt | Entity nghiệp vụ tách khỏi model lưu trữ |
| Test nghiệp vụ | Cần mock ORM hoặc database | Test thuần, không cần hạ tầng |
| Lượng code | Ít hơn | Nhiều hơn (interface, mapper) |
| Phù hợp | Ứng dụng CRUD đơn giản | Nghiệp vụ phức tạp, sống lâu, nhiều kênh đầu vào |
Áp dụng vừa đủ, tránh quá tay
Clean Architecture bị chỉ trích là sinh nhiều lớp và file. Phản biện này đúng khi áp dụng máy móc cho ứng dụng chủ yếu là đọc ghi dữ liệu. Vài gợi ý để giữ cân bằng:
- Chỉ tách mạnh ở module có nghiệp vụ phức tạp. Module quản lý danh mục đơn giản có thể theo kiểu CRUD; module tính giá, khuyến mãi, thanh toán mới cần lõi nghiệp vụ tách biệt.
- Không tạo interface cho mọi thứ. Chỉ tạo interface ở ranh giới thật: lưu trữ, dịch vụ bên ngoài, thời gian, sinh mã ngẫu nhiên.
- Cho phép đọc dữ liệu đi đường tắt. Màn hình danh sách, báo cáo có thể truy vấn thẳng database và trả DTO (ý tưởng gần với CQRS), không cần đi qua entity.
- Đừng viết mapper thủ công vô tận. Nếu entity và model lưu trữ gần như giống nhau, chấp nhận dùng chung với kỷ luật không để annotation framework rò rỉ logic vào.
Kết hợp với microservices và thiết kế dữ liệu
Clean Architecture nói về tổ chức bên trong một ứng dụng, còn microservices nói về chia hệ thống thành các dịch vụ triển khai độc lập. Hai khái niệm bổ sung cho nhau: một modular monolith được tổ chức sạch, ranh giới module rõ ràng sẽ dễ tách thành service sau này hơn nhiều so với một monolith rối. Khi tách, các module giao tiếp qua sự kiện; bài so sánh RabbitMQ, Kafka và Redis Pub/Sub giúp chọn kênh truyền sự kiện.
Về dữ liệu, mỗi module nên sở hữu bảng của mình, module khác không truy vấn trực tiếp bảng đó. Thiết kế bảng vẫn cần tuân theo nguyên tắc chuẩn hóa và ràng buộc như trong bài thiết kế database schema; tách entity khỏi model lưu trữ không có nghĩa là được phép bỏ qua chất lượng schema.
Lộ trình áp dụng cho đội mới
- Thống nhất quy tắc phụ thuộc và cấu trúc thư mục, ghi vào tài liệu kiến trúc ngắn.
- Thêm kiểm tra tự động: công cụ như ArchUnit (Java), dependency-cruiser (JavaScript/TypeScript), import-linter (Python) chặn import sai hướng ngay trong CI.
- Bắt đầu bằng một module có nghiệp vụ rõ, viết use case và test trước, cài đặt hạ tầng sau.
- Sau vài sprint, họp xem lại: lớp nào đang thừa, chỗ nào ranh giới chưa hợp lý, rồi điều chỉnh.
Kiến trúc tốt không phải là kiến trúc có nhiều lớp nhất, mà là kiến trúc giúp đội thay đổi hệ thống nhanh và an toàn khi nghiệp vụ thay đổi.
Câu hỏi thường gặp
Clean Architecture có phù hợp với dự án nhỏ không?
Có thể áp dụng tinh thần của nó (tách nghiệp vụ khỏi framework) mà không cần đủ bốn lớp. Với ứng dụng CRUD nhỏ, chia module rõ ràng và giữ logic nghiệp vụ ngoài controller thường đã đủ.
Clean Architecture khác Hexagonal Architecture thế nào?
Về bản chất rất gần nhau: đều đặt nghiệp vụ ở trung tâm và giao tiếp với bên ngoài qua interface (port) và cài đặt (adapter). Clean Architecture chia vòng trong chi tiết hơn thành entity và use case.
Có cần dùng Domain-Driven Design cùng Clean Architecture không?
Không bắt buộc, nhưng hai cách tiếp cận bổ trợ tốt. DDD giúp mô hình hóa nghiệp vụ và xác định ranh giới module, còn Clean Architecture giúp tổ chức code bên trong mỗi module.
Framework như Spring hay NestJS có mâu thuẫn với Clean Architecture không?
Không mâu thuẫn nếu framework chỉ nằm ở vòng ngoài. Dùng framework để nhận request, tiêm phụ thuộc và kết nối database, còn entity và use case giữ ở dạng code thuần không phụ thuộc annotation hay lớp của framework.
Bài liên quan
Hướng dẫn viết Unit Test và Integration Test đảm bảo chất lượng code
Cách viết unit test và integration test có giá trị: chọn phạm vi, đặt tên, dùng mock đúng chỗ, dựng database thử, đưa test vào CI và tránh test giòn.

So sánh Agile, Scrum và Kanban: chọn cách làm nào cho đội phần mềm
Agile là triết lý, Scrum và Kanban là hai cách thực hành. Bài so sánh vai trò, nhịp làm việc, chỉ số đo lường và gợi ý chọn mô hình theo loại dự án.

So sánh message queue: RabbitMQ, Kafka và Redis Pub/Sub
RabbitMQ, Kafka hay Redis Pub/Sub? So sánh mô hình lưu trữ, đảm bảo giao nhận, thứ tự tin nhắn, vận hành và tình huống nên dùng từng giải pháp.
So sánh nền tảng CMS WordPress, Strapi, Directus cho doanh nghiệp
WordPress, Strapi hay Directus? So sánh kiến trúc, đối tượng biên tập, khả năng mở rộng, bảo mật, chi phí vận hành để doanh nghiệp chọn CMS phù hợp.