Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Gom code theo layer, module hay aggregate?

Câu hỏi bài này trả lời: một thay đổi nghiệp vụ đi qua những file nào, và cây thư mục nào giúp tìm đúng nơi sở hữu quy tắc mà vẫn giữ phụ thuộc rõ ràng?

Cần biết trước: hàm/class, import và transaction. Bài so ba cách bố trí cùng một ứng dụng đặt hàng giả lập, không chọn framework hay triển khai microservice. Các cây và đường dẫn đã kiểm nhất quán; phần hành vi là đặc tả minh họa, chưa phải ứng dụng chạy được.

Ba khái niệm trả lời ba câu hỏi

Khái niệmCâu hỏiVí dụ trong bài
LayerThành phần chịu trách nhiệm kỹ thuật gì, phụ thuộc vào đâu?HTTP nhận request, application điều phối, domain giữ quy tắc, adapter lưu SQL
ModuleNhóm code sở hữu vấn đề nghiệp vụ nào?ordering và inventory
AggregateNhóm đối tượng nào phải giữ quy tắc nhất quán qua một cửa cập nhật?Order cùng OrderItem, trạng thái và sự kiện hủy

Có thể kết hợp cả ba: chia module trước, giữ layer trong mỗi module, gom các kiểu domain theo aggregate. Chúng không phải ba kiến trúc loại trừ nhau; bài so ba mức bố trí để thấy mỗi mức làm rõ điều gì.

Tên folder không tự tạo bounded context, transaction hay service. Module inventory chỉ có ý nghĩa nếu sở hữu quyết định về tồn/reservation và có hợp đồng sử dụng rõ; một folder chung toàn bộ logic vẫn có thể phụ thuộc chằng chịt vào phần còn lại.

Giữ nguyên nghiệp vụ để so

Ứng dụng có đặt và hủy đơn, giữ và trả tồn kho. Order giữ các dòng hàng, mỗi dòng tham chiếu sản phẩm bằng product_id, quantity dương; dữ liệu giá là snapshot của đơn. StockItem sở hữu số lượng của một sản phẩm trong inventory, không trở thành entity con của Order chỉ vì Order cần sản phẩm đó.

Order là cửa cập nhật trạng thái của các dòng và của đơn. Application nạp Order qua OrderStore, gọi hành vi rồi lưu; SqlOrderStore thực hiện lưu trữ, còn OrdersHttp chuyển dữ liệu request thành lệnh. OrderStore là port của use case trong ví dụ này; nó không chứa query SQL.

Ở inventory, StockItem giữ 0 <= reserved <= on_hand; giữ/trả tồn đi qua hành vi của nó. Order chỉ lưu mã sản phẩm và reservation cần liên hệ, không tự sửa lượng tồn. Quy tắc hủy thuộc ordering, còn việc trả reservation phải được inventory kiểm lại và xử lý trùng theo hợp đồng riêng.

Các file ở ba cây đều là cùng 18 file, cùng vai trò. Phần notification/thanh toán chưa nằm trong case; không dùng chúng để làm một phương án trông phức tạp hơn phương án khác.

A — Layer ở ngoài, kiểu domain ở trong

app/
|-- interfaces/
|   |-- OrdersHttp.py
|   `-- InventoryHttp.py
|-- application/
|   |-- PlaceOrder.py
|   |-- CancelOrder.py
|   |-- ReserveStock.py
|   |-- ReleaseStock.py
|   |-- OrderStore.py
|   `-- InventoryStore.py
|-- domain/
|   |-- entities/
|   |   |-- Order.py
|   |   |-- OrderItem.py
|   |   `-- StockItem.py
|   |-- enums/
|   |   `-- OrderStatus.py
|   `-- events/
|       `-- OrderCancelled.py
|-- infrastructure/
|   |-- SqlOrderStore.py
|   `-- SqlInventoryStore.py
`-- tests/
    |-- TestPlaceOrder.py
    |-- TestCancelOrder.py
    `-- TestReleaseStock.py

Nhìn tầng kỹ thuật dễ: toàn bộ adapter ở một chỗ, toàn bộ entry HTTP ở một chỗ. Khi đọc một hành vi đặt hàng, cần đi qua các thư mục trên cùng; đồng thời entities/ gom Order và StockItem dù hai kiểu thuộc hai vấn đề khác nhau.

Cấu trúc này vẫn có thể bảo vệ Order qua method, có test và có module logic rõ. Đừng suy rằng cây theo loại đồng nghĩa anemic domain hoặc sai DDD. Điểm yếu cần đo là chi phí tìm code và phụ thuộc thật, không phải tên Entities.

B — Module ở ngoài, layer ở trong

app/
`-- modules/
    |-- ordering/
    |   |-- interfaces/
    |   |   `-- OrdersHttp.py
    |   |-- application/
    |   |   |-- PlaceOrder.py
    |   |   |-- CancelOrder.py
    |   |   `-- OrderStore.py
    |   |-- domain/
    |   |   |-- Order.py
    |   |   |-- OrderItem.py
    |   |   |-- OrderStatus.py
    |   |   `-- OrderCancelled.py
    |   |-- infrastructure/
    |   |   `-- SqlOrderStore.py
    |   `-- tests/
    |       |-- TestPlaceOrder.py
    |       `-- TestCancelOrder.py
    `-- inventory/
        |-- interfaces/
        |   `-- InventoryHttp.py
        |-- application/
        |   |-- ReserveStock.py
        |   |-- ReleaseStock.py
        |   `-- InventoryStore.py
        |-- domain/
        |   `-- StockItem.py
        |-- infrastructure/
        |   `-- SqlInventoryStore.py
        `-- tests/
            `-- TestReleaseStock.py

Người sửa hủy đơn bắt đầu ở ordering/; người sửa cách giữ tồn bắt đầu ở inventory/. Layer vẫn có trách nhiệm như A. Nhiều aggregate trong một module có thể làm domain/ đông; khi đó mới xét gom theo cụm domain.

Quyết định được che giấu: ordering sở hữu chuyển trạng thái Order; inventory sở hữu cách tính và giữ tồn. Hai module trao đổi qua use case/contract hoặc sự kiện, không sửa trực tiếp entity hay SQL adapter của nhau. Cây này không đòi hai database hoặc hai deploy độc lập.

C — Trong module, domain gom theo aggregate

app/
`-- modules/
    |-- ordering/
    |   |-- interfaces/
    |   |   `-- OrdersHttp.py
    |   |-- application/
    |   |   |-- PlaceOrder.py
    |   |   |-- CancelOrder.py
    |   |   `-- OrderStore.py
    |   |-- domain/
    |   |   `-- OrderAggregate/
    |   |       |-- Order.py
    |   |       |-- OrderItem.py
    |   |       |-- OrderStatus.py
    |   |       `-- OrderCancelled.py
    |   |-- infrastructure/
    |   |   `-- SqlOrderStore.py
    |   `-- tests/
    |       |-- TestPlaceOrder.py
    |       `-- TestCancelOrder.py
    `-- inventory/
        |-- interfaces/
        |   `-- InventoryHttp.py
        |-- application/
        |   |-- ReserveStock.py
        |   |-- ReleaseStock.py
        |   `-- InventoryStore.py
        |-- domain/
        |   `-- StockItemAggregate/
        |       `-- StockItem.py
        |-- infrastructure/
        |   `-- SqlInventoryStore.py
        `-- tests/
            `-- TestReleaseStock.py

Order, trạng thái và sự kiện cùng cụm nên dễ đọc hơn khi có nhiều aggregate. Đó là cách thể hiện quyền sở hữu domain trên đĩa; cơ chế bảo vệ vẫn là API của root, test và lưu trữ có kiểm soát.

CancelOrder, HTTP và SQL adapter vẫn ở layer tương ứng, không nhét mọi file liên quan Order vào OrderAggregate/. Một use case có thể điều phối nhiều ranh giới; aggregate không sở hữu framework hay giao thức HTTP. Aggregate StockItem một entity vẫn hợp lệ nếu có invariant riêng; không cần thêm entity con để folder có vẻ giống mẫu.

Cùng thay đổi: thêm quy tắc và lý do hủy

Yêu cầu mới: đơn đã gửi (SHIPPED) không được hủy; hủy cần lý do không chỉ gồm khoảng trắng; hủy lại đơn CANCELLED không sinh thêm sự kiện. Root sở hữu quyết định chuyển trạng thái. HTTP chỉ kiểm định dạng request; caller khác vẫn phải đi qua quy tắc của root.

Phác thảo hành vi, không phải code của một ứng dụng đã chạy:

Order.cancel(reason):
  nếu status == CANCELLED: trả về không có event mới
  nếu status == SHIPPED: từ chối, không đổi status
  nếu reason.trim() rỗng: từ chối, không đổi status
  status = CANCELLED
  tạo OrderCancelled(order_id, reason.trim())

Thứ tự có chủ đích: yêu cầu hủy lặp là no-op, không ghi đè lý do hủy đầu tiên. Chính sách đó là lựa chọn nghiệp vụ của case, không phải quy tắc phổ quát. Kiểm authorization ở use case trước khi gọi root; tính idempotent không cho phép người lạ hủy đơn.

CancelOrder nhận lý do, nạp root, gọi cancel, rồi lưu trạng thái và sự kiện theo một ranh giới commit đã chọn. Inventory nhận mã Order từ sự kiện để trả reservation; lý do mới không buộc nó sửa code nếu không thuộc hợp đồng inventory. Không giả định commit của Order và xử lý inventory là một transaction phân tán tự động.

File thay đổiA — layer/typeB — module/layerC — module/aggregate
Order.pydomain/entities/Order.pymodules/ordering/domain/Order.pymodules/ordering/domain/OrderAggregate/Order.py
OrderCancelled.pydomain/events/OrderCancelled.pymodules/ordering/domain/OrderCancelled.pymodules/ordering/domain/OrderAggregate/OrderCancelled.py
CancelOrder.pyapplication/CancelOrder.pymodules/ordering/application/CancelOrder.pymodules/ordering/application/CancelOrder.py
OrdersHttp.pyinterfaces/OrdersHttp.pymodules/ordering/interfaces/OrdersHttp.pymodules/ordering/interfaces/OrdersHttp.py
TestCancelOrder.pytests/TestCancelOrder.pymodules/ordering/tests/TestCancelOrder.pymodules/ordering/tests/TestCancelOrder.py

Cả ba đều sửa 5 file; gom thư mục không tự giảm công việc nghiệp vụ. A đi qua năm khu vực kỹ thuật; B/C giữ toàn bộ thay đổi dưới ordering, còn C đặt hai kiểu domain cùng cụm. Trong case nhỏ này, C thêm tên aggregate mà chưa giảm số file so với B. Khi root/enum/event tách nhiều nơi và nhiều aggregate cùng tồn tại, tính cục bộ có thể đáng giá hơn; phải kiểm trên lịch sử thay đổi của dự án.

Hướng phụ thuộc và phép kiểm hành vi

Trong cả ba cây, application dùng domain và port; adapter thực hiện port; HTTP gọi use case, còn wiring chọn adapter. Domain không import HTTP client, SQL driver hoặc lớp lưu cụ thể. Đây là hợp đồng của ví dụ; kiểm bằng import/build rule mới cưỡng chế được, đổi tên folder không cưỡng chế được.

Case cần testKết quả mong đợiĐiều test bảo vệ
CONFIRMED, lý do hợp lệCANCELLED, một event chứa lý do đã trimQuy tắc và payload
SHIPPED, lý do hợp lệTừ chối, trạng thái giữ nguyên, không eventKhông hủy sau gửi
CONFIRMED, lý do trắngTừ chối, trạng thái giữ nguyênLý do bắt buộc
CANCELLED, yêu cầu lặpKhông event mới, giữ lý do đầuKhông phát sinh trả tồn lặp từ root
Hai caller cùng nạp CONFIRMEDMột commit hợp lệ hoặc phát hiện xung độtConcurrency ở persistence, không chỉ method trong RAM

Case cuối cần transaction/version check hoặc cơ chế tương đương ở adapter. Hai instance Order riêng có thể cùng thấy CONFIRMED và cùng tạo event trong RAM; aggregate root không tự khóa database. Phát sự kiện ra ngoài còn cần chống phát/trả tồn trùng ở nơi lưu và nơi nhận.

Các case là acceptance criteria để người đọc triển khai, chưa có số test chạy hay benchmark của ba ứng dụng. Phép kiểm bài này kiểm cây/path và nội dung tài liệu; nó không chứng minh import đúng hay concurrency đã giải quyết.

Chọn và đổi cấu trúc theo chi phí thật

Hoàn cảnhCách bố trí đáng thửChi phí cần giữ trong tầm kiểm soát
Ứng dụng nhỏ, ít quy tắc, đội quen layerA có thể đủTheo dõi file theo một nghiệp vụ, tránh domain dùng adapter trực tiếp
Nhiều nhóm nghiệp vụ, quyền sở hữu rõBHợp đồng module, tránh gọi vào chi tiết nội bộ module khác
Nhiều aggregate và kiểu phụ thuộc riêng mỗi rootC bên trong BXác định invariant đúng; tránh tách aggregate chỉ theo bảng

Lấy vài thay đổi đã xảy ra: tìm root mất bao lâu, phải mở bao nhiêu vùng code, import nào đi qua biên và có sửa nhầm invariant không. Đếm file là một chỉ dấu; năm file nằm cạnh nhau vẫn có thể khó sửa nếu hợp đồng rối. Không suy “nhiều repository OSS dùng” thành “tối ưu cho đội mình”.

Chuyển A → B theo một nghiệp vụ trước: giữ hành vi/test, di chuyển code, sửa import/namespace, wiring, discovery test và cấu hình ORM/serializer nếu chúng dựa đường dẫn/tên. B → C chủ yếu đổi vị trí các kiểu domain, nhưng phải rà tên public được serialize hay reflection tìm class. Đổi folder thường không cần migration database nếu schema không đổi; không hứa chi phí luôn thấp.

Tránh vừa đổi folder vừa đổi mô hình dữ liệu, tách service và sửa chính sách hủy trong một lượt. Với mỗi bước, hỏi quyết định nào được giấu sau biên và caller cần biết gì. Nếu không trả lời được, tên module/aggregate có thể chỉ thêm thao tác điều hướng.

Lỗi thường gặp và học tiếp

  • Root nằm trong folder aggregate nhưng controller sửa status trực tiếp: biên trên cây chưa thành biên hành vi.
  • Shared/ chứa mọi kiểu dùng hai lần: cùng tên chưa chắc cùng ý nghĩa, và shared có thể buộc các module đổi cùng nhau.
  • Mỗi bảng thành một aggregate: xác định bằng invariant và đơn vị thay đổi nhất quán, không chỉ hình dạng SQL.
  • Gom theo module rồi lấy module làm service luôn: quyền sở hữu code và quyền triển khai là hai quyết định riêng, cần xét giao tiếp/dữ liệu/vận hành.

Deadlock qua hai session minh họa transaction và thứ tự lấy khóa: cây aggregate không làm các vấn đề đó tự biến mất. Tiêu chí hoàn tất có thể kiểm chứng giúp biến nhận định kiến trúc thành điều có thể review.

Nguồn sơ cấp đọc ngày 2026-10-03:

  • Microsoft, Design a microservice domain model: entity, root và invariant. Bài áp dụng nguyên lý cho ví dụ một ứng dụng, không mặc định microservice.
  • Microsoft, Design a DDD-oriented microservice: layer logic và hướng phụ thuộc. Vị trí port trong bài là lựa chọn thiết kế của ví dụ, không tuyên bố mọi mẫu đặt port cùng nơi.