Chương 11. Tách Sai Sẽ Thành Distributed Monolith

Microservices không phải lúc nào cũng làm hệ thống tốt hơn. Nếu tách sai, ta có thể tạo ra một thứ còn khó chịu hơn monolith: distributed monolith.

Distributed monolith là hệ thống nhìn bên ngoài giống microservices: nhiều service, nhiều repo, nhiều container, nhiều API. Nhưng bên trong, các service vẫn dính chặt vào nhau. Chúng phải deploy cùng nhau, dùng chung database lung tung, gọi nhau dây chuyền, lỗi một service kéo theo nhiều service khác.

Nói ngắn gọn:

> Distributed monolith là monolith bị cắt ra qua network, nhưng coupling vẫn còn nguyên.

Nó có cái xấu của monolith và cái khó của microservices cùng lúc.

---

11.1. Ví dụ quán bánh tách sai

Quán bánh muốn "chuyên nghiệp hóa", nên tách thành nhiều bộ phận riêng:

  • Bộ phận nhận đơn.
  • Bộ phận bếp.
  • Bộ phận giao hàng.
  • Bộ phận thu tiền.
  • Bộ phận kế toán.

Nghe hợp lý.

Nhưng họ tách sai:

  • Nhận đơn phải gọi bếp để hỏi từng món còn không.
  • Bếp phải gọi kế toán để biết khách đã trả tiền chưa.
  • Giao hàng phải gọi nhận đơn để lấy địa chỉ.
  • Kế toán phải gọi bếp để biết bánh đã làm xong chưa.
  • Mỗi lần đổi giá, cả năm bộ phận phải cập nhật cùng lúc.
  • Sổ dữ liệu vẫn để chung, ai cũng sửa được.

Kết quả:

  • Nhìn thì có nhiều bộ phận.
  • Nhưng không bộ phận nào thật sự độc lập.
  • Một bộ phận chậm làm tất cả chậm.
  • Một thay đổi nhỏ kéo theo mọi bộ phận.

Đây giống distributed monolith.

Tách hình thức, nhưng không tách trách nhiệm thật.

---

11.2. Distributed monolith là gì?

Distributed monolith là hệ thống có nhiều service nhưng vẫn vận hành như một khối dính chặt.

Dấu hiệu:

  • Service phải deploy cùng nhau.
  • Service gọi nhau quá nhiều trong cùng một request.
  • Service dùng chung database trực tiếp.
  • Một thay đổi nhỏ phải sửa nhiều service.
  • Không có ownership dữ liệu rõ.
  • Không có API contract ổn định.
  • Service A chết kéo service B, C, D chết theo.
  • Debug khó hơn monolith nhưng không có lợi ích độc lập của microservices.

Sơ đồ nhìn có vẻ microservices:

API Gateway
  |-- User Service
  |-- Order Service
  |-- Payment Service
  |-- Notification Service

Nhưng thực tế:

Order Service gọi User Service
Order Service gọi Payment Service
Payment Service gọi Order Service
Notification Service query thẳng database Order
User Service sửa bảng dùng chung
Deploy Order phải deploy Payment

Đó không phải microservices khỏe. Đó là monolith phân tán.

---

11.3. Vì sao distributed monolith nguy hiểm?

Monolith ít nhất còn đơn giản ở runtime:

App -> Database

Nếu code rối, vẫn có thể debug trong một process, một codebase, một database.

Microservices đúng có lợi ích:

  • Deploy độc lập.
  • Scale độc lập.
  • Failure isolation.
  • Ownership rõ.

Distributed monolith thì mất cả hai:

  • Không đơn giản như monolith.
  • Không độc lập như microservices.

Nó thêm:

  • Network latency.
  • Timeout.
  • API versioning.
  • Distributed logs.
  • Nhiều deployment.
  • Nhiều failure point.

Nhưng vẫn không có:

  • Service autonomy.
  • Data ownership rõ.
  • Deploy độc lập thật.
  • Failure isolation thật.

Đây là lý do tách sai có thể làm hệ thống tệ hơn trước.

---

11.4. Dấu hiệu 1: service gọi nhau dây chuyền trong một request

Một request đi qua quá nhiều service:

Frontend
  -> API Gateway
  -> Order Service
  -> User Service
  -> Payment Service
  -> Inventory Service
  -> Coupon Service
  -> Notification Service

Nếu mỗi service mất 100ms, tổng latency đã cao. Nếu một service chậm hoặc lỗi, cả request chậm/lỗi.

Vấn đề tăng mạnh khi có retry.

Ví dụ:

  • Order gọi Payment.
  • Payment timeout.
  • Order retry.
  • Payment thật ra vẫn xử lý.
  • Retry tạo giao dịch trùng nếu không idempotent.

Hoặc:

  • Service A gọi B.
  • B gọi C.
  • C gọi D.
  • D chậm.
  • A giữ connection chờ.
  • Nhiều request cùng như vậy làm A cạn thread/connection.

Không phải service không được gọi nhau. Nhưng request path quá dài là dấu hiệu nguy hiểm.

Cách giảm:

  • Gom các logic cần transaction mạnh vào cùng boundary.
  • Dùng async event cho side effect không cần trả ngay.
  • Dùng read model/cache để tránh gọi dây chuyền chỉ để lấy dữ liệu.
  • Đặt timeout.
  • Tránh chatty API.

---

11.5. Dấu hiệu 2: shared database lung tung

Một trong những dấu hiệu rõ nhất của distributed monolith là nhiều service dùng chung database trực tiếp.

Ví dụ:

Order Service -> main_db.orders
Payment Service -> main_db.payments
Notification Service -> main_db.orders, main_db.users, main_db.payments
Report Service -> query mọi bảng

Nếu mỗi service chỉ sở hữu bảng của mình và có quy tắc rõ, còn tạm chấp nhận trong giai đoạn chuyển tiếp.

Nhưng nếu service nào cũng đọc/ghi bảng của service khác, boundary không còn thật.

Hậu quả:

  • Schema change rất khó.
  • Không biết ai sở hữu dữ liệu.
  • Service có thể phá invariant của service khác.
  • Tách database sau này rất đau.
  • Deploy service vẫn phụ thuộc schema chung.

Ví dụ:

Payment Service tự sửa trạng thái order trong bảng orders.

Nghe tiện, nhưng logic order bị bypass. Sau này Order Service không biết vì sao trạng thái đổi.

Cách giảm:

  • Xác định ownership bảng/dữ liệu.
  • Service khác không ghi trực tiếp dữ liệu không thuộc mình.
  • Dùng API/event/read model thay vì query chéo.
  • Nếu đang chuyển tiếp, ghi rõ "tạm thời" và có kế hoạch thoát.

---

11.6. Dấu hiệu 3: deploy vẫn dính nhau

Microservices hứa hẹn deploy độc lập.

Nhưng distributed monolith thường có tình trạng:

  • Deploy Order phải deploy Payment.
  • Deploy Payment phải deploy Notification.
  • Deploy User đổi field làm 5 service lỗi.
  • Một API đổi nhỏ kéo theo frontend và nhiều service.

Khi đó nhiều service chỉ làm deploy phức tạp hơn, không tạo tự do.

Nguyên nhân:

  • API contract không ổn định.
  • Không giữ backward compatibility.
  • Dùng chung model/schema trong nhiều service.
  • Không có versioning.
  • Không có contract test.
  • Service boundary sai.

Cách giảm:

  • API phải backward compatible.
  • Không xóa/sửa field đột ngột.
  • Thêm field trước, migrate consumer sau, rồi mới xóa field cũ.
  • Dùng contract testing cho API quan trọng.
  • Tách deploy thật sự chỉ khi service có boundary ổn định.

---

11.7. Dấu hiệu 4: service quá nhỏ và quá chatty

Tách quá nhỏ dễ tạo chatty services.

Ví dụ:

Order Service cần hiển thị đơn hàng
  -> gọi User Service lấy tên user
  -> gọi Product Service lấy tên sản phẩm
  -> gọi Payment Service lấy trạng thái thanh toán
  -> gọi Delivery Service lấy trạng thái giao hàng
  -> gọi Coupon Service lấy mã giảm giá

Một trang đơn hàng cần 5-10 network call.

Nếu mỗi call có latency, timeout, retry, hệ thống sẽ chậm và khó debug.

Service quá nhỏ còn làm logic nghiệp vụ bị xé vụn. Không ai nhìn thấy toàn bộ use case.

Cách giảm:

  • Tách service theo capability đủ lớn, không tách theo entity nhỏ.
  • Dùng API composition cẩn thận.
  • Dùng read model cho màn hình đọc nhiều.
  • Dùng BFF nếu frontend cần dữ liệu tổng hợp.
  • Tránh service chỉ là CRUD wrapper cho một bảng.

---

11.8. Dấu hiệu 5: một thay đổi nghiệp vụ phải sửa nhiều service

Ví dụ business đổi rule:

> Nếu khách VIP đặt đơn trên 1 triệu, miễn phí giao hàng và tặng voucher.

Nếu phải sửa:

  • User Service để check VIP.
  • Order Service để tính tổng.
  • Delivery Service để miễn phí.
  • Coupon Service để tạo voucher.
  • Notification Service để gửi email.
  • Payment Service để điều chỉnh thanh toán.

Thay đổi nhỏ trở thành chiến dịch liên service.

Điều này có thể bình thường nếu nghiệp vụ thật sự đi qua nhiều domain. Nhưng nếu hầu hết thay đổi đều phải chạm nhiều service, có thể boundary sai.

Câu hỏi:

  • Rule này thật ra thuộc domain nào?
  • Có service nào nên điều phối use case không?
  • Có nên gom một số phần lại?
  • Có phải ta tách theo entity thay vì capability?

Microservices tốt làm một số thay đổi cục bộ hơn. Distributed monolith làm mọi thay đổi lan rộng hơn.

---

11.9. Dấu hiệu 6: không có service owner thật

Một service nên có người/team chịu trách nhiệm.

Nếu không ai sở hữu rõ:

  • Ai sửa cũng được.
  • Ai deploy cũng được.
  • Ai cũng thêm field.
  • Không ai theo dõi alert.
  • Không ai chịu trách nhiệm uptime.

Khi đó service chỉ là folder chạy riêng, không phải đơn vị ownership.

Microservices không chỉ là kỹ thuật. Nó cần ownership.

Nếu team nhỏ và mọi người vẫn sửa mọi service, microservices có thể chưa đem lại lợi ích ownership. Modular monolith có thể đơn giản hơn.

---

11.10. Dấu hiệu 7: thiếu observability

Distributed monolith rất khó debug nếu thiếu observability.

Các câu hỏi thường gặp:

  • Request này đi qua service nào?
  • Service nào chậm?
  • Service nào retry?
  • Lỗi ở đâu?
  • Correlation ID là gì?
  • Event này đã được consume chưa?
  • Queue nào đang kẹt?

Nếu không có:

  • Centralized logs.
  • Correlation ID.
  • Metrics.
  • Distributed tracing.
  • Alert.

Thì mỗi sự cố là một cuộc đào bới log thủ công.

Microservices không nên đi trước khả năng quan sát hệ thống.

---

11.11. Dấu hiệu 8: transaction bị phá nhưng không có saga

Trong monolith:

Tạo order + trừ tồn kho + tạo payment

có thể nằm trong một database transaction.

Sau khi tách service:

Order Service
Inventory Service
Payment Service

Không còn transaction đơn giản.

Nếu hệ thống vẫn giả vờ như transaction cũ còn tồn tại, sẽ có lỗi:

  • Order tạo rồi nhưng payment fail.
  • Payment thành công nhưng inventory fail.
  • Inventory trừ rồi nhưng order bị hủy.
  • Retry tạo trạng thái trùng.

Microservices cần thiết kế consistency:

  • Saga.
  • Compensating transaction.
  • Outbox.
  • Idempotency.
  • Reconciliation.

Nếu tách service mà không thiết kế lại consistency, hệ thống rất dễ sai dữ liệu.

---

11.12. Nguyên nhân tạo distributed monolith

Tách theo database table

Mỗi bảng một service:

User Service
Order Service
OrderItem Service
Payment Service
Product Service

Kết quả là service nhỏ, chatty, thiếu nghiệp vụ.

Tách theo danh từ thay vì capability

Không phải cứ có danh từ là có service.

Service nên gắn với capability:

  • Ordering.
  • Payment.
  • Fulfillment.
  • Identity.
  • Notification.

Không phải chỉ:

  • User.
  • Order.
  • Product.
  • Status.

Tách khi domain chưa rõ

Boundary đoán sai thì service dính nhau.

Tách để né refactor

Code rối chưa được hiểu rõ, tách ra càng rối.

Thiếu kỷ luật API/data ownership

Service boundary chỉ có tên, không có luật.

---

11.13. Cách tránh distributed monolith

Bắt đầu bằng modular monolith nếu chưa chắc boundary

Tách module trong code trước. Quan sát xem module nào thật sự có boundary ổn.

Tách theo capability

Service nên đại diện cho năng lực nghiệp vụ đủ lớn.

Ví dụ:

  • Payment capability.
  • Search capability.
  • Notification capability.
  • Media processing capability.

Sở hữu dữ liệu rõ

Mỗi service phải biết dữ liệu nào thuộc về mình.

Service khác không được ghi trực tiếp vào dữ liệu đó.

Giữ API contract ổn định

Không đổi API tùy hứng. Phải nghĩ đến consumer.

Giao tiếp đúng kiểu

  • Cần kết quả ngay: API.
  • Việc phụ/side effect: event/queue.
  • Nhiều consumer: event.
  • Cần replay: event streaming.

Có observability trước khi tách nhiều

Logs, metrics, traces, correlation ID.

Tách từng phần, không tách hàng loạt

Tách một service, học từ nó, rồi mới tách tiếp.

---

11.14. Khi lỡ có distributed monolith thì làm gì?

Không cần hoảng. Có thể sửa dần.

Bước 1: Vẽ dependency graph

Service nào gọi service nào? Service nào query database nào? Request quan trọng đi qua những service nào?

Bước 2: Tìm service bị gọi quá nhiều

Service nào là bottleneck? Service nào làm nhiều service khác chờ?

Bước 3: Tìm shared database access nguy hiểm

Ai đang đọc/ghi bảng không thuộc mình?

Bước 4: Tạo API/interface chính thức

Thay query chéo bằng API/event/read model ở những nơi quan trọng.

Bước 5: Gom lại nếu tách sai

Đôi khi cách đúng là nhập hai service lại.

Đừng ngại "unservice". Nếu hai service luôn deploy cùng nhau, luôn gọi nhau, luôn dùng chung dữ liệu, có thể chúng nên là một service/module.

Bước 6: Thêm observability

Không thể sửa thứ mình không nhìn thấy.

---

11.15. Ví dụ: Order và Payment tách sai

Tách sai:

Order Service tạo order
Payment Service tự sửa order.status trong database chung
Order Service query payment table để hiển thị trạng thái
Deploy payment đổi schema làm order lỗi

Dấu hiệu:

  • Shared database lung tung.
  • Ownership không rõ.
  • Deploy dính nhau.
  • Logic trạng thái bị chia sai.

Cách sửa:

Order Service sở hữu order.status
Payment Service sở hữu payment transaction
Payment Service phát event PaymentSucceeded
Order Service nghe event và cập nhật order

Hoặc nếu hệ thống còn nhỏ:

Gom payment thành module trong modular monolith

Tùy giai đoạn, cả hai hướng đều có thể đúng.

---

11.16. Ví dụ: Notification tách đúng hơn

Notification thường là candidate dễ tách hơn.

Lý do:

  • Gửi email/SMS/push là side effect.
  • Không cần chặn luồng chính.
  • Có thể nhận event.
  • Có retry riêng.
  • Có provider/rate limit riêng.

Luồng:

Order Service -> event OrderCreated -> Notification Service -> gửi email

Nếu Notification Service chết:

  • Order vẫn tạo được.
  • Event/job vẫn nằm trong queue nếu broker bền.
  • Notification xử lý lại sau.

Đây là boundary tốt hơn vì notification ít cần transaction đồng bộ với order.

---

11.17. Checklist nhận diện distributed monolith

Trả lời các câu sau:

  • Một thay đổi nhỏ có phải sửa nhiều service không?
  • Các service có phải deploy cùng nhau không?
  • Service có dùng chung database lung tung không?
  • Một request có đi qua quá nhiều service không?
  • Có service nào chỉ là CRUD wrapper cho một bảng không?
  • Có thiếu timeout giữa services không?
  • Có retry storm không?
  • Có distributed tracing không?
  • Service chết có làm nhiều service khác chết theo không?
  • Có biết service nào sở hữu dữ liệu nào không?
  • Có API contract rõ không?
  • Có thể rollback một service độc lập không?

Nếu nhiều câu trả lời là "có vấn đề", hệ thống có dấu hiệu distributed monolith.

---

11.18. Kết luận của chương

Distributed monolith là cái bẫy rất phổ biến: hệ thống có nhiều service nhưng vẫn dính chặt như một monolith.

Nó nguy hiểm vì:

  • Vẫn không có deploy độc lập thật.
  • Vẫn không có ownership rõ.
  • Vẫn không có data boundary rõ.
  • Nhưng lại có thêm network, latency, timeout, tracing, versioning và vận hành phức tạp.

Để tránh:

  • Đừng tách theo bảng.
  • Đừng tách khi domain chưa rõ.
  • Đừng dùng shared database lung tung.
  • Đừng để request gọi dây chuyền quá nhiều service.
  • Đừng thiếu timeout/observability.
  • Đừng ngại gom lại nếu tách sai.

Thông điệp quan trọng:

> Microservices không phải là nhiều service. Microservices là các service có ranh giới thật, ownership thật, dữ liệu thật, deploy thật và khả năng vận hành thật.

Nếu chưa đạt được điều đó, modular monolith có thể là lựa chọn lành mạnh hơn.