Phần III: Áp dụng Domain-Driven Design trong thực tế

Chương 13: Domain-Driven Design Trong Thực Tế

Chiến lược triển khai DDD trên các hệ thống legacy, dự án brownfield và phương pháp tiếp cận thực dụng khi môi trường làm việc không hoàn hảo.

Chúng ta đã cùng nhau tìm hiểu toàn bộ kho công cụ mà Domain-Driven Design mang lại: từ phân tích các miền nghiệp vụ, chia sẻ tri thức cho đến đưa ra các quyết định thiết kế chiến lược và chiến thuật. Hãy thử tưởng tượng việc áp dụng những tri thức này vào thực tiễn sẽ tuyệt vời đến mức nào.

Hãy hình dung một kịch bản lý tưởng: bạn đang bắt đầu một dự án hoàn toàn mới (greenfield project). Mọi đồng nghiệp của bạn đều am tường sâu sắc về Domain-Driven Design; ngay từ ngày đầu tiên, tất cả đều dốc lòng thiết kế các mô hình hữu hiệu và tận tụy trau chuốt Ngôn ngữ Chung (Ubiquitous Language). Khi dự án lớn dần, các ranh giới Bounded Context luôn rõ ràng, cô lập và bảo vệ trọn vẹn mô hình miền nghiệp vụ. Và cuối cùng, vì mọi quyết định thiết kế chiến thuật đều ăn khớp chuẩn xác với chiến lược kinh doanh, kho mã nguồn luôn ở trạng thái hoàn hảo: nói cùng ngôn ngữ với nghiệp vụ và triển khai đúng mẫu kiến trúc phù hợp với độ phức tạp của bài toán.

Bây giờ, hãy tỉnh giấc.

Cơ hội để bạn trải nghiệm được các "điều kiện phòng thí nghiệm" lý tưởng như vừa mô tả cũng hiếm hoi tựa như trúng số độc đắc. Dĩ nhiên điều đó vẫn có thể xảy ra, nhưng xác suất là cực kỳ thấp. Đáng buồn thay, nhiều người lầm tưởng rằng Domain-Driven Design chỉ có thể áp dụng cho các dự án greenfield và trong môi trường hoàn mỹ nơi mọi thành viên trong nhóm đều đạt đai đen DDD.

Trớ trêu thay, những dự án cần đến DDD nhất và thu được nhiều giá trị nhất từ nó lại chính là các dự án brownfield: những hệ thống đã chứng minh được tính khả thi về mặt thương mại và đang rất cần một cú hích tái thiết để chống chọi lại món nợ kỹ thuật (technical debt) cùng sự suy thoái thiết kế (design entropy) tích tụ qua nhiều năm tháng. Và trùng hợp thay, làm việc với các hệ thống brownfield, các mã nguồn legacy chắp vá theo kiểu "bãi bùn khổng lồ" (Big Ball of Mud) lại chính là nơi hầu hết các kỹ sư phần mềm chúng ta dành trọn phần lớn sự nghiệp của mình!

"Những hệ thống cần đến Domain-Driven Design nhất chính là các dự án brownfield — nơi giá trị kinh doanh đã được chứng minh và hệ thống đang quằn quại dưới gánh nặng của nợ kỹ thuật và sự hỗn loạn của mã nguồn legacy."

Một quan niệm sai lầm phổ biến khác về DDD là xem nó như một cuộc chơi "được ăn cả, ngã về không" (all-or-nothing): hoặc là bạn áp dụng triệt để mọi công cụ mà phương pháp luận này cung cấp, hoặc đó không phải là Domain-Driven Design. Điều đó hoàn toàn sai. Thật choáng ngợp nếu phải thấu suốt hết toàn bộ các khái niệm này cùng một lúc, chưa nói đến việc triển khai tất cả chúng vào thực tế. May mắn thay, bạn không cần phải áp dụng toàn bộ các mẫu hình và thực hành để gặt hái được giá trị từ Domain-Driven Design. Điều này đặc biệt đúng đối với các dự án brownfield, nơi việc áp đặt tất cả công cụ trong một khung thời gian ngắn là điều bất khả thi về mặt thực tiễn.

Trong chương này, bạn sẽ học các chiến lược áp dụng công cụ và mẫu hình DDD vào thế giới thực — bao gồm cả các dự án brownfield và những môi trường làm việc xa rời lý tưởng.

Phân Tích Chiến Lược (Strategic Analysis)

Theo đúng lộ trình khám phá các mẫu hình và thực hành DDD của cuốn sách này, điểm khởi đầu tốt nhất để đưa Domain-Driven Design vào một tổ chức là dành thời gian tìm hiểu chiến lược kinh doanh của công ty và hiện trạng kiến trúc của các hệ thống phần mềm sẵn có.

Hiểu Rõ Miền Nghiệp Vụ (Understand the Business Domain)

Trước tiên, hãy xác định miền nghiệp vụ của công ty bằng cách trả lời 4 câu hỏi cốt lõi:

  • Miền nghiệp vụ của tổ chức là gì? Công ty đang hoạt động trong lĩnh vực kinh doanh nào?
  • Khách hàng của công ty là ai? Đối tượng sử dụng sản phẩm hay dịch vụ là ai?
  • Công ty cung cấp dịch vụ hoặc giá trị gì cho khách hàng? Sản phẩm giải quyết nỗi đau nào của người dùng?
  • Công ty đang cạnh tranh với những đối thủ hay sản phẩm nào trên thị trường? Đâu là cuộc đua mà công ty đang tham gia?

Việc trả lời thấu đáo các câu hỏi trên sẽ cho bạn một cái nhìn toàn cảnh (bird's-eye view) về các mục tiêu cấp cao của tổ chức. Tiếp theo, hãy "thu phóng" (zoom in) vào sâu trong miền nghiệp vụ và tìm kiếm các khối xây dựng nghiệp vụ (business building blocks) mà tổ chức sử dụng để đạt được những mục tiêu cấp cao đó: đó chính là các miền con (subdomains).

Một kỹ thuật phỏng đoán ban đầu rất hữu hiệu (initial heuristic) là nhìn vào sơ đồ cơ cấu tổ chức của công ty: các phòng ban và đơn vị trực thuộc. Hãy xem xét cách các đơn vị này phối hợp với nhau để giúp công ty cạnh tranh trong miền kinh doanh của mình. Hơn thế nữa, hãy tìm kiếm những dấu hiệu đặc trưng của từng loại miền con:

1. Miền Con Cốt Lõi (Core Subdomains)

Để nhận diện các miền con cốt lõi của công ty, hãy tìm kiếm những yếu tố giúp công ty tạo ra sự khác biệt so với các đối thủ cạnh tranh:

  • Công ty có sở hữu "công thức bí mật" (secret sauce) nào mà đối thủ không có hay không? Ví dụ: tài sản sở hữu trí tuệ, bằng sáng chế hoặc các thuật toán chuyên biệt do nội bộ tự nghiên cứu và phát triển?
  • Hãy nhớ rằng lợi thế cạnh tranh, và do đó là miền con cốt lõi, không nhất thiết phải mang tính kỹ thuật. Liệu công ty có sở hữu một lợi thế cạnh tranh phi kỹ thuật nào không? Ví dụ: khả năng tuyển dụng nhân sự đỉnh cao, phong cách thiết kế nghệ thuật độc bản, hay mạng lưới đối tác độc quyền?

Một dấu hiệu phỏng đoán khác — dù hơi trớ trêu nhưng cực kỳ chuẩn xác — để nhận diện miền con cốt lõi chính là: hãy tìm những thành phần phần mềm có thiết kế tồi tệ nhất! Đó chính là những "bãi bùn khổng lồ" (big balls of mud) mà tất cả kỹ sư đều ngán ngẩm, nhưng ban lãnh đạo doanh nghiệp kiên quyết từ chối viết lại từ đầu vì rủi ro kinh doanh đi kèm quá lớn. Điểm mấu chốt ở đây là: hệ thống legacy đó không thể thay thế bằng một phần mềm mua sẵn (nếu mua được thì nó đã là Generic Subdomain), và bất kỳ sự can thiệp hay sửa đổi nào vào nó đều kéo theo những rủi ro trực tiếp đối với doanh thu và sự sống còn của doanh nghiệp.

2. Miền Con Chung (Generic Subdomains)

Để nhận diện các miền con chung, hãy tìm kiếm các giải pháp thương mại có sẵn trên thị trường (off-the-shelf), các dịch vụ thuê bao đám mây (SaaS), hoặc việc tích hợp các thư viện mã nguồn mở. Như bạn đã học ở Chương 1, các công ty đối thủ cạnh tranh cũng hoàn toàn có thể mua hoặc sử dụng cùng một giải pháp phần mềm đó mà không làm ảnh hưởng tiêu cực đến vị thế kinh doanh của công ty bạn (ví dụ: cổng thanh toán, hệ thống kế toán, dịch vụ xác thực danh tính).

3. Miền Con Hỗ Trợ (Supporting Subdomains)

Đối với các miền con hỗ trợ, hãy tìm những thành phần phần mềm còn lại: chúng không thể thay thế bằng các giải pháp đóng gói sẵn, nhưng bản thân chúng cũng không trực tiếp đem lại lợi thế cạnh tranh khác biệt. Nếu mã nguồn của miền con hỗ trợ có luộm thuộm hay thô sơ, nó cũng ít gây ra phản ứng tiêu cực hay căng thẳng cảm xúc từ phía các lập trình viên, đơn giản vì nó rất ít khi phải thay đổi. Do đó, hậu quả của một thiết kế phần mềm dưới mức tối ưu ở miền con hỗ trợ là không nghiêm trọng so với ở miền con cốt lõi.

Lời khuyên thực tiễn

Bạn không nhất thiết phải nhận diện toàn bộ các miền con trong một công ty. Điều đó là bất khả thi và không thực tế, ngay cả đối với một doanh nghiệp quy mô vừa. Thay vào đó, hãy phác thảo bức tranh tổng thể, nhưng tập trung sự chú ý cao độ vào các miền con có liên quan mật thiết nhất đến hệ thống phần mềm mà bạn và đội ngũ của bạn đang trực tiếp chịu trách nhiệm.

Khảo Sát Thiết Kế Hiện Tại (Explore the Current Design)

Khi bạn đã nắm rõ không gian bài toán (problem domain), bạn có thể tiếp tục điều tra không gian giải pháp và các quyết định thiết kế hiện có. Trước hết, hãy bắt đầu từ các thành phần cấp cao (high-level components). Chúng chưa chắc đã là các Bounded Context theo đúng nghĩa thuần túy của DDD, mà đơn thuần là các ranh giới hiện thời dùng để chia tách miền nghiệp vụ thành các hệ thống con.

Đặc tính quan trọng nhất cần tìm kiếm là vòng đời phát triển độc lập (decoupled lifecycles) của các thành phần. Ngay cả khi toàn bộ các hệ thống con được quản lý chung trong một kho mã nguồn duy nhất (mono-repo) hoặc toàn bộ mã nguồn nằm chung trong một cục monolithic to tướng, hãy kiểm tra xem những phần nào có thể được phát triển, kiểm thử và triển khai một cách độc lập tương đối so với phần còn lại.

Đánh Giá Thiết Kế Chiến Thuật (Evaluate Tactical Design)

Đối với từng thành phần cấp cao, hãy rà soát xem nó đang chứa đựng những miền con nghiệp vụ nào và những quyết định thiết kế kỹ thuật nào đã được đưa ra: mẫu hình nào đang được dùng để triển khai logic nghiệp vụ và kiến trúc tổng thể của thành phần đó ra sao?

  • Giải pháp kỹ thuật hiện tại có phù hợp với độ phức tạp của bài toán hay không?
  • Có khu vực nào đang đòi hỏi những mẫu thiết kế công phu hơn (như Domain Model, Event Sourcing) mà hiện tại lại đang dùng Transaction Script chắp vá?
  • Ngược lại, có miền con nào lẽ ra có thể cắt giảm chi phí bằng cách sử dụng các giải pháp đơn giản hoặc thư viện có sẵn hay không?

Hãy tận dụng những thông tin này để đưa ra các quyết định chiến lược và chiến thuật thông minh hơn.

Đánh Giá Thiết Kế Chiến Lược (Evaluate Strategic Design)

Hãy sử dụng tri thức về các thành phần cấp cao để vẽ nên Bản đồ Ngữ cảnh (Context Map) của thiết kế hiện tại, coi như các thành phần cấp cao này tạm thời đóng vai trò là các Bounded Context. Xác định và theo dõi mối quan hệ giữa các thành phần thông qua các mẫu tích hợp Bounded Context mà bạn đã học ở Chương 4.

Cuối cùng, phân tích Bản đồ Ngữ cảnh thu được và đánh giá kiến trúc dưới lăng kính của Domain-Driven Design. Liệu có những quyết định thiết kế chiến lược nào đang gây tổn hại cho hệ thống? Ví dụ:

  • Nhiều đội ngũ kỹ sư cùng đụng chạm và sửa đổi trên cùng một thành phần mã nguồn cấp cao.
  • Sự trùng lặp việc triển khai các miền con cốt lõi ở nhiều nơi khác nhau trong hệ thống.
  • Giao phó việc triển khai một miền con cốt lõi cho một công ty gia công phần mềm bên ngoài (outsourcing).
  • Sự ma sát và xung đột liên tục giữa các đội ngũ do các tích hợp thường xuyên bị lỗi hoặc phá vỡ hợp đồng giao tiếp.
  • Các mô hình vụng về, luộm thuộm từ các dịch vụ bên ngoài và hệ thống legacy đang lan truyền sang các thành phần hạ nguồn (downstream).

Những phát hiện này là điểm tựa tuyệt vời để lập kế hoạch hiện đại hóa hệ thống. Tuy nhiên, trước khi hành động, hãy chú ý đến một hiểm họa lớn: tri thức miền bị thất lạc (lost domain knowledge). Như đã thảo luận ở Chương 11, tri thức về miền nghiệp vụ rất dễ bị phai nhạt hoặc mất mát vì nhiều lý do (nhân sự cũ nghỉ việc, tài liệu không tồn tại, mã nguồn bị sửa đổi qua nhiều thế hệ lập trình viên). Vấn đề này đặc biệt nhức nhối ở các miền con cốt lõi, nơi logic vừa phức tạp vừa sống còn đối với công ty. Nếu gặp phải tình huống này, hãy chủ động tổ chức các buổi EventStorming (như đã học ở Chương 12) để khôi phục lại tri thức và đặt nền móng cho việc xây dựng một Ngôn ngữ Chung vững chắc.

Chiến Lược Hiện Đại Hóa (Modernization Strategy)

Những nỗ lực "viết lại toàn bộ từ đầu" (big rewrite) — nơi các kỹ sư mơ ước đập đi xây mới toàn bộ hệ thống để làm lại mọi thứ cho thật chuẩn chỉ — hầu như rất hiếm khi thành công. Và càng hiếm hơn nữa khi ban lãnh đạo chịu phê duyệt một cuộc đại tu kiến trúc mạo hiểm như vậy.

Một cách tiếp cận an toàn hơn nhiều để cải thiện thiết kế của các hệ thống sẵn có là: suy nghĩ lớn, nhưng bắt đầu từ những bước nhỏ (think big, start small). Như chuyên gia Eric Evans từng nhấn mạnh: "Không phải toàn bộ một hệ thống lớn đều sẽ được thiết kế hoàn hảo." Đó là sự thật hiển nhiên mà chúng ta phải chấp nhận, và do đó chúng ta phải quyết định một cách có chiến lược xem nên đầu tư nỗ lực hiện đại hóa vào đâu để đạt hiệu quả cao nhất.

Tổ Chức Lại Module Theo Ranh Giới Miền Con (Reorganizing Modules)

Điều kiện tiên quyết để đưa ra quyết định tái cấu trúc là phải có những ranh giới phân định rõ các miền con của hệ thống. Các ranh giới này ban đầu chưa nhất thiết phải là các ranh giới vật lý (biến mỗi miền con thành một Bounded Context độc lập triển khai riêng). Thay vào đó, hãy bắt đầu bằng việc đảm bảo ít nhất các ranh giới logic (như namespaces, modules, hay packages tùy theo nền tảng công nghệ) được căn chỉnh ăn khớp với các ranh giới miền con, như được minh họa trong Hình 13-1.

Hình 13-1: Tổ chức lại các module của Bounded Context để phản ánh ranh giới các miền con nghiệp vụ
Hình 13-1: Tổ chức lại các module của Bounded Context để phản ánh ranh giới các miền con nghiệp vụ thay vì các mẫu hình triển khai kỹ thuật.

Việc điều chỉnh các module của hệ thống là một hình thức tái cấu trúc (refactoring) tương đối an toàn. Bạn không hề thay đổi logic nghiệp vụ, mà chỉ đơn thuần sắp xếp lại vị trí các lớp và kiểu dữ liệu vào một cấu trúc ngăn nắp, có tổ chức hơn. Dù vậy, hãy cẩn trọng rà soát để đảm bảo các tham chiếu bằng tên kiểu dữ liệu đầy đủ (chẳng hạn như việc nạp thư viện động qua reflection) không bị phá vỡ.

Ngoài ra, hãy theo dõi logic nghiệp vụ của các miền con được triển khai phân tán trên các nền tảng khác nhau: chẳng hạn như các stored procedures trong cơ sở dữ liệu, các hàm không máy chủ (serverless functions), v.v. Hãy đảm bảo đưa các ranh giới mới vào cả các nền tảng đó. Ví dụ: nếu một phần logic nằm trong stored procedure của database, hãy đổi tên procedure để phản ánh module mà nó thuộc về, hoặc tạo một database schema riêng biệt và chuyển stored procedure vào đó.

Hiện Đại Hóa Chiến Lược: Tách Ranh Giới Vật Lý

Như chúng ta đã bàn luận ở Chương 10, việc vội vã phân rã hệ thống thành các Bounded Context nhỏ nhất có thể khi chưa hiểu rõ bài toán là một hành động cực kỳ mạo hiểm. Chúng ta sẽ đào sâu hơn về mối quan hệ giữa Bounded Context và Microservices trong Chương 14. Còn hiện tại, hãy tìm xem nơi nào việc biến đổi ranh giới logic thành ranh giới vật lý sẽ mang lại nhiều giá trị kinh doanh nhất.

Quá trình trích xuất một hoặc nhiều Bounded Context bằng cách chuyển ranh giới logic thành ranh giới vật lý được minh họa trong Hình 13-2.

Hình 13-2: Tách một Bounded Context bằng cách chuyển ranh giới logic thành ranh giới vật lý
Hình 13-2: Tách một Bounded Context bằng cách chuyển đổi một ranh giới logic thành một ranh giới vật lý độc lập.

Hai câu hỏi vàng bạn cần tự đặt ra cho mình:

  • Có nhiều đội ngũ đang cùng làm việc trên cùng một kho mã nguồn không? Nếu có, hãy tách rời vòng đời phát triển của họ bằng cách định nghĩa các Bounded Context riêng biệt cho từng đội.
  • Các thành phần khác nhau có đang sử dụng những mô hình mâu thuẫn nhau (conflicting models) không? Nếu có, hãy di dời các mô hình mâu thuẫn đó vào các Bounded Context riêng biệt để giải phóng gánh nặng nhận thức cho lập trình viên.

Áp Dụng Các Mẫu Tích Hợp Bounded Context Trong Thực Tế

Khi những Bounded Context tối thiểu cần thiết đã được định hình, hãy xem xét mối quan hệ và các mẫu tích hợp giữa chúng. Hãy chú ý đến những vấn đề nhức nhối mà các mẫu tích hợp ngữ cảnh có thể giải quyết dứt điểm:

Mẫu Tích Hợp Tình Huống Trong Hệ Thống Thực Tế Giải Pháp Xử Lý Theo DDD
Customer–Supplier (Khách hàng – Nhà cung cấp) Sự mở rộng tổ chức làm mất đi quan hệ đối tác (Partnership) trước đây giữa các đội. Một đội phụ thuộc vào đội kia nhưng không còn tiếng nói bình đẳng. Tái cấu trúc sang một biến thể phù hợp của Customer-Supplier: Conformist (Chấp nhận mô hình thượng nguồn nếu ổn định), Anticorruption Layer (Bảo vệ phía hạ nguồn), hoặc Open-Host Service (Bảo vệ phía thượng nguồn).
Anticorruption Layer (ACL - Lớp chống suy thoái) Hệ thống mới cần tích hợp với hệ thống legacy có mô hình luộm thuộm, hoặc API của bên thứ ba thường xuyên thay đổi làm vỡ mã nguồn hạ nguồn. Xây dựng một lớp ACL đóng vai trò thông dịch viên: chuyển đổi dữ liệu và mô hình thô từ bên ngoài thành các khái niệm tinh khiết, sạch sẽ của Bounded Context nội bộ.
Open-Host Service (OHS - Dịch vụ máy chủ mở) Mỗi khi hệ thống thượng nguồn sửa đổi logic nội bộ, các thay đổi lập tức làm gãy vỡ hàng loạt các hệ thống tiêu thụ phía hạ nguồn. Tách biệt hoàn toàn mô hình triển khai nội bộ khỏi API công khai (Public API) được công bố cho các bên tiêu thụ, sử dụng một Published Language chuẩn tắc.
Separate Ways (Đường ai nấy đi) Xung đột và ma sát kéo dài giữa các đội kỹ sư khi phải cố gắng chia sẻ một chức năng chung mà chức năng đó không phải là Core Subdomain. Cho phép các đội tự do xây dựng giải pháp riêng của mình, triệt tiêu hoàn toàn sự phụ thuộc lẫn nhau để tăng tốc độ phát triển.

Hiện Đại Hóa Chiến Thuật (Tactical Modernization)

Trước hết và quan trọng nhất, từ góc độ chiến thuật, hãy tìm kiếm những sự lệch pha đau đớn nhất giữa giá trị nghiệp vụ và chiến lược triển khai: chẳng hạn như một miền con cốt lõi nhưng lại đang được hiện thực hóa bằng những mẫu hình không tương xứng với độ phức tạp — như Transaction Script hoặc Active Record. Những thành phần hệ thống này tác động trực tiếp đến sự thành bại của doanh nghiệp và buộc phải thay đổi liên tục, nhưng lại cực kỳ đau khổ để bảo trì và nâng cấp do thiết kế yếu kém.

Nuôi Dưỡng Ngôn Ngữ Chung (Cultivate a Ubiquitous Language)

Điều kiện tiên quyết cho việc hiện đại hóa thiết kế thành công là phải sở hữu tri thức miền sâu sắc và một mô hình hữu hiệu về miền nghiệp vụ. Ngôn ngữ Chung của Domain-Driven Design là chìa khóa then chốt để đạt được điều này.

Đừng quên "đường tắt" tuyệt vời của DDD để thu thập tri thức miền: EventStorming. Hãy sử dụng EventStorming để cùng với các chuyên gia nghiệp vụ xây dựng một Ngôn ngữ Chung và mổ xẻ mã nguồn legacy — đặc biệt là khi kho mã nguồn đó là một mớ bòng bong không tài liệu mà không một ai thực sự thấu hiểu từ đầu đến cuối. Hãy tập hợp tất cả mọi người có liên quan đến chức năng của nó lại và cùng nhau khám phá. EventStorming là một công cụ kỳ diệu để khôi phục tri thức miền đã thất lạc!

Một khi bạn đã được trang bị đầy đủ tri thức và mô hình, hãy quyết định xem mẫu hình triển khai logic nghiệp vụ nào phù hợp nhất với chức năng đó (dựa vào các quy tắc thực nghiệm ở Chương 10). Quyết định tiếp theo bạn cần đưa ra là lựa chọn chiến lược hiện đại hóa: thay thế dần dần toàn bộ các thành phần của hệ thống (mẫu Strangler Fig), hay tái cấu trúc từng bước trên giải pháp hiện có.

Mẫu Cây Sung Bóp Cổ (The Strangler Fig Pattern)

Cây sung bóp cổ (Strangler Fig), như được chụp trong Hình 13-3, là một họ thực vật nhiệt đới có kiểu phát triển vô cùng kỳ lạ: chúng sống bám và phát triển bao trùm lên các cây khác — gọi là cây chủ. Một cây sung bóp cổ bắt đầu cuộc đời mình từ một hạt giống rơi trên các cành cao của cây chủ. Khi lớn dần, nó đâm rễ dài xuống phía dưới cho đến khi cắm chặt vào lòng đất. Theo thời gian, tán lá rậm rạp của cây sung bóp cổ vươn lên che khuất toàn bộ ánh sáng mặt trời của cây chủ, dẫn đến cái chết tất yếu của cây chủ.

Hình 13-3: Cây sung bóp cổ mọc bao trùm lên thân cây chủ
Hình 13-3: Một cây sung bóp cổ mọc bao trùm lên thân cây chủ trong tự nhiên (nguồn ảnh: Unsplash / y_l5tep9wxI).

Mẫu di chuyển hệ thống Strangler dựa trên chính động lực phát triển của loài cây này. Ý tưởng cốt lõi là: tạo ra một Bounded Context mới tinh (cây sung bóp cổ), sử dụng nó để triển khai các yêu cầu nghiệp vụ mới, và từng bước di chuyển dần các chức năng từ Bounded Context legacy sang cho nó. Đồng thời, ngoại trừ việc sửa các lỗi khẩn cấp (hotfix), mọi sự phát triển và mở rộng trên Bounded Context cũ đều dừng lại. Theo thời gian, toàn bộ chức năng được di chuyển sang Bounded Context mới, và theo đúng phép loại suy tự nhiên, dẫn đến "cái chết" nhẹ nhàng và êm thấm của cây chủ — mã nguồn legacy cũ kỹ.

Thông thường, mẫu Strangler được áp dụng song hành với mẫu Façade: một lớp trừu tượng mỏng đóng vai trò là giao diện công khai tiếp nhận các yêu cầu từ bên ngoài, chịu trách nhiệm định tuyến các yêu cầu đó tới hệ thống legacy hoặc hệ thống hiện đại hóa tùy theo tiến độ di chuyển chức năng. Khi quá trình di chuyển hoàn tất — tức khi cây chủ đã chết — lớp Façade sẽ được gỡ bỏ vì nó không còn cần thiết nữa (xem Hình 13-4).

Hình 13-4: Lớp Façade chuyển tiếp yêu cầu dựa trên trạng thái di chuyển chức năng
Hình 13-4: Lớp Façade định tuyến yêu cầu dựa trên trạng thái di chuyển chức năng từ hệ thống legacy sang hệ thống hiện đại hóa; khi quá trình di chuyển hoàn tất, cả Façade và hệ thống cũ đều được gỡ bỏ.

Ngoại Lệ Cho Phép Chia Sẻ Cơ Sở Dữ Liệu Tạm Thời

Trái ngược với nguyên tắc vàng của DDD rằng mỗi Bounded Context là một hệ thống con độc lập và không bao giờ được chia sẻ cơ sở dữ liệu với các Bounded Context khác, quy tắc này có thể được nới lỏng tạm thời khi triển khai mẫu Strangler. Cả Bounded Context cũ và mới có thể tạm thời làm việc chung trên cùng một cơ sở dữ liệu để tránh sự phức tạp khủng khiếp của việc tích hợp phân tán — vốn thường đòi hỏi các giao dịch phân tán (distributed transactions) hai pha phức tạp — như minh họa trong Hình 13-5.

Hình 13-5: Cả hệ thống cũ và hệ thống hiện đại hóa tạm thời làm việc chung trên cùng một cơ sở dữ liệu
Hình 13-5: Cả hệ thống cũ và hệ thống hiện đại hóa tạm thời làm việc chung trên cùng một cơ sở dữ liệu.
Điều kiện ràng buộc nghiêm ngặt

Điều kiện tiên quyết để được phép phá vỡ quy tắc "mỗi Bounded Context sở hữu một cơ sở dữ liệu riêng" là: sự chia sẻ này chỉ mang tính tạm thời, và hệ thống legacy nhất định phải sớm được khai tử hoàn toàn để cơ sở dữ liệu trở thành tài sản độc quyền duy nhất của Bounded Context mới.

Tái Cấu Trúc Các Quyết Định Thiết Kế Chiến Thuật Tại Chỗ

Một giải pháp thay thế cho việc di chuyển bằng Strangler là hiện đại hóa mã nguồn legacy trực tiếp tại chỗ (refactoring in place). Trong Chương 11, bạn đã tìm hiểu các khía cạnh tiến hóa quyết định thiết kế. Tuy nhiên, có hai sắc thái cực kỳ quan trọng bạn cần ghi nhớ khi hiện đại hóa một mã nguồn legacy:

Thứ nhất: Các bước nhỏ gia tăng luôn an toàn hơn việc viết lại quy mô lớn. Do đó, đừng bao giờ tái cấu trúc một mã nguồn Transaction Script hoặc Active Record nhảy vọt thẳng sang Event-Sourced Domain Model ngay lập tức! Thay vào đó, hãy thực hiện bước trung gian: thiết kế các Aggregate dựa trên trạng thái (State-based Aggregates) trước.

Hãy đầu tư công sức vào việc tìm kiếm các ranh giới Aggregate hữu hiệu. Đảm bảo rằng tất cả logic nghiệp vụ liên quan đều nằm gọn bên trong các ranh giới đó. Việc chuyển từ Aggregate dựa trên trạng thái sang Aggregate Event-Sourced an toàn hơn gấp bội so với việc bạn phát hiện ra mình đã xác định sai ranh giới giao dịch trong một mô hình Event Sourcing!

Thứ hai: Hãy giải quyết độ phức tạp theo từng tầng. Việc cô lập các trường dữ liệu và bảo đảm tính bất biến (invariants) trong mô hình hướng đối tượng truyền thống sẽ giúp nhóm của bạn tích lũy đủ tri thức sâu sắc về nghiệp vụ trước khi quyết định có cần đưa thêm chiều thời gian vào hay không.

Domain-Driven Design Thực Dụng (Pragmatic Domain-Driven Design)

Một câu hỏi kinh điển mà các lập trình viên thường đặt ra là: "Làm thế nào để tôi 'bán' Domain-Driven Design cho sếp và ban lãnh đạo của mình?"

Câu trả lời thẳng thắn và thực tế nhất là: Đừng bán!

Từ góc nhìn của ban lãnh đạo doanh nghiệp, tại sao họ phải quan tâm đến việc bạn sử dụng Transaction Script, Active Record, hay Domain Model? Miễn là phần mềm giải quyết được nhu cầu kinh doanh, tạo ra doanh thu và vận hành ổn định, thì việc đội ngũ kỹ thuật sử dụng mẫu hình nào đối với họ cũng giống như việc một bác sĩ phẫu thuật dùng nhãn hiệu dao mổ nào vậy. Họ thuê bạn với tư cách là chuyên gia kỹ thuật để đưa ra những quyết định công nghệ đúng đắn nhất, chứ không phải để bắt họ phải đi học lý thuyết DDD!

Khi bạn cố gắng "rao bán" DDD như một sáng kiến chiến lược cấp công ty, bạn thường sẽ vấp phải sự kháng cự. Ban lãnh đạo sẽ thấy một sự thay đổi quy trình đồ sộ, tốn kém chi phí đào tạo và tiềm ẩn rủi ro làm chậm tiến độ giao hàng ngắn hạn. May mắn thay, bạn hoàn toàn không cần sự cho phép của ai để bắt đầu làm phần mềm một cách chuyên nghiệp.

Domain-Driven Design "Ngầm" (Undercover Domain-Driven Design)

Hãy biến Domain-Driven Design thành một công cụ trong hộp đồ nghề cá nhân của bạn, chứ không phải một khẩu hiệu chiến lược của tổ chức. Các mẫu hình và thực hành của DDD là những kỹ thuật kỹ thuật phần mềm (software engineering techniques). Và bởi vì kỹ thuật phần mềm là công việc hàng ngày của bạn, hãy sử dụng chúng!

Dưới đây là cách bạn có thể áp dụng DDD vào công việc thường nhật một cách tự nhiên mà không cần gióng trống khua chiêng:

1. Nuôi Dưỡng Ngôn Ngữ Chung Tự Nhiên

Ngôn ngữ Chung là thực hành nền tảng quan trọng nhất của DDD. Thật may, thực hành này đơn giản và gần gũi đến mức nó tiệm cận với tư duy lẽ thường (common sense):

  • Lắng nghe chăm chú: Hãy để ý kỹ ngôn từ mà các bên liên quan sử dụng khi họ nói về miền nghiệp vụ. Khéo léo hướng thuật ngữ tránh xa các biệt ngữ kỹ thuật và hướng tới ý nghĩa nghiệp vụ thực tế.
  • Tìm kiếm sự mâu thuẫn: Nếu có nhiều cái tên khác nhau cho cùng một khái niệm, hãy tìm hiểu lý do. Phải chăng các mô hình khác nhau đang bị trộn lẫn vào nhau? Hãy làm rõ các ngữ cảnh. Nếu ý nghĩa hoàn toàn giống nhau, hãy kêu gọi sự thống nhất dùng một từ duy nhất.
  • Giao tiếp không hình thức: Không nhất thiết phải mở các cuộc họp trang trọng. Những cuộc trò chuyện bên máy pha cà phê hay bữa ăn trưa là cơ hội vàng. Hãy nói chuyện với chuyên gia nghiệp vụ, thử dùng ngôn ngữ của họ. Các chuyên gia nghiệp vụ hầu như luôn luôn hào hứng khi thấy một kỹ sư phần mềm thực sự quan tâm tìm hiểu bài toán kinh doanh của họ!
  • Đưa vào mã nguồn: Quan trọng nhất, hãy dùng Ngôn ngữ Chung trong mã nguồn, trong tên biến, tên hàm, tên lớp và mọi trao đổi dự án. Hãy kiên nhẫn; việc thay đổi thói quen ngôn từ cần thời gian, nhưng dần dần nó sẽ lan tỏa khắp tổ chức.

Bounded Context & Sự Thuyết Phục Bằng Logic

Khi thảo luận về các phương án phân rã hệ thống với các đồng nghiệp khác, đừng viện dẫn giáo điều: "Chúng ta phải làm thế này vì cuốn sách DDD bảo thế!" Thay vào đó, hãy giải thích bằng chính các nguyên lý logic đằng sau mẫu hình Bounded Context:

  • Tại sao nên thiết kế các mô hình hướng bài toán thay vì một mô hình chung cho mọi use case? → Bởi vì một giải pháp "tất cả trong một" (all-in-one) hiếm khi làm tốt được bất kỳ việc gì.
  • Tại sao một Bounded Context không nên chứa đựng các mô hình xung đột nhau? → Bởi vì nó làm tăng vọt tải trọng nhận thức (cognitive load) và khiến giải pháp trở nên phức tạp một cách không cần thiết.
  • Tại sao nhiều đội kỹ sư cùng làm việc trên một kho mã nguồn lại là ý tồi? → Bởi vì nó tạo ra ma sát, tranh chấp mã nguồn và cản trở sự tự chủ của các đội ngũ.

Lập Luận Cho Các Quyết Định Thiết Kế Chiến Thuật

Tương tự đối với các mẫu hình chiến thuật, hãy luôn dựa trên tư duy logic thực tiễn:

  • Tại sao các ranh giới giao dịch rõ ràng lại quan trọng? → Để bảo vệ tính nhất quán toàn vẹn của dữ liệu kinh doanh.
  • Tại sao một giao dịch cơ sở dữ liệu không nên sửa đổi nhiều hơn một thực thể Aggregate? → Để đảm bảo rằng các ranh giới nhất quán được xác định chính xác và tránh xung đột khóa bản ghi.
  • Tại sao trạng thái của Aggregate không được sửa đổi trực tiếp từ bên ngoài? → Để đảm bảo toàn bộ logic nghiệp vụ được đặt chung một chỗ (co-located) và không bị phân mảnh hay trùng lặp.
  • Tại sao không nên đẩy một phần logic của Aggregate vào Stored Procedure? → Để đảm bảo không có logic nào bị nhân bản. Logic bị trùng lặp ở những thành phần xa cách nhau sẽ chắc chắn bị lệch pha theo thời gian và dẫn đến hỏng hóc dữ liệu.
  • Tại sao nên giữ ranh giới Aggregate càng nhỏ càng tốt? → Bởi vì phạm vi giao dịch quá rộng sẽ vừa làm tăng độ phức tạp của Aggregate, vừa tàn phá nghiêm trọng hiệu năng hệ thống.
  • Tại sao không thể ghi các sự kiện ra file log thay vì dùng Event Sourcing? → Bởi vì file log văn bản không đem lại bất kỳ sự đảm bảo nào về tính nhất quán dữ liệu dài hạn.

Thủ Thuật "Jedi Mind Trick" Để Thuyết Phục Dùng Event Sourcing

Bất chấp những ưu thế vượt trội, Event Sourcing nghe có vẻ quá "cực đoan" và xa lạ đối với nhiều người. Trong trường hợp giải pháp thực sự cần đến một Event-Sourced Domain Model nhưng bạn gặp khó khăn trong việc thuyết phục đội ngũ, hãy áp dụng giải pháp căn bản nhất: để cho miền nghiệp vụ dẫn dắt quyết định này!

Hãy nói chuyện trực tiếp với các chuyên gia nghiệp vụ. Hãy cho họ xem sự so sánh trực quan giữa mô hình dựa trên trạng thái (chỉ thấy giá trị hiện tại) và mô hình dựa trên sự kiện (nhìn thấy toàn bộ lịch sử biến động qua chiều thời gian). Hãy giải thích sự khác biệt và những ưu thế mà Event Sourcing mang lại trong việc truy vết, kiểm toán, phân tích xu hướng và tái hiện quá khứ.

Thực tế cho thấy, phần lớn các chuyên gia nghiệp vụ sẽ vô cùng phấn khích trước mức độ sâu sắc của tri thức mà Event Sourcing đem lại, và chính họ sẽ trở thành những người nhiệt thành nhất ủng hộ và yêu cầu triển khai kiến trúc Event Sourcing!

Tổng Kết Chương (Conclusion)

Trong chương này, bạn đã học được các kỹ thuật đa dạng để khai thác sức mạnh của Domain-Driven Design trong các kịch bản đời thực: khi làm việc trên các dự án brownfield, các mã nguồn legacy, và không nhất thiết phải có một đội ngũ toàn chuyên gia DDD.

  • Luôn bắt đầu bằng phân tích miền nghiệp vụ: Mục tiêu của công ty là gì và chiến lược để đạt được chúng ra sao? Sử dụng cơ cấu tổ chức và các quyết định thiết kế phần mềm hiện tại để nhận diện các miền con và phân loại chúng (Core, Generic, Supporting).
  • Lập kế hoạch hiện đại hóa thông minh: Tìm kiếm những điểm nghẽn đau đớn nhất. Tập trung vào nơi đem lại nhiều giá trị kinh doanh nhất. Hiện đại hóa mã nguồn legacy bằng cách tái cấu trúc (refactoring) hoặc thay thế dần dần (Strangler Fig pattern). Dù theo cách nào, hãy làm từng bước nhỏ gia tăng — những cuộc đại tu viết lại toàn bộ luôn ẩn chứa rủi ro khổng lồ!
  • Áp dụng DDD một cách thực dụng: Bạn hoàn toàn có thể sử dụng các công cụ của DDD ngay cả khi phương pháp này chưa được công ty chính thức áp dụng. Hãy biến nó thành vũ khí chuyên môn thầm lặng, và khi trao đổi với đồng nghiệp, hãy luôn dùng tư duy logic và các nguyên lý kỹ thuật vững chắc đằng sau mỗi mẫu hình thay vì viện dẫn sách vở giáo điều.

Chương này khép lại phần thảo luận độc lập về Domain-Driven Design. Trong Phần IV sắp tới, bạn sẽ khám phá sự giao thoa kỳ thú giữa DDD và các phương pháp luận, mẫu kiến trúc hiện đại khác như Microservices, Event-Driven Architecture và Data Mesh.

Bài Tập Ôn Tập & Thảo Luận Thực Tiễn (Exercises)

Dưới đây là các câu hỏi trắc nghiệm và câu hỏi tình huống thực chiến giúp bạn củng cố kiến thức, đi kèm đáp án và lời giải thích chính thức từ tác giả Vlad Khononov (Appendix B).

Câu 1: Giả sử bạn muốn áp dụng các công cụ và thực hành Domain-Driven Design vào một dự án brownfield (mã nguồn legacy sẵn có). Bước đầu tiên của bạn sẽ là gì?
A. Tái cấu trúc toàn bộ logic nghiệp vụ sang Domain Model hướng sự kiện (Event-Sourced Domain Model).
B. Phân tích miền nghiệp vụ của tổ chức và chiến lược kinh doanh của nó.
C. Cải thiện các thành phần của hệ thống bằng cách đảm bảo chúng tuân theo các nguyên tắc của các Bounded Context chuẩn mực.
D. Không thể áp dụng Domain-Driven Design trong một dự án brownfield.
Đáp án chính xác: B — Phân tích miền nghiệp vụ của tổ chức và chiến lược kinh doanh của nó.
Giải thích chi tiết từ Tác giả (Appendix B): Cho dù là dự án greenfield hay brownfield, bước đầu tiên luôn phải là phân tích chiến lược kinh doanh. Bạn phải hiểu công ty kiếm tiền bằng cách nào, đâu là lợi thế cạnh tranh cốt lõi (Core Subdomain) và đâu là miền hỗ trợ/chung. Nhờ đó, bạn mới biết chính xác khu vực nào cần đầu tư công sức tái cấu trúc để đem lại giá trị kinh tế lớn nhất, tránh lãng phí thời gian và ngân sách vào việc tối ưu hóa những thành phần phụ trợ không đem lại lợi thế kinh doanh.
Câu 2: Trong quá trình di chuyển hệ thống, mẫu Strangler (Cây Sung Bóp Cổ) mâu thuẫn tạm thời với những nguyên tắc cốt lõi nào của Domain-Driven Design?
A. Nhiều Bounded Context cùng chia sẻ chung một cơ sở dữ liệu (Shared Database).
B. Nếu Bounded Context đang được hiện đại hóa là một Core Subdomain, việc triển khai nó sẽ bị trùng lặp ở cả hệ thống cũ và hệ thống mới.
C. Nhiều đội ngũ cùng làm việc trên một Bounded Context duy nhất.
D. Cả A và B đều đúng.
Đáp án chính xác: D — Cả A và B đều đúng.
Giải thích chi tiết từ Tác giả (Appendix B): Khi áp dụng mẫu Strangler, để tránh sự phức tạp quá tải của các giao dịch phân tán, hệ thống cũ và Bounded Context mới tạm thời phải đọc/ghi vào cùng một database (vi phạm nguyên tắc Bounded Context độc lập sở hữu cơ sở dữ liệu riêng). Đồng thời, logic nghiệp vụ của Core Subdomain sẽ tạm thời bị phân mảnh và nhân bản ở cả hai hệ thống trong giai đoạn di chuyển. Đây là sự đánh đổi có tính toán, với điều kiện hệ thống cũ sẽ sớm được khai tử để trả lại tính độc lập cho Bounded Context mới.
Câu 3: Tại sao nhìn chung KHÔNG NÊN tái cấu trúc logic nghiệp vụ đang dùng Active Record thẳng sang Event-Sourced Domain Model trong một bước duy nhất?
A. Một mô hình dựa trên trạng thái (State-based model) giúp việc điều chỉnh ranh giới các Aggregate dễ dàng và linh hoạt hơn nhiều trong quá trình nhóm đang học hỏi tri thức miền.
B. Áp dụng những thay đổi lớn theo từng bước nhỏ gia tăng (incremental steps) luôn an toàn hơn nhiều so với việc nhảy cóc.
C. Cả A và B đều đúng.
D. Không có phương án nào đúng. Hoàn toàn hợp lý khi tái cấu trúc Transaction Script thẳng sang Event-Sourced Domain Model ngay lập tức.
Đáp án chính xác: C — Cả A và B đều đúng.
Giải thích chi tiết từ Tác giả (Appendix B): Khi bắt tay vào làm việc với một mã nguồn legacy, ban đầu bạn thường chưa thể nắm chắc ranh giới giao dịch chính xác. Nếu sử dụng State-based Aggregate, việc gộp hay tách thực thể chỉ đơn giản là sửa đổi các class trong mã nguồn. Ngược lại, nếu bạn đã lưu trữ dữ liệu dạng luồng sự kiện (Event Store) với ranh giới sai, việc di chuyển và tái cấu trúc các luồng sự kiện cũ trong cơ sở dữ liệu sau này sẽ vô cùng đau đớn và phức tạp. Lộ trình an toàn nhất luôn là: Active Record → State-based Aggregate (ổn định ranh giới) → Event-Sourced Domain Model.
Câu 4 (Tình huống thực tế): Khi bạn giới thiệu mẫu Aggregate, đội ngũ kỹ thuật của bạn thắc mắc: "Tại sao Aggregate không thể tham chiếu đến TẤT CẢ các thực thể có thể có, để từ một nơi chúng ta có thể duyệt qua toàn bộ miền nghiệp vụ cho tiện?" Bạn sẽ giải thích cho họ như thế nào?
Lời giải đáp từ Tác giả Vlad Khononov (Appendix B):

"Một Aggregate có ranh giới bao trùm toàn bộ một Bounded Context sẽ biến tất cả dữ liệu của Bounded Context đó thành một phần của một giao dịch khổng lồ duy nhất (one big transaction).

Gần như chắc chắn các vấn đề nghiêm trọng về hiệu năng hệ thống (performance degradation) và xung đột tranh chấp khóa bản ghi (concurrency lock contention) sẽ bộc lộ ngay từ ngày đầu tiên khi nhiều người dùng cùng thao tác đồng thời. Khi điều đó xảy ra, đội ngũ sẽ không còn cách nào khác ngoài việc buộc phải gỡ bỏ ranh giới giao dịch để hệ thống có thể chạy được.

Và hậu quả tất yếu là: bạn sẽ không còn bất kỳ cơ chế nào để giả định hoặc đảm bảo rằng thông tin nằm trong Aggregate đạt được tính nhất quán mạnh (strongly consistent) nữa.

Đó chính là lý do chúng ta phải luôn nỗ lực giữ cho ranh giới Aggregate càng nhỏ càng tốt: chỉ bao bọc những dữ liệu bắt buộc phải đảm bảo quy tắc bất biến (invariants) một cách nhất quán tuyệt đối trong cùng một giao dịch đơn lẻ."