← Chương trước: Part 2: Thực Tế Chi Phí FinOps - Thuế Tàng Hình Của Microservices | Mục lục Series | Chương tiếp theo: Part 4: Đơn Giản Hóa CI/CD & Atomic Deployments →

Answer-first: Thiết lập ranh giới module bằng Domain-Driven Design (DDD) trong Modular Monolith ngăn chặn mã nguồn biến thành Big Ball of Mud. Áp dụng Bounded Contexts, public API nội bộ, in-memory event bus và các công cụ kiểm tra kiến trúc tự động như Packwerk và ArchUnit.


Part 3: Thiết Kế Ranh Giới (DDD) Trong Modular Monolith

Lý do lớn nhất khiến các đội ngũ kỹ sư sợ hãi kiến trúc Monolith là do trải nghiệm tồi tệ trong quá khứ với các “Spaghetti Monoliths” hoặc “Big Ball of Mud” — nơi mà code của chức năng Thanh toán gọi trực tiếp vào cơ sở dữ liệu của chức năng Giỏ hàng, tạo ra một mạng lưới phụ thuộc đan chéo không thể tháo gỡ.

Để tận dụng ưu điểm hiệu năng của Monolith mà vẫn đạt được tốc độ phát triển độc lập như Microservices, chúng ta phải xây dựng Modular Monolith. Chìa khóa của kiến trúc này là áp dụng nghiêm ngặt các nguyên tắc Domain-Driven Design (DDD) và thiết lập các “đường biên giới” cứng rắn ngay trong code.

Theo tiêu chuẩn kiến trúc nổi bật trong năm 2026, các tổ chức nhận ra rằng module hóa là vấn đề về Quyền sở hữu (Ownership) hành vi và dữ liệu, chứ không chỉ là cấu trúc thư mục vật lý. Một ranh giới module thực sự phải là ranh giới logic. Nếu bạn không thể thay đổi mã nguồn nội bộ hoặc schema cơ sở dữ liệu của một module mà không làm hỏng module khác, thì ranh giới của bạn chưa được xác định hiệu quả.

1. Nguyên Tắc Cốt Lõi: Bounded Contexts, Logical Boundaries và API Nội Bộ

Trong Microservices, nếu Dịch vụ A muốn lấy dữ liệu của Dịch vụ B, nó bắt buộc phải gọi qua HTTP API hoặc gRPC, nó không thể chọc thẳng vào Database của B. Đây là rào cản vật lý.

Trong Modular Monolith, do tất cả code đều nằm chung một memory space, bạn rất dễ vi phạm quy tắc này. Để ngăn chặn điều đó, chúng ta tạo ra các Bounded Contexts (Ngữ cảnh giới hạn) bằng quy ước kiến trúc:

  • Mỗi Domain/Module (như Billing, Inventory, User) được cô lập vào một folder/package riêng biệt.
  • Mỗi Module chỉ lộ ra một tập hợp các Interface hoặc Public Class làm API Nội Bộ (Internal API).
  • Quy tắc Vàng: Các Module khác tuyệt đối không được gọi vào các class mang tính implementation (private/internal) hoặc truy cập trực tiếp vào bảng cơ sở dữ liệu (Database Table) của Module khác. Chúng phải giao tiếp thông qua Internal API.

2. Ranh Giới Cơ Sở Dữ Liệu: Chống Lại Cross-Schema JOINs

Mức độ gắn kết nguy hiểm nhất trong Monolith không nằm ở code, mà nằm ở Database. Việc thực hiện truy vấn JOIN giữa bảng orders của module Order và bảng users của module Identity sẽ phá vỡ hoàn toàn khả năng tách biệt module.

Mô hình thiết kế chuẩn (Database-per-module pattern):

  • Vẫn dùng chung một máy chủ Database Server (để tiết kiệm chi phí phần cứng).
  • Phân tách dữ liệu thành các Schema riêng biệt (ví dụ PostgreSQL schemas: schema_orders, schema_identity).
  • Nếu module Order cần thông tin User, hệ thống sẽ thực thi một method call trong ứng dụng (ví dụ: UserService.getUserById(id)), lấy kết quả về RAM, và xử lý trong code (Application-level join) thay vì dùng SQL JOIN trực tiếp.
  • Nếu cần đồng bộ hóa dữ liệu quy mô lớn, hãy sử dụng Internal Event Bus (Event-driven architecture trong bộ nhớ) thay vì chia sẻ chung một transaction.

3. Thực Thi Ranh Giới Bằng Công Cụ Tự Động

Các quy ước trên giấy (Conventions) thường bị phá vỡ khi áp lực deadline tăng cao. Giải pháp của các công ty công nghệ hàng đầu là biến các quy ước này thành công cụ phân tích tĩnh (Static Analysis) chạy trực tiếp lúc biên dịch hoặc trong quá trình CI/CD.

A. Spring Modulith (Dành cho Java / Spring Boot)

Dự án Spring Modulith cung cấp công cụ tự động phát hiện và kiểm chứng cấu trúc package. Bằng cách tích hợp thư viện ArchUnit vào bộ Unit Test, Spring Modulith đảm bảo rằng:

  • Các Class nội bộ trong package của một Module không bị truy cập bởi Module khác.
  • Các sự kiện (Application Events) được phát hành và lắng nghe đúng chuẩn. Nếu một kỹ sư cố tình vi phạm ranh giới, Unit Test sẽ thất bại (Fail) ngay trên máy cá nhân, ngăn chặn dòng code rác bị merge vào nhánh chính.

B. Packwerk: Bài Học Từ Shopify và Gusto (Dành cho Ruby on Rails)

Cả Shopify và công ty phần mềm nhân sự Gusto đều vận hành trên kiến trúc Ruby on Rails Monolith khổng lồ. Để tránh hỗn loạn, họ áp dụng Packwerk (một thư viện open-source do Shopify phát triển):

  • Hệ thống code được chia thành các “Packs” (các domain ảo).
  • Bất cứ khi nào mã nguồn của Pack A gọi vào một class nội bộ (private method) hoặc truy vấn database trực tiếp của Pack B, Packwerk sẽ in ra cảnh báo lúc biên dịch (Compile-time warning).
  • Gusto chia sẻ rằng nhờ áp dụng Packwerk, họ đã loại bỏ được các lỗi phụ thuộc vòng (circular dependencies) và giảm 50% thời gian Onboarding cho kỹ sư mới, vì cấu trúc code trở nên rành mạch như hệ thống microservices.

4. Mô Hình “Citadel” Của DHH (Basecamp)

David Heinemeier Hansson (DHH) - cha đẻ của Ruby on Rails, đề xuất mô hình “Majestic Monolith & Citadel (Thành Trì)”. Theo đó, 99% logic nghiệp vụ sẽ nằm ở “Thành Trì” trung tâm (Monolith). Tuy nhiên, nếu có một chức năng đặc thù yêu cầu công nghệ khác biệt (như xử lý AI bằng Python, hay xử lý luồng WebSockets khổng lồ bằng Elixir), nó mới được tách ra thành các “Tiền Đồn” (Outposts) độc lập.

Điều này chứng minh rằng Modular Monolith không phải là tư duy bảo thủ “tất cả trong một”, mà là tư duy tối ưu hóa: Chỉ phân tán những gì thực sự cần phân tán. Đây chính là con đường tiến hóa (Evolutionary Path) của kiến trúc phần mềm năm 2026, giúp các đội ngũ tránh “thuế Microservices” (microservices tax) không cần thiết, trong khi vẫn duy trì ranh giới sạch để sẵn sàng bóc tách thành Microservices khi có nhu cầu mở rộng độc lập thực sự.

[!FAQ] Hỏi: Việc cấm SQL JOIN có làm giảm hiệu năng của Monolith không?
Đáp: Đối với các tác vụ hiển thị phức tạp (Dashboards), việc gọi nhiều Internal API thay vì 1 câu lệnh JOIN có thể tạo ra overhead nhỏ. Để xử lý, hệ thống Modular Monolith thường áp dụng mô hình CQRS (Command Query Responsibility Segregation) – tách riêng cơ sở dữ liệu để ghi (chứa ranh giới module nghiêm ngặt) và tạo các materialized views (bảng hiển thị tổng hợp) chuyên dụng cho việc đọc (được tự động cập nhật qua event).

Giữ vững biên giới code giúp bạn biến Monolith thành một bộ sưu tập các module độc lập. Nhưng làm thế nào để quy trình Build và Test một khối CodeBase khổng lồ không bị quá tải? Xem lời giải của Shopify ở Phần 4: Đơn Giản Hóa CI/CD.


🔗 Đọc thêm các chuyên đề liên quan:


← Chương trước: Part 2: Thực Tế Chi Phí FinOps - Thuế Tàng Hình Của Microservices | Mục lục Series | Chương tiếp theo: Part 4: Đơn Giản Hóa CI/CD & Atomic Deployments →