Nếu bạn đã dành thời gian đọc về kiến trúc phần mềm, chắc chắn bạn đã gặp ba cái tên cứ xuất hiện cùng nhau: Hexagonal Architecture, Onion Architecture, và Clean Architecture. Mỗi cái có sơ đồ riêng, từ vựng riêng, và những người hâm mộ nhiệt thành riêng. Đọc cả ba cùng lúc đôi khi cảm giác như xem ba người đang tranh luận về cách tốt nhất để nói cùng một câu.
Đây là phiên bản thành thật: chúng là cùng một ý tưởng, được khám phá độc lập vào những thời điểm khác nhau, được vẽ bằng những bức tranh hơi khác nhau. Mỗi cái thêm vào một sắc thái nhỏ mà những cái còn lại không nhấn mạnh nhiều bằng. Khi bạn nhìn thấy Ngôi Sao Phương Bắc chung, sự khác biệt trở nên rõ ràng — và bạn có thể chọn từ vựng nào mà team bạn đã quen nhất.
Ý tưởng chung mà cả ba cùng chia sẻ
Mọi kiến trúc trong số này đều dựa trên một quy tắc duy nhất, đôi khi gọi là Quy tắc Phụ thuộc: phụ thuộc trong mã nguồn luôn phải hướng vào trong, về phía quy tắc nghiệp vụ, và không bao giờ hướng ra ngoài về phía framework, database, hay I/O.
Hãy nghĩ nó giống như lực hấp dẫn. Domain của bạn — logic làm cho sản phẩm đáng sử dụng — nằm ở trung tâm. Mọi thứ xung quanh nó (database, web framework, payment SDK, email vendor, CLI tool) quay quanh ở phía ngoài. Các lớp ngoài được phép biết về các lớp trong. Các lớp trong tuyệt đối bị cấm biết các lớp ngoài tồn tại.
Tại sao điều này quan trọng? Vì những thứ thay đổi thường xuyên nhất nằm ở phía ngoài: bạn đổi payment provider, di trú sang database mới, viết lại frontend, thêm ứng dụng mobile. Nếu quy tắc nghiệp vụ của bạn bị ràng buộc với những chi tiết đó, mọi thay đổi sẽ lan rộng vào phần code bạn quan tâm nhất. Nếu chúng được cách ly, một thay đổi vendor là một hoán đổi cục bộ ở rìa — phần lõi không hề hay biết.
Alistair Cockburn, Jeffrey Palermo, và Robert C. Martin đều nhận ra pattern này từ các góc độ khác nhau. Cockburn vẽ một hình lục giác và gọi các điểm kết nối là "port". Palermo vẽ các vòng đồng tâm và gọi chúng là "lớp". Martin vẽ các vòng tròn đồng tâm và thêm tên chặt chẽ cho các lớp. Hướng của mũi tên trong mọi sơ đồ đều giống nhau: hướng vào trong.
Phụ thuộc trong mã nguồn hướng vào trong. Domain định nghĩa interface; thế giới bên ngoài hiện thực chúng. Lật ngược hướng đó ở bất kỳ đâu là bạn đã phá vỡ kiến trúc — dù bạn đang dùng tên nào.
Hexagonal Architecture (Ports & Adapters)
Alistair Cockburn giới thiệu Hexagonal Architecture vào năm 2005 với bí danh Ports & Adapters. Điểm cốt lõi là tính đối xứng: ứng dụng của bạn có hai loại bên ngoài — những thứ điều khiển nó (người dùng bấm một nút, một test runner, một scheduled job) và những thứ nó điều khiển (database, email provider, payment API). Cả hai phía giao tiếp qua port — interface mà domain sở hữu — và adapter — các hiện thực cụ thể kết nối công nghệ thật với các interface đó. Hình lục giác chỉ là gợi ý trực quan rằng có nhiều port như vậy, không chỉ là "trên" và "dưới". Nếu bạn muốn tìm hiểu đầy đủ về port, adapter, hai phía điều khiển/bị điều khiển, và ví dụ code thực tế, bài viết trước trong loạt này — Ports & Adapters — trình bày từ những nguyên tắc cơ bản nhất. Điểm mấu chốt ở đây là Hexagonal là kiến trúc đối xứng nhất trong ba cái: nó đối xử với các lời gọi đến và lời gọi đi với cùng mức độ nghiêm ngặt, và từ vựng của nó (port, adapter, điều khiển, bị điều khiển) là cụ thể và cơ học nhất.
Onion Architecture
Jeffrey Palermo mô tả Onion Architecture vào năm 2008. Ông giữ nguyên quy tắc phụ thuộc hướng vào trong nhưng chọn một bức tranh khác: các vòng đồng tâm, giống như các lớp của một củ hành. Vòng trong cùng là Domain Model — các entity và value object cốt lõi của bạn, các khái niệm nghiệp vụ thuần túy không có bất kỳ công nghệ nào. Bao quanh nó là vòng Domain Services, chứa logic điều phối nhiều domain object nhưng vẫn không biết gì về thế giới bên ngoài. Tiếp theo là vòng Application Services, nơi các use case tồn tại và quá trình điều phối diễn ra. Ở phía ngoài cùng là các chi tiết infrastructure và UI — database, web framework, external API.
Điều Onion nhấn mạnh mà hai cái còn lại không nói nhiều bằng là sự phân biệt giữa domain model và domain service. Palermo viết cho các team .NET thường nhầm lẫn "thứ đại diện cho một Order" với "service xử lý các Order" — một sự tách biệt thực sự hữu ích. Onion cũng làm cho việc phân lớp trở nên rất trực quan: bạn có thể nhìn vào danh sách import của bất kỳ class nào và ngay lập tức biết nó thuộc vòng nào dựa trên những gì nó được phép import. Nếu một domain service đang cố import một thư viện database, rõ ràng nó đang ở sai vòng.
Clean Architecture
Robert C. Martin (Uncle Bob) xuất bản Clean Architecture vào năm 2017, tổng hợp các ý tưởng ông đã tinh chỉnh qua nhiều năm. Ông vẽ cùng các vòng tròn đồng tâm nhưng đặt tên chặt chẽ và mục đích rõ ràng cho từng vòng. Từ trong ra ngoài: Entities (các quy tắc nghiệp vụ toàn doanh nghiệp — những thứ vẫn đúng ngay cả khi bạn không có máy tính), Use Cases (các quy tắc nghiệp vụ đặc thù cho ứng dụng — chính xác là phần mềm phải làm gì cho người dùng), Interface Adapters (controller, presenter, gateway — code dịch thuật giữa thế giới use case và thế giới bên ngoài), và Frameworks & Drivers (vòng ngoài cùng — database, web framework, UI library, device driver).
Bổ sung đặc trưng nhất mà Clean Architecture mang lại là lớp Use Cases được đặt tên rõ ràng. Trong nhiều codebase, "use case" là một khái niệm không chính thức — bạn có thể có một UserService làm mười việc khác nhau. Martin nhấn mạnh việc làm mỗi use case thành một artefact có tên rõ ràng, mang tính đầu tiên: một class hoặc function có tên nói thẳng ra nó làm gì (PlaceOrder, RegisterUser, GenerateInvoice). Ông gọi đây là "kiến trúc biết la hét" — khi một developer mới mở cấu trúc project, họ nên ngay lập tức thấy hệ thống làm gì, không phải framework nó dùng. Martin cũng hình thức hóa khái niệm Boundary — các seam interface rõ ràng ở mỗi điểm giao giữa các vòng — và ý tưởng rằng dữ liệu đi qua một boundary phải là các cấu trúc dữ liệu đơn giản có thể serialize được, không phải domain object giàu mang hành vi ẩn.
Cụm từ "screaming architecture" của Martin nắm bắt một khát vọng đơn giản: mở bất kỳ thư mục nào trong project và bạn phải ngay lập tức hiểu ý định nghiệp vụ, không phải tech stack. Một thư mục tên use-cases/ chứa PlaceOrder.ts và RegisterUser.ts la hét ý định của nó. Một thư mục tên controllers/ chứa mọi thứ khác thì thì thầm điều vô ích.
So sánh đối chiếu: ánh xạ từ vựng
Trở ngại thực tế lớn nhất khi đọc cả ba là chúng dùng những từ khác nhau cho các khái niệm chồng lấn. Đây là bảng dịch thuật:
| Khái niệm | Hexagonal (Cockburn) | Onion (Palermo) | Clean (Martin) |
|---|---|---|---|
| Lõi trong cùng | Domain (không có tên vòng cụ thể) | Vòng Domain Model | Vòng Entities |
| Use case nghiệp vụ | Logic ứng dụng bên trong hình lục giác | Vòng Application Services | Vòng Use Cases (rõ ràng, có tên) |
| Lớp dịch thuật | Adapter (điều khiển + bị điều khiển) | Vòng Infrastructure | Vòng Interface Adapters |
| Công nghệ ngoài cùng | Bên ngoài hình lục giác (HTTP, DB, v.v.) | Vòng UI / Infrastructure | Vòng Frameworks & Drivers |
| Điểm nối | Port (interface do domain sở hữu) | Interface ẩn tại ranh giới mỗi vòng | Boundary (interface rõ ràng ở mỗi điểm giao) |
| Điểm nhấn chính | Port đối xứng; khả năng kiểm thử; khả năng thay thế mọi adapter | Các vòng phân lớp; phân biệt domain model với domain service | Use case được đặt tên rõ ràng; cấu trúc biết la hét; boundary dữ liệu nghiêm ngặt |
| Phù hợp nhất khi… | Bạn có nhiều tích hợp và cần mỗi adapter độc lập có thể thay thế | Domain model giàu với điều phối service phức tạp | Team lớn cần từ vựng chung và cấu trúc thư mục "biết la hét" |
Những điểm thực sự khác nhau trong thực tế
Quy tắc chung che giấu những khác biệt thực sự về trọng tâm xuất hiện khi bạn ngồi xuống viết code.
Hexagonal là cơ học nhất. Nó cho bạn các slot được đặt tên chính xác: đây là adapter điều khiển, đây là port nó gọi, đây là use case xử lý nó, đây là output port, đây là adapter bị điều khiển. Nếu bạn tuân thủ các slot đó trung thực, cấu trúc gần như tự viết — và vì cả hai phía đối xứng, sự kỷ luật ngăn bạn cho database chui vào domain giống sự kỷ luật ngăn domain rò rỉ vào lớp HTTP. Đánh đổi là Hexagonal không nói gì về cách cấu trúc bên trong hình lục giác — nó im lặng về việc liệu lõi có nên có các lớp phụ như domain model vs domain service hay không.
Onion là phân lớp nhất. Trong khi Hexagonal vẽ một ranh giới (bên trong hình lục giác so với bên ngoài), Onion vẽ nhiều vòng bên trong lõi. Điều này có giá trị khi domain của bạn đủ lớn để cần cấu trúc nội bộ riêng — phân biệt các entity thuần túy với domain service điều phối chúng, và domain service với application service điều phối use case. Phép ẩn dụ về vòng rất trực quan, nhưng bạn cần kỷ luật khi nghiêm túc áp dụng: rất dễ bị cám dỗ với tay qua các vòng khi đang vội, và Onion không có kiểm tra cơ học theo cách mà từ vựng port/adapter của Hexagonal có.
Clean là quy định cụ thể nhất. Martin đặt tên mọi lớp và mọi khái niệm, điều đó vừa là điểm mạnh vừa là nguồn tranh luận. Điểm mạnh là các team có từ vựng chung rõ ràng — khi hai kỹ sư thảo luận về "use case boundary" ai cũng biết đó là gì. Nguồn tranh luận là cấu trúc được quy định có thể cảm thấy nặng nề cho các ứng dụng nhỏ: tạo các interface InputBoundary, OutputBoundary, và UseCase rõ ràng cho mọi tính năng là nhiều lễ nghi khi bạn đang xây một prototype. Clean Architecture cũng nhấn mạnh các data transfer object (DTO) thuần dữ liệu đi qua mọi ranh giới vòng, điều này thêm code nhưng làm cho các seam rất rõ ràng. Lợi nhuận đến ở quy mô lớn, khi việc ngăn chặn "object giàu rò rỉ" qua các lớp trở thành vấn đề thực tế chứ không phải lý thuyết.
Tóm lại: Hexagonal cho bạn hướng dẫn cơ học rõ ràng nhất về cách nối dây mọi thứ. Onion cho bạn phân lớp nội bộ của domain rõ ràng nhất. Clean cho bạn từ vựng rõ ràng nhất và cấu trúc thư mục có chính kiến nhất. Hầu hết các codebase thực tế pha trộn cả ba mà không nhận ra.
Nên chọn cái nào
Vì cả ba chia sẻ cùng quy tắc lõi, việc lựa chọn giữa chúng thực ra là câu hỏi về từ vựng, quy mô team, và sắc thái nào phù hợp với tình huống của bạn.
| Team & giai đoạn | Cách tiếp cận gợi ý | Tại sao |
|---|---|---|
| Solo / startup giai đoạn đầu | Hexagonal, nhẹ nhàng. Một hoặc hai port quanh database và vendor dễ biến động nhất. | Tốc độ quan trọng hơn sự hoàn chỉnh. Vài interface cho bạn test nhanh và lối thoát khi cần đổi; lễ nghi đầy đủ có thể chờ. |
| Team nhỏ, đang lớn (≈ Series A) | Onion hoặc Hexagonal với các interface port rõ ràng trên mọi I/O. | Test đang trở thành bottleneck, và người mới cần hiểu domain nhanh. Nhãn vòng (hoặc tên port) làm cấu trúc dễ đọc ngay. |
| Tầm trung (nhiều squad) | Từ vựng Clean Architecture như hợp đồng chung giữa các team. | Các squad dẫm chân nhau khi ranh giới mờ nhạt. Đặt tên mọi use case và mọi boundary cho team một seam sạch để làm việc độc lập. |
| Enterprise lớn | Cả ba cùng nhau, với boundary rõ ràng, hợp đồng có phiên bản, và nhiều adapter trên mỗi port. | Yêu cầu quy định, hệ thống cũ, và mua sắm đa vendor đòi hỏi sự rõ ràng mà chỉ có boundary đầy đủ Clean + Hexagonal mới cung cấp. |
Một vài pattern thực tế khớp với bảng này:
Một startup fintech (8 kỹ sư) dùng hai port — PaymentGateway và LedgerRepository — và để mọi thứ còn lại gọi trực tiếp. Khi họ đổi payment provider mười tám tháng sau, chỉ cần một class adapter mới và thay đổi một dòng đấu nối. Phần còn lại của codebase không bị đụng. Đó là Hexagonal ở mức tinh gọn và hiệu quả nhất.
Một công ty SaaS tầm trung (60 kỹ sư) áp dụng nhãn vòng Onion như quy tắc code review: "không có gì trong vòng domain model import từ bên ngoài vòng, không ngoại lệ". Kỹ sư mới hiểu chính sách ranh giới trong lần review PR đầu tiên. Từ vựng về vòng trở thành phím tắt — "bạn đang với tay qua các vòng ở đây" là một nhận xét review hoàn chỉnh mà ai cũng hiểu.
Một ngân hàng enterprise (vài trăm kỹ sư, codebase 20 năm tuổi) dùng từ vựng Clean Architecture đầy đủ, với các interface UseCase được đặt tên, object DTO rõ ràng ở mọi điểm giao vòng, và hai adapter trên mỗi output port (mainframe cũ + hệ thống core-banking mới chạy song song). Lễ nghi nặng nề, nhưng kiểm toán viên và team tuân thủ có thể đọc kiến trúc như một đặc tả, và các squad có thể deploy độc lập vì các ranh giới là seam thực trong code.
Kết quả tệ duy nhất thực sự là dùng cả ba tên thay nhau trên cùng một team mà không có bảng chú giải chung. Nếu một nửa team gọi nó là "port" và nửa kia gọi là "boundary", code review trở thành bài tập dịch thuật. Chọn một từ vựng, viết nó xuống, và tuân thủ. Quy tắc cốt lõi — phụ thuộc hướng vào trong — mới là thứ quan trọng.
Những điều cốt lõi cần nhớ
- Một quy tắc, ba sơ đồ. Hexagonal, Onion, và Clean Architecture đều tuân thủ cùng Quy tắc Phụ thuộc: phụ thuộc trong mã nguồn hướng vào trong, về phía quy tắc nghiệp vụ, không bao giờ hướng ra ngoài về framework hay I/O.
- Hexagonal (Cockburn, 2005) cho bạn hướng dẫn cơ học nhất: port và adapter, hai phía điều khiển và bị điều khiển đối xứng. Tốt nhất khi bạn có nhiều tích hợp cần hoán đổi.
- Onion (Palermo, 2008) thêm các vòng đồng tâm rõ ràng bên trong domain, tách domain model khỏi domain service và application service. Tốt nhất khi domain đủ lớn để cần cấu trúc nội bộ.
- Clean (Martin, 2017) đặt tên mọi lớp và làm use case thành artefact đầu tiên. Thêm "screaming architecture" — cấu trúc thư mục tuyên bố ý định nghiệp vụ. Tốt nhất cho team lớn cần từ vựng chung và DTO nghiêm ngặt ở mọi ranh giới.
- Chúng bổ sung nhau, không cạnh tranh. Hầu hết codebase trưởng thành pha trộn cả ba: đấu nối port/adapter kiểu Hexagonal, kỷ luật vòng kiểu Onion bên trong domain, đặt tên use case kiểu Clean ở lớp ứng dụng.
- Liều lượng phù hợp với team. Cấu hình hai port kiểu Hexagonal là đủ cho startup. Lễ nghi đầy đủ kiểu Clean được đền đáp ở quy mô enterprise. Đừng trả tiền cho từ vựng bạn chưa cần.
- Chọn một từ vựng cho mỗi team và viết nó xuống. Tên quan trọng ít hơn sự nhất quán.
Bài tiếp theo trong loạt này đi sâu hơn một cấp vào cơ chế giúp cả ba trở nên khả thi: Dependency Injection & Inversion of Control — phần đấu nối thực tế kết nối adapter với port lúc runtime mà domain không hề phải chạm tới framework.