Diagram illustrating chaotic microservices connections, clean domain boundaries grouping services, and deployment pipeline steps

Quy Trình Phân Tích Thiết Kế Hệ Thống Phần Mềm: Tại Sao Service Boundary Quyết Định Tất Cả

Năm 2019, một startup fintech vừa migrate xong từ monolith sang microservices — 47 services, Kubernetes, CI/CD đầy đủ. Sáu tháng sau, họ mất 3 ngày chỉ để deploy một tính năng thêm trường email phụ cho user.

Không phải vì họ dùng công nghệ sai. Mà vì họ đã chia service theo cách sai ngay từ đầu.

Cái Bẫy Mà 80% Team Dính Phải

Khi thiết kế hệ thống mới, hầu hết developer đều làm theo bản năng: vẽ boxes và arrows. Box UserService, box OrderService, box PaymentService. Nhìn rất chuyên nghiệp trên whiteboard.

Vấn đề: những cái tên đó không phản ánh tại sao code thực sự thay đổi. Đây là “The Working Code Trap” (Martin Fowler): code chạy được không có nghĩa là code được thiết kế đúng.

Giải Pháp Hiển Nhiên (Và Tại Sao Nó Thất Bại)

Chia service theo technical layer (Frontend, Backend, Database, Auth, Notification) hoặc theo team trông có vẻ hợp lý. Nhưng 6–12 tháng sau bạn gặp “Morning After Syndrome” (Sam Newman):

  • Deploy tính năng A phải coordinate với 4 team
  • Một service thay đổi, 3 service khác bị break
  • Hotfix đơn giản cần sign-off từ nhiều service owner

Vấn Đề Thực Sự: The Kitty Problem

Giả sử bạn có hệ thống e-commerce với ProductService, InventoryService, SearchService. Business yêu cầu: “Khi sản phẩm hết hàng, ẩn nó khỏi search results”. Logic này thuộc về service nào?

Đây là Kitty Problem — một concern cắt ngang qua nhiều service. Khi bạn duplicate logic để giải quyết, bạn tạo ra distributed monolith: tệ nhất của cả hai thế giới.

Nguyên nhân gốc rễ: service boundary được vẽ theo “danh từ” (noun-based) thay vì theo “lý do thay đổi” (reason-to-change).

Nguyên Tắc Nền Tảng: Axes of Change

Robert C. Martin định nghĩa Common Closure Principle (CCP):

“Gather together the things that change for the same reasons. Separate the things that change for different reasons.”

Dịch thực tế: Code thay đổi cùng nhau vì cùng lý do → nên sống trong cùng service.

Trong một hệ thống logistics, các thử này có axes of change khác nhau và nên là 4 service riêng biệt:

  • Pricing rules — thay đổi khi business thay đổi chiến lược giá
  • Route optimization — thay đổi khi thuật toán mới ra đời
  • Driver assignment — thay đổi khi supply/demand model thay đổi
  • Notification templates — thay đổi khi marketing team A/B test messaging

Domain Discovery: Quy Trình 5 Bước

Làm thế nào để tìm ra đúng service boundary? Đây là quy trình kết hợp Domain-Driven Design và Event Storming:

Bước 1 — Event Storming: Map Domain Events

Viết tất cả domain events theo thứ tự thời gian (dùng sticky notes trên Miro), past tense, không vẽ boxes:

OrderPlaced → PaymentProcessed → InventoryReserved → WarehousePicked → ShipmentDispatched → DeliveryConfirmed

Bước 2 — Tìm Pivot Events

Pivot events là những events mà sau đó actor hoặc context thay đổi. Ví dụ: sau PaymentProcessed → trách nhiệm chuyển sang Fulfillment, không phải Sales. Những pivot events này là natural boundary candidates.

Bước 3 — Xác Định Bounded Context

Nhóm events vào các Bounded Context — vùng trong đó ngôn ngữ có nghĩa nhất quán. Từ “Order” có nghĩa khác nhau trong Sales context (đơn hàng chưa thanh toán) vs. Fulfillment context (đơn hàng cần xử lý) vs. Shipping context (kiện hàng cần giao). Đây là 3 different objects, không phải 3 trạng thái của cùng 1 object.

Bước 4 — Test Với “Independent Deployability” Check

Với mỗi proposed boundary, hỏi: Nếu team A thay đổi logic ở context này, có cần coordinate với team B không? Nếu câu trả lời là “có” với >50% câu hỏi → boundary đang sai. Merge lại hoặc redraw.

Bước 5 — Define Integration Contract

Khi boundary đã đúng, định nghĩa cách các context communicate: Synchronous (REST/gRPC) khi cần response ngay, Asynchronous (events) khi downstream chỉ cần “biết” không cần block, Shared Kernel khi 2 context chia sẻ một số domain model (dùng hạn chế).

Case Study: Craft Dispatch System (1985)

Một trong những ví dụ kinh điển nhất về service boundary đúng đến từ… năm 1985.

Craft Dispatch System là hệ thống điều phối xe cứu hỏa của thành phố London. Họ phải hot-swap components trong khi hệ thống vẫn đang chạy — vì cứu hỏa không được phép downtime.

Giải pháp: state machine được externalized ra khỏi business logic. Mỗi “capability” (locate, route, assign) là một replaceable component communicate qua well-defined events. Kết quả: deploy thuật toán route mới mà không cần restart hệ thống.

40 năm sau, đây vẫn là mô hình đúng: boundary được định nghĩa bởi capability (khả năng thay đổi độc lập), không phải bởi noun.

Monolith-First: Khi Nào Nên Chia Service

Bạn không thể vẽ đúng service boundary trước khi bạn hiểu domain đủ sâu. Martin Fowler đề xuất Monolith-first pattern:

  1. Xây dựng monolith với internal module boundaries rõ ràng
  2. Vận hành trong production đủ lâu để hiểu actual axes of change
  3. Khi một module có scaling/deployment requirement khác → extract thành service

Extract khi: module cần scale độc lập, cần deploy frequency khác (ML model hàng ngày vs. core logic hàng tuần), cần SLA/SLO riêng biệt, hoặc owned bởi team khác với tech stack khác. Không extract chỉ vì “nghe có vẻ là service riêng”.

Checklist Trước Khi Vẽ Architecture Diagram

Lần tới khi bạn cầm bút vẽ boxes và arrows, hỏi những câu này trước:

  1. Domain events nào xảy ra trong system này? (Map events trước, boxes sau)
  2. Ai là actor của mỗi event? (Người/system nào trigger? Ai receive?)
  3. Context nào có ngôn ngữ riêng biệt? (Từ “Order” có nghĩa giống nhau ở mọi nơi không?)
  4. Nếu tôi thay đổi phần này, team nào cần biết? (Nếu >1 team → boundary đang sai)
  5. Có operational reason để tách không? (Hay chỉ vì “nghe đúng”?)

Kết: Service Boundary Là Quyết Định Tốn Kém Nhất

Code có thể refactor. Database schema có thể migrate. Nhưng service boundary sai — một khi đã có team, pipeline, contract, client phụ thuộc — thì cực kỳ tốn kém để sửa.

Startup fintech kia cuối cùng dành 6 tháng để merge lại thành fewer, larger services — sau khi đã tốn 18 tháng để tách ra. Họ gọi đó là “the great consolidation”.

Nguyên tắc đơn giản: boundary đúng = team có thể deploy độc lập. Nếu bạn vẫn cần coordinate nhiều team để deploy một tính năng nhỏ, boundary đang sai — dù architecture diagram trông đẹp đến đâu.

Bước tiếp theo: Thử Event Storming với team trong 2 tiếng. Map ra tất cả domain events. Bạn sẽ tìm thấy ít nhất 2–3 chỗ mà boundaries hiện tại không khớp với axes of change thực tế.

Bài liên quan: Lộ Trình 3 Tháng Dành Cho Beginner: Từ Học Sai Cách Đến Tự Tin Phỏng Vấn | Tại Sao Dev Giỏi Debug 3x Nhanh Hơn?

Tài liệu tham khảo:


Comments

Gửi phản hồi

Khám phá thêm từ Hiểu Code

Đăng ký ngay để tiếp tục đọc và truy cập kho lưu trữ đầy đủ.

Tiếp tục đọc