Kiến trúc lục giác (Hexagonal Architecture, Ports and Adapters)
1. Tổng quan
Kiến trúc lục giác là một phong cách kiến trúc tách lõi nghiệp vụ của ứng dụng khỏi công nghệ bên ngoài, và kết nối với người dùng, cơ sở dữ liệu, message và hệ thống bên ngoài thông qua các port (cổng) biểu đạt mục đích và các adapter (bộ chuyển đổi) chuyển đổi công nghệ.
Phần mềm doanh nghiệp thường có cấu trúc bị quyết định bởi cách gọi của web framework, ORM, message broker, SDK đám mây hơn là bởi quy tắc nghiệp vụ. Ban đầu có thể nhanh chóng tạo màn hình và bảng, nhưng theo thời gian, quy tắc nghiệp vụ lọt vào controller, đối tượng miền bị trói buộc vào một ORM cụ thể và phải khởi chạy cơ sở dữ liệu thật chỉ để kiểm thử. Hiện tượng này có thể gọi là sự phụ thuộc công nghệ và sự ô nhiễm logic nghiệp vụ.
Kiến trúc lục giác đặt ứng dụng ở trung tâm và biểu diễn thế giới bên ngoài như các điểm kết nối theo nhiều hướng. Bản thân hình lục giác không có nghĩa là sáu tầng cố định, mà là một ẩn dụ trực quan được dùng thay cho hình chữ nhật để biểu diễn nhiều cuộc hội thoại (conversation) khác nhau. Điều quan trọng không phải là bên ngoài nằm ở hướng nào, mà là lõi chỉ hội thoại với bên ngoài thông qua hợp đồng trừu tượng gọi là port.
Port là hợp đồng của một cuộc hội thoại có ý nghĩa mà ứng dụng cung cấp hoặc cần đến.
Ví dụ, các mục đích nghiệp vụ như tạo đơn hàng, đặt giữ tồn kho, lưu kết quả phê duyệt thanh toán có thể được biểu diễn thành port.
Adapter chuyển đổi các đầu vào/ra kỹ thuật như HTTP, SQL, Kafka, tệp, test double thành lời gọi và dữ liệu của port.
Do đó, có thể đồng thời gắn REST adapter và CLI adapter vào cùng một port, và có thể hoán đổi PostgreSQL adapter với adapter bộ nhớ trên cùng port lưu trữ.
Mục tiêu của phong cách này không đơn thuần là chia thư mục thành domain, application, infrastructure.
Mục tiêu cốt lõi là hướng chiều phụ thuộc vào bên trong, để quy tắc nghiệp vụ có thể được thực thi và kiểm chứng mà không cần biết đến sự tồn tại của UI, cơ sở dữ liệu hay framework.
Khi tách lý do thay đổi của công nghệ bên ngoài khỏi lý do thay đổi của chính sách nghiệp vụ, có thể thu hẹp phạm vi lan truyền của thay đổi yêu cầu ra toàn bộ mã nguồn.
Việc áp dụng hay không cần được quyết định theo hướng thay đổi chứ không theo trào lưu. Hiệu quả sẽ lớn khi phương thức thanh toán, kho lưu trữ, nền tảng message, kênh thay đổi, hoặc khi cần tái sử dụng cùng một quy tắc nghiệp vụ từ nhiều điểm vào. Ngược lại, nếu tạo quá nhiều port và adapter cho hệ thống có vòng đời ngắn và hầu như không có quy tắc nghiệp vụ như màn hình CRUD đơn giản, chỉ có chi phí trừu tượng hóa tăng lên. Kỹ sư chuyên nghiệp (Professional Engineer) phải so sánh đồng thời hiệu quả cô lập thay đổi kỳ vọng với chi phí vận hành và đào tạo của cấu trúc bổ sung.
2. Sơ đồ khái niệm tổng thể và nguyên lý thiết kế
Kiến trúc lục giác là cách mô tả ranh giới giữa lõi và bên ngoài hơn là một mô hình phân tầng cố định. Bên trong là quy tắc miền và use case, bên ngoài là các yếu tố công nghệ gọi vào hoặc được gọi bởi chúng. Quy tắc bên trong không trực tiếp import bên ngoài là cốt lõi của cấu trúc.
flowchart LR
U[Người dùng·batch·hệ thống ngoài] --> IA[Driving Adapter\nREST·CLI·message consumer]
IA --> IP[Driving Port\nhợp đồng đầu vào use case]
IP --> AC[Application Core\nuse case·quy tắc miền]
AC --> OP[Driven Port\nhợp đồng lưu trữ·phát hành·dịch vụ ngoài]
OP --> OA[Driven Adapter\nDB·broker·SDK thanh toán]
OA --> E[Công nghệ·hạ tầng bên ngoài]
T[Kịch bản kiểm thử·test double] --> IP
AC --> T
Trong hình trên, driving adapter khởi động luồng từ ngoài vào trong. HTTP controller, GraphQL resolver, bộ lập lịch, CLI, message consumer chuyển yêu cầu thành lệnh mà input port hiểu được rồi gọi application core. Adapter này đảm nhận các chuyển đổi kỹ thuật như phân tích JSON, trích xuất ngữ cảnh xác thực, phản hồi lỗi theo từng giao thức, và không quyết định quy tắc giảm giá hay bất biến tồn kho.
Driven port trừu tượng hóa năng lực mà lõi yêu cầu từ bên ngoài.
Lõi không cần biết OrderRepository là PostgreSQL hay cơ sở dữ liệu tài liệu, cũng không cần biết PaymentGateway là SDK của một công ty thẻ cụ thể hay đối tượng giả lập.
Khi lõi sở hữu interface của port, hiện thực bên ngoài sẽ kết nối theo hợp đồng đó, nên nguyên lý đảo ngược phụ thuộc thể hiện ra thành chiều import thực tế trong mã.
Driven adapter chuyển lời gọi của lõi thành giao thức kỹ thuật. SQL adapter ánh xạ đối tượng miền với hàng (row), message adapter tuần tự hóa sự kiện miền thành message của broker, còn adapter API bên ngoài dịch timeout, retry, mã lỗi thành kết quả mà lõi nghiệp vụ sử dụng. Chỉ khi không trộn trách nhiệm chuyển đổi này vào lõi mới có thể tách dạng thức thất bại của hạ tầng khỏi chính sách nghiệp vụ.
2.1 Ranh giới giữa bên trong và bên ngoài
Application core có thể được xem như gồm mô hình miền và application service. Mô hình miền bảo vệ quy tắc nghiệp vụ và bất biến như số tiền, trạng thái, quyền hạn, tồn kho, còn application service điều phối trình tự thực thi một use case. Cả hai đều không được nhận trực tiếp các khái niệm bên ngoài như đối tượng request của web framework hay lazy loading của ORM.
Khi xác định ranh giới giữa lõi nghiệp vụ và phần công nghệ bên ngoài, hãy đặt câu hỏi về lý do thay đổi.
Thời hạn được phép hủy đơn thay đổi do chính sách, còn mã trạng thái HTTP thay đổi do hợp đồng API.
Nếu hai quy tắc nằm trong cùng một lớp, thay đổi bên này sẽ làm lung lay kiểm thử và triển khai bên kia, vì vậy các yếu tố có nguyên nhân thay đổi khác nhau được tách bằng ranh giới port và adapter.
| Phân loại | Câu hỏi cốt lõi | Thành phần tiêu biểu | Phụ thuộc bên ngoài được phép |
|---|---|---|---|
| Lõi miền | Điều gì luôn phải đúng trong nghiệp vụ? | Entity·value object·domain service | Cấm phụ thuộc trực tiếp framework·DB |
| Lõi ứng dụng | Điều phối use case theo trình tự nào? | Command·handler·ranh giới giao dịch | Chỉ phụ thuộc interface của port |
| Driving port | Bên ngoài yêu cầu chức năng nghiệp vụ nào? | PlaceOrderUseCase |
Dùng thuật ngữ nghiệp vụ hơn thuật ngữ kỹ thuật |
| Driven port | Lõi cần năng lực bên ngoài nào? | OrderRepository, PaymentPort |
Phụ thuộc hợp đồng chứ không phải hiện thực |
| Driving adapter | Chuyển đầu vào bên ngoài thành lời gọi lõi thế nào? | REST·CLI·consumer | Giao thức·xác thực·giải tuần tự hóa |
| Driven adapter | Chuyển lời gọi lõi thành công nghệ bên ngoài thế nào? | SQL·Kafka·SDK | Ánh xạ·retry·timeout |
Các phân loại trong bảng không có nghĩa là đơn vị triển khai. Ngay trong một monolith nhỏ cũng có thể tồn tại đầy đủ port và adapter; ngược lại, dù là microservice mà lõi bị công nghệ bên ngoài làm ô nhiễm thì vẫn không tuân thủ nguyên tắc lục giác. Quyết định kiến trúc ưu tiên việc tách phụ thuộc, trách nhiệm và ranh giới thay đổi hơn là tách tiến trình.
2.2 Ý nghĩa và chiều của port
Port không phải là tập hợp mọi phương thức CRUD đơn thuần, mà là ranh giới của một cuộc hội thoại có mục đích.
Nếu phơi bày bừa bãi save(), find(), update(), adapter sẽ thao túng cấu trúc dữ liệu nội bộ của lõi, vì vậy cần dùng tên và đầu vào bảo toàn ý định của use case và bất biến của miền.
Ví dụ, reserveStock(productId, quantity) thể hiện chính sách đặt giữ tồn kho, còn updateInventoryRow() lại thể hiện cấu trúc kho lưu trữ.
Driving port là hợp đồng đầu vào mà lõi cung cấp cho bên ngoài.
REST và message consumer có thể gọi cùng một use case theo những cách khác nhau, nhưng từ góc nhìn của lõi có thể thống nhất thành cùng một cuộc hội thoại truyền PlaceOrderCommand.
Nếu port biết đến HTTP header hay offset của broker, ranh giới kỹ thuật sẽ xâm nhập vào bên trong, nên chỉ những ý nghĩa cần cho xử lý nghiệp vụ như chủ thể xác thực, trace ID mới được truyền qua một ngữ cảnh riêng.
Driven port khai báo kết quả mà lõi muốn nhận từ bên ngoài.
Port lưu trữ biểu đạt ý nghĩa của việc lưu bền và điều kiện đồng thời, còn port thông báo biểu đạt mục đích “thông báo sự kiện đơn hàng đã được tiếp nhận”.
Giá trị trả về và lỗi của port cũng phải được thiết kế theo góc nhìn nghiệp vụ; không lan truyền nguyên SQLException hay ngoại lệ của SDK cụ thể vào lõi, mà chuyển thành các ý nghĩa như có thể/không thể retry, trùng lặp, bị từ chối.
Việc nhiều adapter được kết nối vào một port là đặc tính quan trọng của phong cách này.
Khi gắn adapter thanh toán thật, adapter sandbox và bộ mô phỏng sự cố vào PaymentPort, có thể hoán đổi công nghệ giữa các môi trường vận hành, kiểm chứng và phát triển.
Tuy nhiên, không cần tạo port cho mọi hệ thống bên ngoài; nên trừu tượng hóa trước những ranh giới có khả năng thay thế, cần cô lập kiểm thử hoặc mang ý nghĩa nghiệp vụ.
2.3 Đảo ngược phụ thuộc và lắp ráp
Đảo ngược phụ thuộc không phải là khẩu hiệu “hãy trừu tượng hóa”, mà là thiết kế thay đổi quyền sở hữu của phụ thuộc. Trong cấu trúc truyền thống, service gọi hiện thực DB và mã nghiệp vụ có thể được viết theo API mà DB cung cấp. Trong cấu trúc lục giác, lõi khai báo port là hợp đồng mình cần, và adapter bên ngoài hiện thực port đó.
sequenceDiagram
participant C as Client
participant A as REST Adapter
participant P as PlaceOrder Port
participant S as Application Core
participant R as OrderRepository Port
participant DB as SQL Adapter
participant G as Payment Adapter
C->>A: POST /orders
A->>P: PlaceOrderCommand
P->>S: execute(command)
S->>R: load(orderId)
R->>DB: SELECT / transaction
DB-->>R: Order aggregate
S->>G: authorize(amount)
G-->>S: approved / declined
S->>R: save(order)
R->>DB: INSERT or UPDATE
S-->>A: OrderResult
A-->>C: HTTP response
Lắp ráp (composition) là bước kết nối port với adapter hiện thực trong môi trường thực thi.
Có thể dùng container tiêm phụ thuộc (dependency injection), nhưng bản thân container không bảo đảm cấu trúc lục giác.
Tại điểm khởi động production sẽ kết nối PostgresOrderRepository, RealPaymentAdapter, RestController, còn trong kiểm thử sẽ kết nối InMemoryOrderRepository, StubPaymentAdapter.
Nếu mã lắp ráp quản lý khác biệt giữa các môi trường tại một chỗ, có thể giảm việc lõi trực tiếp đọc tệp cấu hình hoặc singleton toàn cục. Để không vô tình kết nối adapter in-memory chỉ dành cho môi trường phát triển vào vận hành, cần quản lý song song việc kiểm tra cấu hình khi khởi động và danh sách phụ thuộc. Khi kết nối adapter thất bại, việc khởi động ứng dụng một phần hay thất bại ngay lập tức cũng phải được quyết định theo yêu cầu tính sẵn sàng của dịch vụ.
3. Thiết kế từng thành phần và quy trình hiện thực
3.1 Application core lấy use case làm trung tâm
Use case được định nghĩa theo mục đích nghiệp vụ, không theo màn hình người dùng.
Thay vì “nút lưu” của màn hình đơn hàng, các tên như tiếp nhận đơn hàng, hủy thanh toán, thay đổi địa chỉ giao hàng phù hợp hơn làm tên use case của lõi.
Như vậy, khi REST, ứng dụng di động, batch gọi cùng một chức năng nghiệp vụ thì chỉ có cách biểu diễn khác nhau, còn quy tắc không bị trùng lặp.
Application service kiểm tra đầu vào, truy vấn các aggregate cần thiết, gọi hành vi miền, và điều phối ranh giới lưu trữ và phát hành sự kiện. Tuy nhiên, nếu dồn toàn bộ quy tắc mang ý nghĩa nghiệp vụ như tính tỷ lệ giảm giá hay chuyển trạng thái đơn hàng vào service, mô hình miền sẽ trở nên nghèo nàn (anemic). Cần phân bổ trách nhiệm để service đảm nhận điều phối (orchestration) còn đối tượng miền bảo vệ bất biến.
Đầu vào của use case được tách khỏi DTO bên ngoài.
Dù API bên ngoài dùng customer_id, lõi không có lý do phải biết quy ước đặt tên JSON, và adapter sẽ chuyển thành các kiểu mang ý nghĩa như CustomerId và Money.
Ngược lại, phản hồi cũng không trả nguyên entity miền mà chuyển thành DTO kết quả của use case để ngăn rò rỉ thuộc tính nội bộ và phụ thuộc tuần tự hóa.
3.2 Mô hình miền và bất biến
Entity miền có định danh và vòng đời, còn value object hoàn chỉnh ý nghĩa bằng giá trị và quy tắc kiểm tra.
Quy tắc không cho Order ở trạng thái CANCELLED được phê duyệt thanh toán lại phải nằm bên trong miền.
Nếu quy tắc này chỉ nằm trong câu lệnh if của controller, trạng thái sai sẽ được tạo ra khi batch adapter hoặc message adapter đi vòng qua.
Mô hình miền không phải là bản sao của bảng cơ sở dữ liệu.
Nếu biến mọi cột của một bảng thành thuộc tính công khai, ai cũng có thể thay đổi trạng thái trực tiếp và bất biến bị suy yếu.
Cần giới hạn thay đổi trạng thái thành các hành vi có ý nghĩa như cancel(reason), confirmPayment(reference), và từ chối các chuyển trạng thái không hợp lệ bằng lỗi miền tường minh.
Sự kiện miền biểu đạt những sự thật có ý nghĩa bên trong lõi.
OrderPlaced có nghĩa là “đã xảy ra một sự kiện mà tồn kho, thông báo, quyết toán giờ có thể phản ứng”, và không áp đặt lệnh hiện thực cụ thể lên consumer.
Khi phát hành sự kiện ra broker bên ngoài, adapter tuần tự hóa sẽ thêm phiên bản schema, trace ID, khóa xử lý trùng lặp, nhưng không được để mô hình miền biết đến header của Kafka.
3.3 Port kho lưu trữ và adapter lưu bền
Port kho lưu trữ biểu đạt ý nghĩa truy vấn và lưu trữ mà miền cần. Port khôi phục aggregate và port truy vấn khối lượng lớn cho màn hình phân tích có yêu cầu về hiệu năng, hình dạng và tính nhất quán khác nhau, nên tốt hơn là không dồn tất cả vào một repository vạn năng. Nếu read model chỉ là một phép chiếu (projection) đơn giản, có thể đặt thành query port riêng để tách khỏi độ phức tạp của mô hình miền ghi.
SQL adapter đảm nhận ánh xạ giữa hàng và đối tượng miền. Cách đưa trực tiếp annotation entity của ORM vào đối tượng miền có thể tăng năng suất ban đầu, nhưng lazy loading, proxy và vòng đời lưu bền có thể xâm nhập vào quy tắc nghiệp vụ. Dù dùng ORM tùy theo năng lực công nghệ của tổ chức và yêu cầu hiệu năng, vẫn cần quyết định một cách có ý thức mức độ gắn kết giữa mô hình miền và chiến lược ánh xạ.
Ranh giới giao dịch được gắn với yêu cầu nhất quán của use case.
Nếu không thể gộp lưu đơn hàng và phê duyệt thanh toán vào một giao dịch cục bộ, cần nêu rõ bù trừ khi phê duyệt thất bại, retry, tính lũy đẳng (idempotency) và truy vấn trạng thái trong use case và port.
Dù adapter cung cấp giao dịch cơ sở dữ liệu, điều đó không tự động bảo đảm tính nguyên tử của hệ thống thanh toán bên ngoài.
3.4 Trách nhiệm chuyển đổi của adapter hệ thống bên ngoài
Adapter trông như mã chuyển tiếp đơn giản, nhưng thực chất là bộ phiên dịch bảo toàn ý nghĩa tại ranh giới.
Khi công ty thanh toán bên ngoài trả về AUTHORIZED, PENDING, DECLINED, lõi không dùng nguyên các chuỗi này mà chuyển thành trạng thái nội bộ như đã phê duyệt, đang xử lý, bị từ chối.
Cần quản lý bảng ánh xạ và contract test để tên gọi và hệ thống lỗi của hệ thống bên ngoài không làm ô nhiễm mô hình nội bộ.
Adapter mạng có chính sách timeout và retry. Không thể áp dụng cùng một chính sách cho truy vấn an toàn khi retry và yêu cầu phê duyệt có thể gây thanh toán trùng. Cần liên kết idempotency key, lỗi có thể retry, exponential backoff, circuit breaker, truy vấn bù trừ với hợp đồng port và chỉ số vận hành.
Message adapter tách message kỹ thuật khỏi sự kiện nghiệp vụ. Offset của broker đã được commit không có nghĩa là xử lý nghiệp vụ đã hoàn tất, và cần tính đến xử lý lại và hàng đợi cách ly khi xử lý thất bại. Consumer phải giả định có thể nhận cùng một sự kiện hai lần và bảo đảm tính lũy đẳng dựa trên event ID hoặc khóa nghiệp vụ.
3.5 Quy trình áp dụng từng bước
Thứ nhất, chọn các use case thay đổi thường xuyên và có rủi ro nghiệp vụ lớn. Thay vì viết lại toàn bộ hệ thống một lần, hãy vẽ phụ thuộc hiện tại và điểm sự cố cho các luồng có hiệu quả kiểm thử và thay thế rõ ràng như thanh toán, phân quyền, hủy đơn hàng. Tiêu chí thành công của đối tượng áp dụng bao gồm thời gian chạy kiểm thử, phạm vi tệp thay đổi, khả năng thay thế công nghệ bên ngoài, mức độ cô lập sự cố.
Thứ hai, chuẩn hóa ngôn ngữ miền và use case. Thống nhất trạng thái, hành vi, lỗi, ngoại lệ với người phụ trách nghiệp vụ, và không thiết kế port chỉ dựa trên tên bảng kỹ thuật hay tên màn hình. Xác nhận các thuật ngữ đã định nghĩa được dùng nhất quán trong mã, API, kiểm thử, dashboard vận hành.
Thứ ba, khai báo tách biệt input port và output port. Tài liệu hóa bên gọi, chủ sở hữu, đầu vào/ra, lỗi, giới hạn thời gian, yêu cầu nhất quán của từng port. Port quá lớn khiến mọi thay đổi dồn vào cùng một interface, port quá nhỏ làm tăng trừu tượng hóa vô nghĩa, nên tìm mức chia nhỏ thích hợp dựa trên use case và khả năng thay thế.
Thứ tư, tạo kiểm thử đơn vị chạy lõi mà không cần công nghệ bên ngoài. Dùng kho lưu trữ in-memory và adapter thanh toán kiểm thử để kiểm chứng điều kiện bình thường, thất bại, biên, và xác nhận kiểm thử có thể lặp lại nhanh. Sau đó bổ sung kiểm thử tích hợp adapter và contract test dùng DB, message, API bên ngoài thật.
Thứ năm, đưa việc lắp ráp và khả năng quan sát lên mức vận hành. Phải có thể kiểm tra trên dashboard vận hành việc kết nối adapter theo môi trường, kiểm tra cấu hình, trace ID, độ trễ theo port, tỷ lệ lỗi bên ngoài, số lần retry, hàng đợi cách ly. Dù đã tách cấu trúc, nếu không đo được thì khó đánh giá khi có sự cố liệu ranh giới có thực sự hữu ích hay không.
4. Thiết kế kiểm thử, chất lượng và vận hành
4.1 Kim tự tháp kiểm thử và kiểm thử port
Lợi ích trực tiếp nhất của cấu trúc lục giác là có thể kiểm thử quy tắc nghiệp vụ cốt lõi tách khỏi môi trường công nghệ. Kiểm thử đơn vị miền xác nhận chuyển trạng thái và bất biến mà không cần DB hay HTTP server, còn kiểm thử use case xác nhận trình tự gọi, xử lý thất bại, ý nghĩa giao dịch thông qua test double của port. Kiểm thử nhanh không có nghĩa là lỗi ánh xạ của adapter thật biến mất, nên cần tách mục đích theo từng tầng.
| Mức kiểm thử | Đối tượng | Double·môi trường chính | Câu hỏi cần xác nhận |
|---|---|---|---|
| Kiểm thử đơn vị miền | Value object·entity·chính sách | Không có phụ thuộc bên ngoài | Có từ chối trạng thái sai không? |
| Kiểm thử use case | Application service | Port in-memory·stub | Đầu vào, đầu ra, luồng lỗi có đúng không? |
| Kiểm thử tích hợp adapter | Adapter SQL·HTTP·message | Hạ tầng thật hoặc tương thích | Ánh xạ·timeout·tuần tự hóa có đúng không? |
| Contract test | Port và hợp đồng bên ngoài | Hợp đồng consumer·provider | Khi thay đổi, hai bên có tương thích không? |
| Kiểm thử đầu cuối | Kịch bản nghiệp vụ chính | Môi trường tương tự vận hành | Hệ thống đã lắp ráp có hoàn thành luồng mục tiêu không? |
Test double tiện lợi nhưng có thể không tái hiện được tính đồng thời, ràng buộc và độ trễ của hạ tầng thật. Ví dụ, kho lưu trữ in-memory không tự động mô phỏng ràng buộc unique hay mức cô lập của SQL. Vì vậy, quy tắc của lõi được bảo vệ bằng kiểm thử nhanh, còn các thất bại thực tế của adapter được bổ sung bằng kiểm thử tích hợp dựa trên container và thử nghiệm sự cố.
4.2 Thiết kế hợp đồng và lỗi
Hợp đồng port chỉ định nghĩa kết quả thành công là chưa đủ. Cần định nghĩa khi xảy ra chậm trễ thanh toán bên ngoài, xung đột lưu trữ, hết hạn xác thực, message trùng lặp, lệch phiên bản schema thì lõi tạo ra trạng thái nghiệp vụ nào và adapter chọn đường phục hồi nào. Lỗi có thể được quản lý theo thông điệp hiển thị cho người dùng, khả năng retry, đối tượng kiểm toán, mức cảnh báo vận hành.
API adapter phân biệt các phản hồi như HTTP 400·409·429·500 thành lỗi nghiệp vụ và lỗi kỹ thuật. Cùng là phản hồi 500, chiến lược retry sẽ khác nhau tùy đó là sự cố bên ngoài tạm thời hay vi phạm hợp đồng vĩnh viễn. Nếu lõi không trực tiếp phán đoán mã số HTTP mà trả về kiểu lỗi có ý nghĩa, REST adapter, message adapter, batch adapter có thể tự chuyển đổi thành biểu diễn phù hợp.
4.3 Bảo mật và khả năng quan sát
Ranh giới càng nhiều thì trách nhiệm bảo mật càng tăng. Input adapter kiểm tra thông tin xác thực và phân quyền, còn lõi xác nhận trong ngữ cảnh nghiệp vụ liệu chủ thể gọi có quyền thực thi use case hay không. Adapter bên ngoài không ghi bí mật vào log, che (masking) yêu cầu và phản hồi nhạy cảm, và dùng thông tin xác thực với quyền tối thiểu cần thiết cho mỗi lời gọi port.
Khả năng quan sát được thiết kế dọc theo ranh giới giữa lõi và adapter. Khi theo dõi tên use case, tên port, đích gọi bên ngoài, số lần retry, correlation ID, có thể phân biệt bản thân “tiếp nhận đơn hàng” chậm hay “adapter thanh toán” chậm. Tuy nhiên, không đưa thông tin cá nhân nghiệp vụ vào trace ID, và phải xác định đồng thời thời hạn lưu giữ dữ liệu và quyền truy cập.
5. So sánh với các kiến trúc tương tự
5.1 So sánh với kiến trúc phân lớp
Kiến trúc phân lớp truyền thống sắp xếp theo chiều dọc các tầng trình bày, dịch vụ, truy cập dữ liệu. Cấu trúc dễ hiểu và có thể áp dụng nhanh cho hệ thống CRUD, nhưng nếu service tầng trên phụ thuộc trực tiếp vào ORM hay mô hình DB tầng dưới, quy tắc nghiệp vụ có thể bị cách lưu trữ chi phối. Ngoài ra, nếu thiết kế chỉ giả định một điểm vào duy nhất, quy tắc có thể bị sao chép trong xử lý batch và message.
Kiến trúc lục giác cũng có thể dùng tầng, nhưng nhấn mạnh tính độc lập của lõi và quyền sở hữu port hơn là tên tầng. Trong cấu trúc phân lớp, nếu service gọi hiện thực repository thì phụ thuộc có thể hướng ra ngoài, còn trong cấu trúc lục giác, lõi định nghĩa port và adapter hiện thực nó. Do đó, hai kiến trúc không loại trừ nhau mà có thể hiểu là quan hệ trong đó kiến trúc phân lớp được củng cố bằng đảo ngược phụ thuộc và ranh giới đầu vào/ra.
5.2 So sánh với Clean Architecture và Onion Architecture
Clean Architecture và Onion Architecture cũng chia sẻ nguyên tắc tách miền khỏi công nghệ bên ngoài và phụ thuộc phải hướng vào trong. Thuật ngữ và số tầng trong hình vẽ có thể khác nhau, nhưng cả ba cách tiếp cận đều đặt framework, UI, DB ở bên ngoài chính sách. Kiến trúc lục giác nhấn mạnh trực quan tính đối xứng giữa đầu vào và đầu ra, mục đích của port và khả năng thay thế của nhiều adapter.
Trong thực tế có thể kết hợp cả ba cách tiếp cận. Ví dụ, định nghĩa port lục giác bên trong cấu trúc onion lấy miền làm trung tâm, đồng thời dùng use case interactor và enterprise rule của Clean Architecture. Điều quan trọng không phải là thống nhất tên gọi, mà là bảo đảm một lõi có thể kiểm thử, ranh giới rõ ràng, phụ thuộc kiểm soát được và việc lắp ráp có thể vận hành.
| Hạng mục so sánh | Phân lớp | Lục giác | Họ Clean·Onion |
|---|---|---|---|
| Mối quan tâm trung tâm | Tầng theo trách nhiệm | Ranh giới port và adapter | Chính sách bên trong và quy tắc phụ thuộc |
| Góc nhìn đầu vào/ra | Chủ yếu UI→service→DB | Xử lý driving·driven đối xứng | Biểu diễn bằng ranh giới·use case |
| Ưu điểm cốt lõi | Đơn giản·dễ học | Thay thế công nghệ·cô lập kiểm thử | Độc lập chính sách và khả năng mở rộng |
| Rủi ro chính | DB·framework xâm nhập | Interface quá mức·boilerplate | Cấu trúc phức tạp và hiểu sai về tầng |
| Tình huống phù hợp | CRUD đơn giản·vòng đời ngắn | Quy tắc phức tạp·nhiều liên kết | Sản phẩm dài hạn·hệ thống nhiều thay đổi |
5.3 Quan hệ với microservice
Kiến trúc lục giác và microservice là các quyết định ở những chiều khác nhau. Cái trước xử lý phụ thuộc và ranh giới bên trong một ứng dụng, cái sau là cách tổ chức hệ thống tách biệt triển khai, vận hành và quyền sở hữu. Một monolith cũng có thể là lục giác, và từng microservice trong số nhiều microservice cũng có thể là lục giác.
Áp dụng nguyên tắc lục giác bên trong microservice giúp API, message, DB bên ngoài không chia sẻ trực tiếp lõi nghiệp vụ của dịch vụ. Tuy nhiên, nếu chia ranh giới dịch vụ sai, dù sắp xếp port và adapter tốt đến đâu thì lời gọi giữa các dịch vụ vẫn tăng và trở thành distributed monolith. Trước hết cần đánh giá ranh giới miền, quyền sở hữu dữ liệu, khả năng triển khai độc lập, cô lập sự cố, rồi thiết kế cấu trúc bên trong để hỗ trợ các mục đích đó.
6. Tình huống áp dụng
6.1 Tình huống hệ thống đặt hàng và thanh toán trực tuyến
Trong hệ thống đặt hàng trực tuyến, có thể định nghĩa PlaceOrder là driving port và thiết kế để các adapter REST, di động, batch truyền lệnh đặt hàng.
Lõi điều phối trình tự kiểm tra tồn kho, áp dụng giảm giá, chuyển trạng thái đơn hàng, yêu cầu phê duyệt thanh toán nhưng không trực tiếp xử lý HTTP header hay SDK của công ty thẻ.
Khi gắn adapter công ty thẻ thật và adapter thanh toán kiểm thử vào PaymentPort, có thể kiểm chứng độc lập các trường hợp phê duyệt thành công, bị từ chối, chậm trễ.
Ví dụ, với giả định API đặt hàng nhận 2,000 yêu cầu mỗi giây vào giờ cao điểm, kiểm thử lõi phải chạy nhanh bất kể số máy chủ hay số kết nối cơ sở dữ liệu.
Kiểm thử tải thực tế được thực hiện trong môi trường đã lắp ráp bao gồm adapter SQL, cache, thanh toán, và ranh giới port giúp phân biệt tốc độ kiểm thử với vị trí sự cố.
Nếu phê duyệt thanh toán kết thúc bất đồng bộ ở bên ngoài, cần nêu rõ trạng thái PAYMENT_PENDING và đặt chính sách lũy đẳng để dù cùng khóa thanh toán được gửi lại cũng không bị phê duyệt trùng.
Đánh đổi quan trọng nhất trong tình huống này là số lượng trừu tượng hóa và độ chính xác khi thanh toán thất bại. Nếu bọc mọi phương thức SDK thành port, lõi trở nên phức tạp và ưu điểm của hiện thực bên ngoài có thể mất đi. Ngược lại, các chức năng rủi ro nghiệp vụ lớn như phê duyệt, hủy, truy vấn được bọc thành port, còn các công nghệ có hiệu quả thay thế thấp như thư viện ghi log đơn giản thì được dùng trực tiếp một cách hạn chế ở tầng bên ngoài.
6.2 Tình huống giám sát thiết bị sản xuất
Ứng dụng giám sát thiết bị nhà máy có thể được xây dựng để MQTT consumer, bộ thu thập tệp, bộ mô phỏng cùng gọi input port ReceiveTelemetry.
Lõi quản lý chuyển đổi đơn vị giá trị cảm biến, phán định dấu hiệu bất thường, điều kiện cảnh báo, chuyển trạng thái thiết bị, và không biết định dạng message của một nhà sản xuất thiết bị cụ thể.
Khi gắn adapter cơ sở dữ liệu chuỗi thời gian và adapter lưu trữ tạm tại hiện trường vào port TelemetryStore, có thể thay đổi chính sách đệm khi mất kết nối mạng.
Ví dụ, quy tắc tạo sự kiện cảnh báo khi giá trị nhiệt độ độ C vượt 80 độ và được quan sát liên tiếp 3 lần sẽ được kiểm chứng bằng kiểm thử miền. Khoảng thời gian kết nối lại MQTT, QoS, message trùng lặp, độ trễ lưu chuỗi thời gian được kiểm chứng bằng kiểm thử tích hợp adapter và thử nghiệm sự cố. Khi tách quy tắc khỏi sự cố truyền thông như vậy, việc thay cảm biến sẽ không làm lung lay không cần thiết kiểm thử hồi quy của chính sách cảnh báo.
Tuy nhiên, trong hệ thống hiện trường, tính thời gian thực và an toàn là cốt lõi nên chỉ tách port đơn thuần là không đủ. Cần đưa các điều kiện vận hành như hoạt động ngoại tuyến, đảo thứ tự thời gian, xác thực thiết bị, gửi lại lệnh, dừng an toàn vào hợp đồng port và mô hình trạng thái. Để trừu tượng hóa adapter không che giấu đặc tính thất bại của thiết bị thật và cản trở phán đoán an toàn, cần nêu rõ các chế độ thất bại và giá trị mặc định bảo thủ.
6.3 Tình huống dịch vụ dân sự công
Dịch vụ dân sự công có thể để cổng web, di động, màn hình tư vấn viên, batch ban đêm cùng gọi use case SubmitApplication.
Input adapter xử lý xác thực và tải tệp theo từng kênh, còn lõi quản lý quy tắc về tư cách nộp đơn, trạng thái hồ sơ, thời hạn xử lý, phân công cơ quan phụ trách.
Lưu trữ tài liệu điện tử, tin nhắn thông báo, liên kết thông tin hành chính được đặt thành adapter của từng driven port để cô lập thay đổi của một kênh hay hệ thống cơ quan cụ thể.
Ví dụ, chính sách phải phân công cơ quan phụ trách trong vòng 24 giờ sau khi tiếp nhận đơn được xử lý bằng cách tách quy tắc thời gian của lõi khỏi việc lập lịch của batch adapter.
Khi liên kết hành chính thực tế tạm ngừng, đơn không bị thất lạc mà được giữ ở trạng thái chờ liên kết, và duy trì tính liên tục nghiệp vụ thông qua xử lý lại và thông báo cho người phụ trách.
Thông tin cá nhân của người dân được giảm thiểu trong DTO của port và log, đồng thời áp dụng chính sách lưu giữ và masking theo từng adapter.
7. Chuyên sâu: Liên kết với DDD, kiến trúc hướng sự kiện và cloud native
7.1 Kết hợp với DDD
Kiến trúc lục giác mô tả hướng kết nối ra bên ngoài và sự cô lập công nghệ, còn DDD mô tả mô hình và ranh giới bên trong lõi. Do đó, trong hệ thống nghiệp vụ phức tạp, có thể lấy bounded context làm ranh giới ứng dụng, biểu diễn use case của mỗi context thành driving port và tích hợp bên ngoài thành driven port.
Tuy nhiên, việc áp dụng port và adapter không tự động làm mô hình miền trở nên tốt hơn. Nếu sao chép nguyên bảng và API thành interface, mô hình bên trong vẫn lấy công nghệ làm trung tâm. Phải thiết kế đồng thời ngôn ngữ chung (ubiquitous language), bất biến aggregate, sự kiện và quy tắc dịch giữa các context thì việc cô lập công nghệ bên ngoài mới dẫn đến bảo vệ ý nghĩa nghiệp vụ.
7.2 Tích hợp hướng sự kiện và nhất quán cuối cùng
Dùng event adapter có thể giảm gắn kết thời gian giữa lõi và consumer, nhưng tính nhất quán tức thời có thể bị suy yếu.
Nếu lõi đơn hàng phát hành OrderPlaced để tồn kho, thông báo, quyết toán xử lý riêng, cần đối phó với consumer thất bại, trùng lặp, đảo thứ tự, tiến hóa schema.
Outbox và hàng đợi xử lý lại là cách giảm thất lạc giữa lưu trữ và phát hành, còn tính lũy đẳng của consumer là cách giảm ảnh hưởng của trùng lặp.
Khi chọn sự kiện, không biến mọi lời gọi phương thức thành sự kiện. Cần phân biệt sự thật nghiệp vụ mà context khác cần biết với thay đổi trạng thái nội bộ đơn thuần, và xác định chủ sở hữu sự thật cùng chính sách phiên bản schema. Nếu cố biến nghiệp vụ phê duyệt tức thì vốn phù hợp với port đồng bộ thành bất đồng bộ, trải nghiệm người dùng và logic bù trừ có thể trở nên phức tạp.
7.3 Vận hành cloud native
Trong môi trường container và serverless, hạ tầng bên ngoài được thay thế thường xuyên hơn và sự cố xảy ra cục bộ. Cấu trúc lục giác tách adapter DB, message, API bên ngoài khỏi cấu hình vận hành, giúp làm rõ khác biệt kết nối giữa phát triển, kiểm thử, staging, production. Tuy nhiên, khi lời gọi mạng tăng, cần xử lý timeout, circuit breaker, giới hạn tốc độ, lan truyền trace, đo lường chi phí như hợp đồng phi chức năng của port.
Khả năng thay thế adapter không đồng nghĩa ngay với tính khả chuyển multi-cloud. Di chuyển dữ liệu, khác biệt tính năng của managed service, quy định và tính địa phương, năng lực đội vận hành mới quyết định chi phí di chuyển thực tế. Kỹ sư chuyên nghiệp đánh giá phân biệt giữa khả năng về cấu trúc “có thể thay thế” và khả năng về kinh doanh “có thể di chuyển một cách kinh tế”.
7.4 Hướng ra đề dự kiến và chiến lược xây dựng bài làm
Trong bài thi, không chỉ viết định nghĩa mà cần liên kết tình huống vấn đề với nguyên lý giải quyết. Trước tiên nêu vấn đề logic nghiệp vụ bị phụ thuộc vào web, DB, hệ thống bên ngoài, rồi trình bày cấu trúc lõi, port, adapter, đảo ngược phụ thuộc bằng sơ đồ khái niệm tổng thể. Tiếp đó triển khai phân biệt driving và driven, thiết kế port, chuyển đổi của adapter, hiệu quả kiểm thử và vận hành kèm tình huống thì mạch bài luận sẽ tự nhiên.
Với đề so sánh, giải thích nguyên tắc chung và khác biệt với kiến trúc phân lớp, Clean, Onion, và nhất định phải đề cập rủi ro thiết kế thừa cho CRUD đơn giản. Ở phần tình huống, chọn một trong thanh toán, IoT, dịch vụ công để cụ thể hóa input adapter và output adapter, bất biến cốt lõi, ứng phó sự cố, chiến lược kiểm thử. Cuối cùng, trong phần lưu ý theo góc nhìn Kỹ sư chuyên nghiệp, trình bày năng lực tổ chức, khả năng quan sát, bảo mật, chi phí, áp dụng từng bước để thể hiện sự cân bằng trong lựa chọn công nghệ.
8. Lưu ý và hàm ý
8.1 Chi phí trừu tượng hóa và ranh giới thích hợp
Mục tiêu không phải là thêm một interface cho mỗi lớp bên ngoài. Nếu biến cả những yếu tố có khả năng thay thế, nhu cầu kiểm thử, ý nghĩa nghiệp vụ, hiệu quả cô lập sự cố thấp thành port, chi phí tra cứu mã và bảo trì sẽ tăng. Ưu tiên trừu tượng hóa use case cốt lõi, liên kết bên ngoài rủi ro cao, ranh giới dự kiến có thay đổi, và mở rộng ranh giới dựa trên các vấn đề thực tế lặp lại.
8.2 Tính ổn định của port và tiến hóa hợp đồng
Port che giấu hiện thực nội bộ, nhưng nếu thiết kế sai sẽ trở thành một điểm gắn kết khác cố định nguyên DTO bên ngoài và thuật ngữ kỹ thuật. Tài liệu hóa tên, đầu vào, đầu ra, lỗi, giới hạn thời gian, điều kiện lũy đẳng của port bằng ngôn ngữ nghiệp vụ, và quản lý thay đổi phiên bản cùng chính sách tương thích ngược bằng contract test. Nếu phải thay đổi hợp đồng thường xuyên, hãy xem xét lại liệu port có đang phơi bày các lời gọi kỹ thuật ở mức quá thấp hay không.
8.3 Nhất quán dữ liệu và giao dịch
Việc tách port lưu trữ và port dịch vụ bên ngoài không giải quyết được vấn đề giao dịch phân tán. Phải nêu rõ theo từng use case giao dịch cục bộ, giao dịch bù trừ, nhất quán cuối cùng, xử lý trùng lặp, trạng thái tại thời điểm đọc. Phân biệt trạng thái hiển thị cho người dùng với trạng thái xử lý lại nội bộ, và đưa công việc đối soát, hiệu chỉnh để phát hiện vi phạm tính toàn vẹn vào quy trình vận hành.
8.4 Bảo mật và bảo vệ thông tin cá nhân
Adapter là ranh giới nơi đầu vào bên ngoài và thông tin xác thực đi vào, nên cần áp dụng kiểm tra đầu vào, xác minh chứng chỉ, quản lý bí mật, quyền tối thiểu, masking đầu ra. Nếu in nguyên đối tượng miền ra log, thông tin cá nhân có thể vượt ranh giới và bị rò rỉ, nên dùng DTO log an toàn và hệ thống phân loại trường. Trường hợp liên kết bên ngoài lưu trữ tại region ở nước ngoài hoặc chuyển cho bên xử lý thứ ba, cần xem xét không chỉ cấu trúc kỹ thuật mà cả căn cứ pháp lý và chính sách lưu giữ.
8.5 Sự cố, hiệu năng và khả năng vận hành
Nếu sao chép giá trị mặc định timeout và retry cho từng adapter, có thể xảy ra bùng nổ sự cố. Liên kết ngân sách theo từng lời gọi, giới hạn đồng thời, circuit breaker, pool cách ly, đường thay thế, ngưỡng cảnh báo với mục tiêu mức dịch vụ của hệ thống. Đo độ trễ, lỗi, retry, tồn đọng hàng đợi theo từng port để xác định ranh giới nào là nút thắt, và kiểm thử tải thực tế để trừu tượng hóa không che giấu vấn đề hiệu năng.
8.6 Chuyển đổi từng bước và năng lực tổ chức
Cách viết lại toàn bộ hệ thống kế thừa có nguy cơ đồng thời đánh mất tri thức nghiệp vụ và sự ổn định vận hành. Cách an toàn là bọc các use case thay đổi thường xuyên thành port trước, bao các chức năng hiện có bằng adapter, rồi vừa bảo đảm kiểm thử vừa dần chỉnh lý lõi. Không chỉ lập trình viên mà cả người phụ trách vận hành, bảo mật, dữ liệu cũng phải hiểu hợp đồng thất bại và chỉ số quan sát của port thì cấu trúc mới được duy trì trong vận hành thực tế.
Tài liệu tham khảo
- Alistair Cockburn, Hexagonal Architecture
- AWS Prescriptive Guidance, Hexagonal architecture pattern
- Martin Fowler, Microservices
Tóm tắt một câu: Kiến trúc lục giác là phương pháp thiết kế khiến lõi nghiệp vụ chỉ nhìn vào các hợp đồng hướng mục đích gọi là port, còn công nghệ được thay thế và cô lập bằng adapter, qua đó nâng cao khả năng kiểm thử, dễ thay đổi và năng lực ứng phó sự cố.