Composable E-Commerce Migration: Overcoming Tech Debt
Answer-First: Migrating a monolithic e-commerce application (such as Magento) to composable architecture requires decomposing domains into 21 bounded contexts using Strangler Fig proxy routing with Envoy, real-time Debezium CDC for zero-drift database sync, and Go microservices built on Kratos v2 and Protobuf gRPC.
See the 21-service e-commerce architecture blueprint for the domain boundaries this migration targets.
- Why replacing a legacy PHP monolith (Magento) requires 21 DDD bounded contexts rather than naive 4–6 microservices.
- Strangler Fig routing configurations for Envoy that migrate traffic path-by-path from Magento to Go microservices without dropping active sessions.
- How to implement a double-write database sync listener in Go to prevent data drift during the multi-month migration window.
- Production Go microservice architecture using Kratos v2, Wire compile-time DI, and Protobuf Money types.
In theory, MACH (Microservices, API-first, Cloud-native, Headless) and Composable Commerce are the “holy grail” of the e-commerce industry. However, when systems scale to process millions of transactions, issues regarding data consistency, domain decomposition, and observability costs surface. This guide details the lessons and architectural patterns from migrating a monolithic Magento application into a production-grade 21-service Go microservices platform.
1. Why DDD Bounded Contexts: Decomposing Magento Modules
Every engineering team that decides to migrate a legacy Magento monolith faces the question: how many services? Naive approaches draw service boundaries around existing database tables (catalog_product_entity → Product Service, sales_order → Order Service). This creates anemic REST wrappers with tight database coupling moved to network calls.
Domain-Driven Design (DDD) groups code around business capabilities and invariants. A rule is enforced within a single service’s transaction boundary:
graph TD
subgraph Commerce Flow
Checkout[Checkout Service]
Order[Order Service]
Payment[Payment Service]
end
subgraph Product & Content
Catalog[Catalog Service]
Pricing[Pricing Service]
Promotion[Promotion Service]
Search[Search Service]
end
subgraph Identity & Access
Auth[Auth Service]
User[User Service]
Customer[Customer Service]
end
subgraph Logistics
Warehouse[Warehouse Service]
Fulfillment[Fulfillment Service]
Shipping[Shipping Service]
end
subgraph Post-Purchase
Return[Return Service]
Loyalty[Loyalty Service]
end
subgraph Platform & Operations
Gateway[Gateway Service]
Analytics[Analytics Service]
Review[Review Service]
Notification[Notification Service]
Location[Location Service]
end
Gateway --> Checkout
Gateway --> Catalog
Gateway --> Customer
Checkout --> Order
Checkout --> Pricing
Checkout --> Promotion
Checkout --> Payment
Order --> Warehouse
Warehouse --> Fulfillment
Fulfillment --> Shipping
Return --> Order
Loyalty --> Order
The Two Counter-Intuitive Domain Splits
- Checkout ≠ Order: Checkout Service manages temporary, expendable cart state. Order Service manages permanent, audited financial state across an 8-state lifecycle (
PENDING → CONFIRMED → PAYMENT_CAPTURED → PROCESSING → FULFILLMENT_STARTED → SHIPPED → DELIVERED → COMPLETED). Separating them enables independent pod scaling during peak events. - Pricing ≠ Promotion: Pricing Service owns base price calculations (high read rate, Redis cache TTL = 1h). Promotion Service applies rules and coupon redemptions (event-driven, transactional PostgreSQL deduplication).
2. Monorepo Architecture: Rush & Go Workspaces
Managing 21 Go microservices and 2 frontend applications (Next.js storefront and React admin panel) within a single codebase requires structured monorepo governance. Without strict dependency management and code sharing policies, multi-service repositories quickly degrade into fragmented codebases with divergent library versions and redundant utility functions.
composable-commerce/
├── rush.json ← Microsoft Rush config for TS apps & packages
├── common/config/rush/
│ └── common-versions.json ← Strict dependency version governance
├── apps/
│ ├── storefront/ ← Next.js customer storefront
│ └── admin-dashboard/ ← React admin panel
├── packages/
│ ├── ui-components/ ← Shared Tailwind design system
│ └── api-client/ ← TypeScript SDK generated from Go protos
└── services/ ← 21 Go microservices (Go module per service)
Protobuf contract compilation bridges Go microservices and frontend TypeScript code using buf generate. By configuring automated pre-commit hooks and CI checks, any API schema change in a Go service .proto file immediately regenerates TypeScript interfaces and client stubs. If a backend developer modifies a RPC parameter signature, frontend builds fail immediately during type-checking before code can be merged into production.
3. Core Backend: Golang + Kratos v2 Internals
Each Go microservice implements a strict 5-layer Clean Architecture layout using the Kratos v2 framework. The code implementation below illustrates the production configuration, error handling, and performance optimization techniques. Writing clean, performant code requires adhering to established software engineering patterns and defensive programming. The code implementation below illustrates the production configuration, memory efficiency rules, error handling strategies, and performance optimization techniques.
order-service/
├── api/order/v1/order.proto ← gRPC + HTTP proto contract
├── cmd/order-service/
│ ├── main.go ← Entry point
│ ├── wire.go ← Compile-time DI declarations
│ └── wire_gen.go ← Auto-generated DI initialization
├── internal/
│ ├── biz/ ← Pure business logic & aggregate roots
│ ├── data/ ← PostgreSQL & Redis repository implementations
│ ├── service/ ← Transport handlers (Proto → Biz mapping)
│ └── server/ ← gRPC (:9001) & HTTP (:8001) servers
Wire Compile-Time Dependency Injection
Unlike runtime reflection-based DI containers (such as Spring or Magento XML injection), Go Wire resolves component dependencies at compile time. wire.go specifies provider sets for databases, repositories, business logic use-cases, and transport servers. Running wire generates deterministic initialization code in wire_gen.go. If a developer forgets to provide a required dependency, go build fails immediately at compile time rather than crashing in production.
Protobuf Money Type & Cursor Pagination
- Exact Money Representation: Financial calculations must never use floating-point types (
float32/float64) due to binary rounding inaccuracies (e.g.,0.1 + 0.2 != 0.3). Microservices adopt Google’s standardgoogle.type.Moneyprotobuf contract (currency_code,units,nanos), executing monetary arithmetic using 64-bit integer fixed-point units in Go. - High-Performance Cursor Pagination: Standard SQL offset pagination (
OFFSET 10000 LIMIT 20) forces database engines to perform full index scans to discard 10,000 rows. Composable services implement seek-based cursor pagination (WHERE (created_at, id) < ($cursor_time, $cursor_id) ORDER BY created_at DESC, id DESC LIMIT 20), providing O(1) query execution speeds regardless of table depth across millions of historical orders.
4. The Real Bottleneck in Decoupling (Eventual Consistency)
When separating read-heavy search services (Elasticsearch/Meilisearch) from core transactional inventory services via an asynchronous event bus, temporary data replication lag (typically 200ms to 2s) is inevitable. The key technical guidelines, architectural requirements, and implementation steps are detailed in the breakdown below.
- The Concurrency Hazard: A customer views a product page showing 1 item remaining and clicks “Add to Cart”. The Inventory Service immediately reserves the item and decrements stock to 0. However, because the CDC event has not yet reached Elasticsearch, a second customer refreshes the search results, sees 1 item remaining, attempts to checkout, and encounters a frustrating failure.
- Practical Mitigation (Redis Lease Locking at BFF Layer): To eliminate checkout friction without coupling read services back to the primary database, the Backend-For-Frontend (BFF) gateway acquires a temporary lease lock in Redis upon inventory reservation. Subsequent checkout requests for that SKU check the Redis lease key before touching the database. If leased, the BFF returns instant backpressure response status to the second user, preventing phantom checkout attempts while Elasticsearch completes replication.
5. Solving Legacy Monolith Sync: The CDC Architecture
Avoiding application-level double writing prevents database drift, dual-phase commit lock contention, and network latency overhead. Change Data Capture (CDC) uses Debezium and Kafka Connect to tail the PostgreSQL Write-Ahead Log (WAL) or MySQL binary logs (binlogs) out-of-band. The key technical guidelines, architectural requirements, and implementation steps are detailed in the breakdown below.
- Debezium WAL Log Parsing: Debezium captures row-level insert, update, and delete mutations directly from the database engine log without modifying application code or incurring SQL query overhead on the legacy database.
- Kafka Event Streaming: Captured mutations are published to dedicated Apache Kafka topics (e.g.
legacy.inventory_stocks), preserving absolute global transaction order across table entities. - Eventual Consistency & Reconciliation: Downstream composable microservices consume binlog events asynchronously. While eventual consistency introduces a transient replication delay (typically under 100ms), downstream consumers enforce schema transformation and write to domain-isolated databases, guaranteeing eventual data convergence across legacy and modern platforms.
graph TD
subgraph Monolith Legacy
APP[Monolith App] -->|Write Data| DB[(Legacy DB MySQL/PG)]
end
subgraph CDC & Event Streaming
DB -.->|Read Binlog| DEB[Debezium CDC]
DEB -->|Publish Event| KAFKA[Kafka Event Bus]
end
subgraph Composable Services
KAFKA -->|Consume| OS[Order Service]
KAFKA -->|Consume| INV[Inventory Service]
OS --> O_DB[(New Order DB)]
INV --> I_DB[(New Inventory DB)]
end
6. The Phased Migration Roadmap & Envoy Routing
Transitioning a high-volume monolithic e-commerce application to composable microservices without taking downtime requires an incremental three-phase Strangler Fig pattern managed by Envoy Proxy. The breakdown below summarizes the primary technical criteria, phase milestones, and architectural recommendations. Selecting the optimal technical path requires evaluating workload scale, team operational maturity, and infrastructure costs across all deployment phases. The breakdown below summarizes the primary technical criteria, phase milestones, risk mitigations, and architectural recommendations.
- Phase 1: Read-Only Shadow Routing & Validation: Envoy mirrors production traffic to new composable microservices in shadow mode. Incoming read requests are evaluated side-by-side to compare response payloads and latency metrics without exposing users to potential bugs.
- Phase 2: Weighted Cluster Routing & Phased Traffic Shifting: Using Envoy runtime weighted cluster routing, engineering teams shift traffic incrementally (e.g., 1% → 5% → 25% → 100%) to new domain microservices while keeping fallback routes active to the legacy monolith.
- Phase 3: Full Cutover & Monolith Strangling: Once a domain route (such as
/api/v1/cartor/api/v1/catalog) achieves zero error rates over sustained peak traffic windows, the legacy monolith endpoints for that domain are formally deprecated and unmounted.
graph TD
Client[User Client] -->|HTTP Requests| Gateway[Ingress Gateway / Envoy]
subgraph Route Evaluation
Gateway -->|/api/v1/cart/*<br/>(Migrated)| CartCluster[Composable Cart Service]
Gateway -->|/api/v1/catalog/*<br/>(Migrated)| CatalogCluster[Composable Catalog Service]
Gateway -->|/*<br/>(Legacy Default)| MonoCluster[Monolith Legacy Cluster]
end
subgraph Composable Layer
CartCluster -->|1. Write| NewCartDB[(New Cart DB)]
CatalogCluster -->|Read/Write| NewCatalogDB[(New Catalog DB)]
end
subgraph Legacy Layer
MonoCluster -->|Write| LegacyDB[(Legacy DB)]
end
subgraph CDC Data Synchronization
NewCartDB -.->|2. Capture Binlogs| CDC[Debezium CDC]
CDC -->|3. Publish| Kafka[[Kafka Event Bus]]
Kafka -->|4. Consume| SyncWorker[Go Sync Worker]
SyncWorker -.->|5. Replicate| LegacyDB
end
style Gateway fill:#f9f,stroke:#333,stroke-width:2px
style MonoCluster fill:#fbb,stroke:#333,stroke-width:1px
style CartCluster fill:#bfb,stroke:#333,stroke-width:1px
style CatalogCluster fill:#bfb,stroke:#333,stroke-width:1px
Envoy Configuration Snippet
static_resources:
listeners:
- name: ingress_edge_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/api/v1/cart"
route:
cluster: composable_cart_service
timeout: 3s
- match:
prefix: "/api/v1/catalog"
route:
cluster: composable_catalog_service
timeout: 2s
- match:
prefix: "/"
route:
cluster: monolith_legacy_cluster
timeout: 10s
7. Go Event Listener for Parallel Database Sync
To process CDC event streams reliably at high throughput without risking data drift or duplicate processing during database synchronization, the Go synchronization worker implements a worker pool concurrent pipeline. The key technical guidelines, architectural requirements, and implementation steps are detailed in the breakdown below.
- Worker Pool Concurrency & Go Channels: Incoming Kafka messages are dispatched across a pool of concurrent Go worker goroutines via buffered channels. Each worker manages its own database transaction lifecycle, enabling parallel table row replication up to thousands of events per second.
- Idempotency Key Verification: Because Kafka guarantees at-least-once delivery, every CDC message carries a unique event UUID. Workers perform transactional idempotency checks against a
processed_eventstable before executingUPSERTstatements, preventing duplicate state mutations. - Transaction Rollback & Error Recovery: If a database error occurs during payload deserialization or SQL execution, the worker triggers an immediate transaction rollback, routes the failing event to a Dead Letter Queue (DLQ) topic, and dispatches alert metrics to OpenTelemetry.
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"time"
"github.com/segmentio/kafka-go"
)
type InventorySyncEvent struct {
SKU string `json:"sku"`
Quantity int `json:"quantity"`
WarehouseID int `json:"warehouse_id"`
EventTime time.Time `json:"event_time"`
EventUUID string `json:"event_uuid"`
}
type SyncWorker struct {
db *sql.DB
kafkaReader *kafka.Reader
}
func NewSyncWorker(db *sql.DB, brokers []string, topic string) *SyncWorker {
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: brokers,
GroupID: "inventory-sync-group",
Topic: topic,
MinBytes: 10e3,
MaxBytes: 10e6,
})
return &SyncWorker{db: db, kafkaReader: r}
}
func (w *SyncWorker) processSyncEvent(ctx context.Context, event InventorySyncEvent) error {
tx, err := w.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer tx.Rollback()
var processed bool
err = tx.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM processed_events WHERE event_uuid = $1)",
event.EventUUID).Scan(&processed)
if err != nil {
return fmt.Errorf("idempotency check failed: %w", err)
}
if processed {
return nil
}
_, err = tx.ExecContext(ctx, `
INSERT INTO inventory_stocks (sku, available_qty, warehouse_id, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (sku, warehouse_id)
DO UPDATE SET available_qty = EXCLUDED.available_qty, updated_at = NOW()`,
event.SKU, event.Quantity, event.WarehouseID)
if err != nil {
return fmt.Errorf("failed to upsert stock: %w", err)
}
_, err = tx.ExecContext(ctx,
"INSERT INTO processed_events (event_uuid, processed_at) VALUES ($1, NOW())",
event.EventUUID)
if err != nil {
return fmt.Errorf("failed to log processed event: %w", err)
}
return tx.Commit()
}
FAQ: Composable Commerce Migration
When should a business migrate to Composable Commerce?
When revenue hits the $5 million/year mark, or when the current monolithic platform severely bottlenecks release frequency. For small startups, packaged SaaS solutions remain more cost-effective.
Why do we need a BFF (Backend-For-Frontend)?
The BFF aggregates data from multiple microservices into a single API response for the Frontend, minimizing network calls and acting as a Circuit Breaker when backend services experience latency.
How long does the full migration take?
End-to-end: 14–19 weeks for a production store. Phase 1 (read-only) takes 2–3 weeks, Phase 2 (dual-write per domain) takes 4–6 weeks, and Phase 3 (full cutover) takes 8–10 weeks.
How much faster is gRPC than REST+JSON internally?
In production microservices, gRPC binary serialization and HTTP/2 multiplexing are 3–7× faster than REST+JSON text parsing, reducing internal multi-hop latency from ~90ms to ~15ms.
