Every Magento team that decides to migrate to microservices faces the same first question: how many services?

The industry says 4–6. “Catalog service, Order service, Customer service, Inventory service, Payment service, and maybe CMS.” Every blog post, every conference talk converges on this list. It’s a reasonable starting point — and it’s wrong for serious e-commerce at scale.

The Composable Commerce Platform we’re documenting in this series has 21 microservices across 6 bounded context groups. That’s 3–4× the industry recommendation. This article explains why — with a complete Magento module → service mapping table, and the two counter-intuitive domain splits that Magento engineers almost always get wrong.

Answer-first: The number of services you need is determined by your team structure, scaling profile, and the boundaries of your business invariants — not by convention. At 10,000+ SKUs, 20+ warehouses, and 10,000+ orders/day with multiple independent engineering teams, 21 services is correct. The same platform for a 500-order/day shop should have 5–7.

1. Why DDD Boundaries, Not Database Tables

The most common decomposition mistake Magento engineers make is to look at Magento’s database tables and draw service boundaries around them.

“We have catalog_product_entity, so we need a Product Service.”
“We have sales_order, so we need an Order Service.”
“We have customer_entity, so we need a Customer Service.”

This produces anemic services — services that are just thin REST wrappers over database tables with no real domain logic. They don’t reduce coupling; they just move the coupling from Magento’s PHP code to API calls between services.

Domain-Driven Design takes a different approach: group code around business capabilities and their invariants, not data structures.

The test is: “Can this business rule be enforced within a single service’s database transaction?” If yes, the service boundary is correct. If the invariant requires coordination between multiple services, you need a Saga or Domain Event.

For example:

  • “An order cannot be placed if the customer’s email is unverified” → Order Service must read Customer Service data. This is a read query, acceptable via synchronous gRPC call.
  • “Stock cannot go negative across multiple warehouses” → Warehouse Service owns all stock. The invariant is within one service. Correct boundary.
  • “A coupon can only be used once per customer” → Promotion Service owns coupon redemption. The invariant is within one service. Correct boundary.

2. The 6 Bounded Context Groups

The platform organizes 21 services into 6 domain groups. Here is the complete mapping — including which Magento modules each service replaces:

🛒 Group 1: Commerce Flow (3 services)

ServiceHTTP PortgRPC PortReplaces Magento Modules
Checkout Service80049004Magento_Checkout, Magento_Quote
Order Service80019001Magento_Sales, Magento_SalesSequence
Payment Service80039003Magento_Payment, Magento_Braintree, Magento_Paypal

📦 Group 2: Product & Content (4 services)

ServiceHTTP PortgRPC PortReplaces Magento Modules
Catalog Service80059005Magento_Catalog, Magento_CatalogImportExport
Pricing Service80029002Magento_CatalogRule, Magento_CatalogPrice, Magento_Tax
Promotion Service80119011Magento_SalesRule, Magento_Coupon, Magento_Reward (partial)
Search Service80129012Magento_Search, Magento_CatalogSearch, Magento_Elasticsearch

🔐 Group 3: Identity & Access (3 services)

ServiceHTTP PortgRPC PortReplaces Magento Modules
Auth Service80139013Magento_Authorization, Magento_JwtUserToken
User Service80149014Magento_User, Magento_Backend (admin users)
Customer Service80069006Magento_Customer, Magento_CustomerBalance

🚚 Group 4: Logistics (3 services)

ServiceHTTP PortgRPC PortReplaces Magento Modules
Warehouse Service80089008Magento_InventoryAdminUi, Magento_InventoryApi, Magento_CatalogInventory
Fulfillment Service80099009Magento_InventoryShipping, Magento_InventorySourceDeductionApi
Shipping Service80109010Magento_Shipping, Magento_OfflineShipping, Magento_ShippingCore

🔄 Group 5: Post-Purchase (2 services)

ServiceHTTP PortgRPC PortReplaces Magento Modules
Return Service80159015Magento_Rma, Magento_SalesRuleSample
Loyalty Service80169016Magento_Reward, Magento_CustomerBalance (partial)

⚙️ Group 6: Platform & Operations (6 services)

ServiceHTTP PortgRPC PortRole
Gateway Service80009000API Gateway, JWT auth, rate limiting
Analytics Service80179017Purchase events, reporting (no Magento equivalent)
Review Service80189018Magento_Review, Magento_Rating
Notification Service80199019Magento_Email, Magento_SendFriend
Location Service80079007Geographic data, address validation
CommonOpsShared ops tooling, not deployed as a service

3. The Two Counter-Intuitive Splits

Split 1: Checkout ≠ Order

This is the decomposition that Magento engineers resist most strongly. In Magento, Magento_Checkout and Magento_Sales are technically separate modules — but they share database tables and are deeply coupled. Engineers naturally want to keep “checkout and order” as a single service.

Here’s why they must be separate:

Checkout Service manages temporary, expendable state:

  • Shopping cart lifecycle (add item, update qty, apply coupon, get totals)
  • Price revalidation at checkout time (preventing stale prices)
  • Stock reservation check (soft reserve, not permanent)
  • Shipping cost calculation orchestration
  • Payment method selection

Order Service manages permanent, critical state:

  • Order lifecycle with 8 states: PENDING → CONFIRMED → PAYMENT_CAPTURED → PROCESSING → FULFILLMENT_STARTED → SHIPPED → DELIVERED → COMPLETED
  • Order cancellation with compensation events
  • Return and refund state machine
  • Historical order data (never deleted, audited)

The invariant difference is decisive: Checkout can lose state without business consequence (an abandoned cart is fine). Order cannot lose state under any circumstances (a lost order is revenue lost and a customer complaint).

This separation enables independent scaling: during a flash sale, Order processing spikes after checkout completes. With separate services, you scale Order pods from 3 → 30 without touching Checkout pods. With a combined service, you scale everything.

Split 2: Pricing ≠ Promotion

Most architecture guides (and most SERP results) merge these into a single “Pricing & Promotions” service. The Composable Commerce Platform keeps them separate because they have fundamentally different characteristics:

Pricing Service:

  • Source of truth for base prices — what a product costs before any discount
  • Handles: base price, tiered pricing (B2B), multi-currency, tax calculation
  • Update frequency: low (product prices change infrequently)
  • Scaling profile: extremely high read rate (every catalog page call hits pricing)
  • Technology optimization: Redis cache with write-through, TTL = 1 hour

Promotion Service:

  • Applies discount rules — reducing prices, not defining them
  • Handles: coupon codes, BOGO rules, cart-level discounts, loyalty point redemptions
  • Update frequency: high (promotions created/expired constantly)
  • Scaling profile: event-driven — consumes order.cancelled events to reverse redemptions
  • Technology optimization: PostgreSQL for transactional coupon redemption tracking

The ADR-021 documents this explicitly: “Pricing Service owns the price; Warehouse Service owns the stock level; Promotion Service owns the discount application logic.” When ownership is clear, there are no distributed transaction problems for simple read operations.

4. DDD Principles Applied

Four explicit DDD principles from ADR-002:

1. Single Responsibility: Each service owns exactly one business domain. Order Service = order lifecycle only. Payment Service = payment transactions only. No service is a general-purpose utility.

2. Database Per Service (ADR-004): No direct database access between services. Enforced at the infrastructure level — each service has its own PostgreSQL instance. Cross-service data access is exclusively via gRPC (synchronous) or Dapr PubSub events (asynchronous).

3. Ubiquitous Language: Each domain uses consistent terminology. Warehouse Service uses “stock allocation” and “bin location”. Order Service uses “reservation” and “fulfillment request”. These terms don’t bleed across service boundaries.

4. Anti-Corruption Layer: The Gateway Service (port 8000) protects all microservices from external client schema changes. Frontend teams work against the Gateway’s REST API contract; internal service contracts evolve independently.

5. Why 21 Services Is Right at This Scale

ADR-002 explicitly justifies the service count against four business requirements:

RequirementImplication
10,000+ SKUs with dynamic EAV attributesCatalog + Pricing must be separated from Checkout (read profile vs write profile)
20+ warehouses with bin-level trackingWarehouse cannot be part of Catalog (inventory ownership is separate business domain)
10,000+ orders/day with independent payment gatewaysOrder, Payment, and Checkout must scale independently
Multiple engineering teams working in parallelService count ≈ team count × 2–3 (Conway’s Law)

ADR-002 also acknowledges the risk: “Service Explosion: Mitigated by clear domain boundaries and DDD principles.” For teams smaller than 20 engineers or platforms processing fewer than 2,000 orders/day, a 5–7 service decomposition is more appropriate.

6. Cross-Domain Communication Matrix

FromToProtocolWhen
CheckoutOrder, Payment, Pricing, ShippinggRPC (sync)Real-time checkout flow
OrderWarehouse, Fulfillment, CustomerDapr events (async)Post-order processing
WarehouseFulfillmentDapr events (async)Stock allocation triggers
PaymentExternal gateways (VNPay, MoMo)REST (sync)Payment processing
CatalogSearchDapr events (async)Search index sync
PromotionOrder, CustomerDapr events (async)Redemption reversal on cancellation

The pattern: synchronous gRPC for real-time user-facing flows, asynchronous events for post-transaction processing.

7. Service Maturity at Migration Start

Not all 21 services are production-ready simultaneously. The migration playbook in Part 6 uses service maturity as a proxy for migration sequencing:

🟢 Production-ready first (migrate in Phase 1 and 2): Auth, Customer, Catalog, Pricing, Gateway, Search, Location

🟡 Near-production (migrate in Phase 2 and 3): Order, Payment, Warehouse, Shipping, Return, Loyalty

🔵 Parallel development (complete during migration, deploy in Phase 3): Promotion, Fulfillment, Analytics, Review, Notification

Starting Strangler Fig with production-ready services reduces risk: if the migration approach fails, you’ve exposed it on lower-stakes domains first (catalog browsing) before it reaches Order creation.

What’s Next

You now have the domain map: 21 services, 6 groups, clear ownership boundaries, and the rationale for the two counter-intuitive splits.

The next question is tooling: how do you manage 21 Go services + 2 React frontends in a single repository, maintain consistent dependency versions, and run incremental builds in CI? That’s Part 2: Rush Monorepo Setup.

Or, if you want to go straight to the implementation layer: Part 3: Kratos v2 Internals shows exactly what a single Go microservice looks like from main.go to database query.


FAQ

Do I need exactly 21 services for a Magento-to-microservices migration?

No. 21 services is the right number for a platform processing 10,000+ orders/day with multiple independent engineering teams. For a shop with fewer than 2,000 orders/day and a team under 10 engineers, 5–7 services is more appropriate. The principle is: service count ≈ team count × 2–3, bounded by your actual scaling invariants.

Why must Checkout and Order be separate services?

Checkout manages temporary, expendable state (shopping cart). Order manages permanent, audited financial state. They have opposite failure tolerance: an abandoned cart is acceptable; a lost order is a revenue loss. Separating them also enables independent scaling — during flash sales, Order pods scale 10× while Checkout pods stay constant.

What happens if I don’t separate Pricing from Promotion?

Merging them creates a single service with two incompatible scaling profiles: Pricing reads happen on every catalog page (extremely high read rate, cache-friendly), while Promotion applies discount rules with transactional coupon deduplication (event-driven, write-heavy). A combined service forces you to over-provision resources for the lower-traffic workload and creates a tighter blast radius when either component fails.

How do I validate my bounded context boundaries before writing code?

Apply the transaction test: “Can this business rule be enforced within a single service’s database transaction?” If yes, the boundary is correct. If the rule requires coordinating two services, you need a Saga or a read query — and that coordination cost is the price you pay for keeping those services separate.


This series documents a real production platform. Every service port, every ADR reference, and every domain boundary in this article reflects the actual implementation — not a theoretical exercise.

For a comparison of how a regional super-app decomposed similar domains at 100× the order volume, see the Shopee Architecture Series — particularly useful when deciding whether your service count should scale with transaction volume or team topology.