Bất kỳ team Magento nào khi quyết định chuyển dịch sang microservices cũng đều phải đối mặt với cùng một câu hỏi đầu tiên: cần bao nhiêu service?
Ngành công nghiệp thường bảo là 4–6. “Service danh mục (Catalog), service đơn hàng (Order), service khách hàng (Customer), service tồn kho (Inventory), service thanh toán (Payment), và có thể thêm CMS nữa.” Mọi bài blog, mọi bài diễn thuyết ở hội thảo đều tựu trung ở danh sách này. Đó là một điểm khởi đầu hợp lý — nhưng nó hoàn toàn sai lầm khi áp dụng cho e-commerce nghiêm túc ở quy mô lớn.
Nền tảng Composable Commerce mà chúng tôi đang ghi chép trong series này sở hữu 21 microservices phân bổ qua 6 nhóm bounded context (ngữ cảnh giới hạn). Số lượng này gấp 3–4 lần so với mức khuyến nghị chung. Bài viết này sẽ giải thích tại sao — với một bảng ánh xạ hoàn chỉnh từ module Magento sang service, và hai pha chia tách (domain splits) nghe có vẻ ngược đời mà các kỹ sư Magento hầu như lúc nào cũng làm sai.
Answer-first: Số lượng service bạn cần được quyết định bởi cấu trúc đội ngũ, đặc tả chịu tải (scaling profile), và ranh giới của các bất biến nghiệp vụ (business invariants) của bạn — chứ không phải bởi khuôn mẫu (convention). Với quy mô 10,000+ SKU, 20+ kho hàng, và hơn 10,000 đơn hàng/ngày vận hành bởi nhiều team kỹ sư độc lập, thì 21 service là con số chính xác. Cùng một nền tảng đó nhưng nếu chỉ là một shop 500 đơn/ngày thì số lượng chỉ nên là 5–7.
1. Tại sao lại chia theo Ranh giới DDD, mà không phải Bảng Database
Sai lầm phổ biến nhất khi phân rã hệ thống mà các kỹ sư Magento hay mắc phải là nhìn vào các bảng database của Magento và vẽ ranh giới service bao quanh chúng.
“Chúng ta có bảng
catalog_product_entity, vậy ta cần một Product Service.”
“Chúng ta có bảngsales_order, vậy ta cần một Order Service.”
“Chúng ta có bảngcustomer_entity, vậy ta cần một Customer Service.”
Cách làm này tạo ra những service thiếu máu (anemic services) — những service rốt cuộc chỉ là những lớp vỏ bọc REST mỏng dính trùm lên các bảng database mà chẳng chứa chút logic nghiệp vụ (domain logic) thực sự nào. Chúng không hề làm giảm sự phụ thuộc rườm rà (coupling); chúng chỉ bê nguyên xi mớ phụ thuộc đó từ code PHP của Magento biến thành các lời gọi API chéo nhau giữa các service.
Thiết kế Hướng Miền (Domain-Driven Design - DDD) áp dụng một cách tiếp cận khác: gom nhóm code xoay quanh các năng lực nghiệp vụ (business capabilities) và những quy tắc bất biến (invariants) của chúng, chứ không phải cấu trúc dữ liệu.
Phép thử ở đây là: “Quy tắc nghiệp vụ này có thể được cưỡng chế thi hành nằm trọn vẹn bên trong một database transaction của một service duy nhất hay không?” Nếu có, ranh giới service đó là chính xác. Nếu quy tắc bất biến đòi hỏi phải có sự phối hợp giữa nhiều service, bạn sẽ cần đến một Saga hoặc một Sự kiện Miền (Domain Event).
Ví dụ:
- “Không thể đặt hàng nếu email của khách chưa được xác thực” → Order Service phải đọc dữ liệu từ Customer Service. Đây là một truy vấn đọc (read query), hoàn toàn chấp nhận được nếu gọi đồng bộ qua gRPC.
- “Lượng tồn kho không được phép rơi xuống số âm xuyên suốt nhiều kho” → Warehouse Service nắm trọn quyền quản lý mọi loại tồn kho. Quy tắc bất biến nằm trọn vẹn bên trong một service. Ranh giới chính xác.
- “Một mã giảm giá (coupon) chỉ được dùng một lần duy nhất cho mỗi khách hàng” → Promotion Service sở hữu toàn quyền việc đổi mã. Quy tắc bất biến nằm trọn vẹn bên trong một service. Ranh giới chính xác.
2. Nhóm 6 Bounded Context
Nền tảng này tổ chức 21 service thành 6 nhóm domain. Dưới đây là bảng ánh xạ hoàn chỉnh — bao gồm cả việc từng service sẽ thay thế cho những module Magento nào:
🛒 Nhóm 1: Luồng Thương mại (3 services)
| Service | HTTP Port | gRPC Port | Thay thế các Module Magento |
|---|---|---|---|
| Checkout Service | 8004 | 9004 | Magento_Checkout, Magento_Quote |
| Order Service | 8001 | 9001 | Magento_Sales, Magento_SalesSequence |
| Payment Service | 8003 | 9003 | Magento_Payment, Magento_Braintree, Magento_Paypal |
📦 Nhóm 2: Sản phẩm & Nội dung (4 services)
| Service | HTTP Port | gRPC Port | Thay thế các Module Magento |
|---|---|---|---|
| Catalog Service | 8005 | 9005 | Magento_Catalog, Magento_CatalogImportExport |
| Pricing Service | 8002 | 9002 | Magento_CatalogRule, Magento_CatalogPrice, Magento_Tax |
| Promotion Service | 8011 | 9011 | Magento_SalesRule, Magento_Coupon, Magento_Reward (một phần) |
| Search Service | 8012 | 9012 | Magento_Search, Magento_CatalogSearch, Magento_Elasticsearch |
🔐 Nhóm 3: Định danh & Truy cập (3 services)
| Service | HTTP Port | gRPC Port | Thay thế các Module Magento |
|---|---|---|---|
| Auth Service | 8013 | 9013 | Magento_Authorization, Magento_JwtUserToken |
| User Service | 8014 | 9014 | Magento_User, Magento_Backend (dành cho admin user) |
| Customer Service | 8006 | 9006 | Magento_Customer, Magento_CustomerBalance |
🚚 Nhóm 4: Hậu cần - Logistics (3 services)
| Service | HTTP Port | gRPC Port | Thay thế các Module Magento |
|---|---|---|---|
| Warehouse Service | 8008 | 9008 | Magento_InventoryAdminUi, Magento_InventoryApi, Magento_CatalogInventory |
| Fulfillment Service | 8009 | 9009 | Magento_InventoryShipping, Magento_InventorySourceDeductionApi |
| Shipping Service | 8010 | 9010 | Magento_Shipping, Magento_OfflineShipping, Magento_ShippingCore |
🔄 Nhóm 5: Hậu mãi - Post-Purchase (2 services)
| Service | HTTP Port | gRPC Port | Thay thế các Module Magento |
|---|---|---|---|
| Return Service | 8015 | 9015 | Magento_Rma, Magento_SalesRuleSample |
| Loyalty Service | 8016 | 9016 | Magento_Reward, Magento_CustomerBalance (một phần) |
⚙️ Nhóm 6: Nền tảng & Vận hành (6 services)
| Service | HTTP Port | gRPC Port | Vai trò |
|---|---|---|---|
| Gateway Service | 8000 | 9000 | API Gateway, xác thực JWT, giới hạn rate limit |
| Analytics Service | 8017 | 9017 | Các sự kiện mua hàng, báo cáo (Không có phần tương đương bên Magento) |
| Review Service | 8018 | 9018 | Magento_Review, Magento_Rating |
| Notification Service | 8019 | 9019 | Magento_Email, Magento_SendFriend |
| Location Service | 8007 | 9007 | Dữ liệu địa lý, xác thực địa chỉ |
| CommonOps | — | — | Các công cụ vận hành dùng chung, không được deploy như một service |
3. Hai Pha Chia Tách Nghe Có Vẻ Ngược Đời
Chia tách 1: Checkout ≠ Order
Đây là pha phân rã mà các kỹ sư Magento thường phản đối kịch liệt nhất. Trong Magento, Magento_Checkout và Magento_Sales về mặt kỹ thuật là các module riêng biệt — nhưng chúng chia sẻ chung các bảng database và liên kết dính chặt với nhau. Các kỹ sư theo lẽ tự nhiên luôn muốn gom “checkout và order” vào chung một service.
Và đây là lý do tại sao bắt buộc phải tách chúng ra:
Checkout Service quản lý các trạng thái tạm thời, có thể bỏ đi (expendable state):
- Vòng đời của giỏ hàng (thêm món, cập nhật số lượng, áp mã giảm giá, tính tổng tiền)
- Tái xác thực giá (price revalidation) ở bước checkout (ngăn chặn việc giá bị cũ/lệch)
- Kiểm tra giữ chỗ tồn kho (giữ chỗ mềm - soft reserve, không phải trừ vĩnh viễn)
- Điều phối việc tính toán phí vận chuyển
- Lựa chọn phương thức thanh toán
Order Service quản lý các trạng thái vĩnh viễn, đặc biệt quan trọng (permanent, critical state):
- Vòng đời đơn hàng với 8 trạng thái:
PENDING → CONFIRMED → PAYMENT_CAPTURED → PROCESSING → FULFILLMENT_STARTED → SHIPPED → DELIVERED → COMPLETED - Hủy đơn hàng kèm các sự kiện bù đắp (compensation events)
- Cỗ máy trạng thái (state machine) cho việc hoàn hàng và hoàn tiền
- Dữ liệu đơn hàng lịch sử (không bao giờ bị xóa, luôn được audit - kiểm toán)
Sự khác biệt về tính bất biến (invariant) mới là yếu tố định đoạt: Checkout có thể làm mất trạng thái mà không gây ra hậu quả nghiệp vụ nào (một giỏ hàng bị bỏ quên là chuyện bình thường). Còn Order không được phép mất trạng thái trong bất kỳ tình huống nào (một đơn hàng bị mất đồng nghĩa với mất doanh thu và hứng chịu phàn nàn từ khách hàng).
Việc chia tách này mở đường cho khả năng scale độc lập: trong một đợt flash sale, tiến trình xử lý Order chỉ tăng vọt sau khi checkout hoàn tất. Với các service tách biệt, bạn có thể scale số lượng pod của Order từ 3 → 30 mà không cần đụng chạm gì đến pod của Checkout. Nếu gộp chung lại làm một, bạn sẽ phải scale toàn bộ hệ thống.
Chia tách 2: Pricing ≠ Promotion
Hầu hết các tài liệu kiến trúc (và đa phần kết quả tìm kiếm trên Google) đều gộp hai thứ này lại thành một service duy nhất “Pricing & Promotions”. Nền tảng Composable Commerce giữ chúng tách biệt bởi vì chúng sở hữu những đặc tính khác nhau một cách căn bản:
Pricing Service (Dịch vụ Định giá):
- Nguồn sự thật (Source of truth) cho giá gốc (base prices) — giá tiền của sản phẩm trước khi áp dụng bất kỳ khoản chiết khấu nào
- Đảm nhận: giá gốc, giá phân bậc (B2B), đa tiền tệ, tính thuế
- Tần suất cập nhật: thấp (giá sản phẩm ít khi thay đổi liên tục)
- Đặc tả chịu tải: tỷ lệ đọc cực kỳ cao (extremely high read rate) (mọi request gọi vào trang catalog đều đập vào pricing)
- Tối ưu công nghệ: Redis cache kèm write-through, TTL = 1 giờ
Promotion Service (Dịch vụ Khuyến mãi):
- Áp dụng các quy tắc giảm giá — nó làm giảm giá tiền, chứ không định hình ra giá tiền
- Đảm nhận: mã coupon, các luật mua-1-tặng-1 (BOGO), chiết khấu trên tổng giỏ hàng, quy đổi điểm tích lũy
- Tần suất cập nhật: cao (khuyến mãi được tạo/hết hạn diễn ra liên tục)
- Đặc tả chịu tải: hướng sự kiện (event-driven) — consume (lắng nghe) các event
order.cancelledđể hoàn trả lại lượt dùng (redemptions) - Tối ưu công nghệ: PostgreSQL để theo dõi lịch sử quy đổi coupon một cách giao dịch (transactional)
Tài liệu ADR-021 đã ghi chú rõ ràng về việc này: “Pricing Service làm chủ mức giá; Warehouse Service làm chủ lượng tồn kho; Promotion Service làm chủ logic áp dụng giảm giá.” Khi quyền sở hữu đã rõ ràng rành mạch, sẽ không bao giờ phát sinh các rắc rối về distributed transaction (giao dịch phân tán) cho các tác vụ đọc đơn giản.
4. Ứng dụng các Nguyên tắc DDD
Bốn nguyên tắc DDD cực kỳ rõ ràng trích từ ADR-002:
1. Đơn nhiệm (Single Responsibility): Mỗi service sở hữu chính xác một domain nghiệp vụ. Order Service = chỉ lo vòng đời đơn hàng. Payment Service = chỉ lo giao dịch thanh toán. Không có service nào đóng vai trò tiện ích đa năng đụng đâu cũng làm (general-purpose utility).
2. Database Per Service (Mỗi service một DB) (ADR-004): Tuyệt đối không truy cập database trực tiếp chéo qua lại giữa các service. Điều này được cưỡng chế ngay ở tầng hạ tầng — mỗi service có một PostgreSQL instance (phiên bản) của riêng nó. Việc truy cập dữ liệu liên-service chỉ được thực hiện độc quyền thông qua gRPC (đồng bộ) hoặc Dapr PubSub event (bất đồng bộ).
3. Ngôn ngữ Phổ quát (Ubiquitous Language): Mỗi domain sử dụng một bộ thuật ngữ nhất quán. Warehouse Service sẽ dùng từ “stock allocation” (cấp phát tồn kho) và “bin location” (vị trí ngăn/kệ). Order Service sẽ dùng từ “reservation” (giữ chỗ) và “fulfillment request” (yêu cầu xuất hàng). Các thuật ngữ này tuyệt đối không “chảy máu” chéo qua lại các ranh giới service.
4. Lớp Chống tham nhũng (Anti-Corruption Layer): Gateway Service (port 8000) bảo vệ mọi microservice khỏi những thay đổi schema đến từ các client bên ngoài. Đội ngũ frontend làm việc bám theo REST API contract của Gateway; trong khi các service contract (hợp đồng service) nội bộ có thể tiến hóa độc lập hoàn toàn.
5. Tại sao 21 Service lại Phù hợp ở Quy mô này
Tài liệu ADR-002 biện minh rõ ràng cho số lượng service dựa trên bốn yêu cầu nghiệp vụ:
| Yêu cầu | Hệ quả |
|---|---|
| 10,000+ SKU với các thuộc tính EAV động | Catalog + Pricing phải được tách khỏi Checkout (read profile khác biệt so với write profile) |
| 20+ kho hàng có theo dõi cấp độ kệ (bin-level) | Warehouse không thể nhồi chung vào Catalog (quyền sở hữu tồn kho là một domain nghiệp vụ riêng) |
| 10,000+ đơn hàng/ngày với nhiều cổng thanh toán độc lập | Order, Payment, và Checkout phải có khả năng scale độc lập với nhau |
| Nhiều team kỹ thuật (engineering teams) làm việc song song | Số lượng Service ≈ số lượng team × 2–3 (Định luật Conway) |
ADR-002 cũng thừa nhận một rủi ro: “Bùng nổ Service (Service Explosion): Được giảm thiểu bằng cách vạch ranh giới domain rõ ràng và tuân thủ các nguyên tắc DDD.” Với các team có quy mô dưới 20 kỹ sư hoặc các nền tảng xử lý ít hơn 2,000 đơn hàng/ngày, cách chia tách thành 5–7 service sẽ phù hợp hơn.
6. Ma trận Giao tiếp Liên Domain
| Từ | Đến | Giao thức | Khi nào |
|---|---|---|---|
| Checkout | Order, Payment, Pricing, Shipping | gRPC (sync) | Luồng thanh toán thời gian thực |
| Order | Warehouse, Fulfillment, Customer | Dapr events (async) | Xử lý sau khi đã chốt đơn (Post-order) |
| Warehouse | Fulfillment | Dapr events (async) | Kích hoạt (trigger) các luồng phân bổ kho |
| Payment | Các cổng bên ngoài (VNPay, MoMo) | REST (sync) | Xử lý thanh toán thực tế |
| Catalog | Search | Dapr events (async) | Đồng bộ dữ liệu index tìm kiếm |
| Promotion | Order, Customer | Dapr events (async) | Hủy quy đổi (redemption reversal) khi hủy đơn |
Mô hình chung: Dùng gRPC đồng bộ cho các luồng xử lý thời gian thực hướng tới user (user-facing), dùng event bất đồng bộ cho các tác vụ hậu giao dịch (post-transaction).
7. Mức độ Trưởng thành (Maturity) của Service khi Bắt đầu Chuyển đổi
Không phải cả 21 service đều có thể đạt chuẩn production-ready (sẵn sàng lên production) cùng một lúc. Cẩm nang chuyển đổi ở Phần 6 sử dụng mức độ trưởng thành của service như một thước đo để sắp xếp trình tự (sequencing):
🟢 Sẵn sàng Production đầu tiên (chuyển đổi trong Phase 1 và 2): Auth, Customer, Catalog, Pricing, Gateway, Search, Location
🟡 Gần đạt Production (chuyển đổi trong Phase 2 và 3): Order, Payment, Warehouse, Shipping, Return, Loyalty
🔵 Phát triển song song (hoàn tất trong quá trình chuyển đổi, deploy trong Phase 3): Promotion, Fulfillment, Analytics, Review, Notification
Bắt đầu chiến lược Strangler Fig với những service đã sẵn sàng (production-ready) sẽ giúp giảm thiểu rủi ro: nếu phương pháp chuyển đổi vấp phải thất bại, bạn sẽ sớm phơi bày nó ở những domain có rủi ro thấp hơn (chẳng hạn như duyệt catalog) trước khi nó kịp chạm tay đến luồng sinh tử là khởi tạo Đơn hàng (Order creation).
Bước Tiếp Theo
Bạn giờ đây đã có bản đồ domain trong tay: 21 service, 6 nhóm, các ranh giới sở hữu cực kỳ rõ ràng, và lập luận đằng sau hai pha chia tách (splits) có vẻ ngược đời.
Câu hỏi tiếp theo là công cụ (tooling): làm thế nào để bạn quản lý 21 service bằng Go + 2 frontend bằng React nằm chung trong một kho lưu trữ (repository) duy nhất, duy trì sự đồng nhất của phiên bản dependency, và chạy các bộ biên dịch tăng dần (incremental builds) trong hệ thống CI? Đó chính là nội dung của Phần 2: Thiết lập Rush Monorepo.
Hoặc, nếu bạn muốn nhảy thẳng xuống tầng thực thi code: Phần 3: Kratos v2 Internals sẽ chỉ cho bạn xem một Go microservice trông ra làm sao từ hàm main.go chạy cho đến tận lúc truy vấn database.
Câu Hỏi Thường Gặp (FAQ)
Có bắt buộc phải chia đúng 21 service cho một cuộc chuyển đổi từ Magento sang microservices không?
Không. 21 service là con số phù hợp cho một nền tảng xử lý hơn 10,000 đơn hàng/ngày với nhiều team kỹ thuật hoạt động độc lập. Đối với một cửa hàng dưới 2,000 đơn hàng/ngày và có team dưới 10 người, từ 5–7 service là hợp lý hơn nhiều. Nguyên tắc là: Số lượng service ≈ Số lượng team × 2–3, bị giới hạn bởi các bất biến về chịu tải (scaling invariants) thực tế của bạn.
Tại sao bắt buộc Checkout và Order phải tách ra thành các service riêng?
Checkout quản lý các trạng thái tạm thời, có thể bỏ đi được (chẳng hạn giỏ hàng). Order thì quản lý trạng thái tài chính vĩnh viễn, được kiểm toán kỹ lưỡng. Chúng sở hữu những mức dung sai thất bại (failure tolerance) hoàn toàn đối nghịch nhau: một giỏ hàng bị bỏ mặc thì bình thường; nhưng làm mất một đơn hàng đồng nghĩa với việc mất doanh thu thật. Việc tách bạch chúng cũng mở đường cho việc scale độc lập — trong mùa flash sale, số lượng pod của Order có thể scale lên 10× trong khi pod của Checkout vẫn giữ nguyên không đổi.
Điều gì xảy ra nếu tôi không chịu tách Pricing ra khỏi Promotion?
Gộp chung chúng lại sẽ đẻ ra một service đơn lẻ nhưng lại phải gánh 2 đặc tả chịu tải cực kỳ chọi nhau: việc đọc dữ liệu (read) Pricing xảy ra trên mọi trang catalog (tỷ lệ đọc cực cao, rất thân thiện với cache), trong khi việc Promotion áp mã giảm giá thì lại cần đến thao tác trừ mã (deduplication) mang tính giao dịch nghiêm ngặt (hướng sự kiện, ghi dữ liệu liên tục - write-heavy). Một service bị gộp chung ép bạn phải cấp phát thừa mứa tài nguyên (over-provision) cho luồng ít traffic hơn, đồng thời tạo ra một “vùng nổ” (blast radius) rộng hơn rất nhiều mỗi khi một trong hai component này tạch.
Làm thế nào để kiểm chứng các ranh giới Bounded Context trước khi bắt tay viết code?
Hãy áp dụng phép thử giao dịch (transaction test): “Quy tắc nghiệp vụ này có thể được cưỡng chế thi hành nằm trọn vẹn bên trong một database transaction của một service duy nhất hay không?” Nếu có, ranh giới đó là đúng. Nếu quy tắc yêu cầu phải có sự phối hợp từ hai service, bạn sẽ cần đến một Saga hoặc một truy vấn đọc — và chi phí phối hợp đó chính là cái giá bạn phải trả để giữ cho các service đó được tách bạch với nhau.
Series này ghi lại câu chuyện từ một nền tảng production có thật. Từng cổng (port) của service, từng tham chiếu ADR, và từng ranh giới domain xuất hiện trong bài viết này đều phản ánh quá trình triển khai thực tế — không phải là lý thuyết suông.
Để tham khảo xem một siêu ứng dụng (super-app) trong khu vực đã phân rã những domain tương tự với khối lượng đơn hàng gấp 100 lần như thế nào, hãy xem Series Kiến trúc Shopee — đặc biệt hữu dụng khi bạn cần cân nhắc xem liệu số lượng service có nên tăng tỷ lệ thuận theo khối lượng giao dịch hay cấu trúc nhóm (team topology).
Bài viết này nằm trong Series Chuyển đổi sang Composable Commerce. Hãy xem toàn bộ mục lục để nắm bắt ngữ cảnh kiến trúc đầy đủ nhất.
Bạn cần hỗ trợ đánh giá rủi ro cho đợt chuyển đổi nền tảng sắp tới? → Đặt lịch Tư vấn Kiến trúc 1:1