Answer-first: Complete architectural blueprint of a Go 21-service e-commerce platform. Covers domain boundaries, traffic flow, and event-driven patterns.

What You’ll Learn That AI Won’t Tell You

  • Practical latency and memory metrics comparing an Envoy-based API Gateway to a custom Go reverse proxy under 100k concurrent connections.
  • How to tune circuit breaker thresholds (go-resiliency/breaker) to prevent premature service isolation during temporary network jitters.

When transitioning from a monolithic platform to a distributed microservice setup, the hardest question isn’t “How do we write the code?” — it’s “How do these moving parts talk to each other safely, and why is each boundary drawn exactly where it is?”

This post is the architectural anchor for the full composable commerce series. It presents the complete system blueprint and explains the reasoning behind each domain boundary. For deep-dives into specific layers, each section links to the dedicated post in the series.

The 6 Business Domains

A scalable e-commerce architecture divides into six core domains: Identity (Auth), Catalog (Search/Products), Cart (Checkout), Orders (Fulfillment), Inventory (Stock), and Promotions. Each domain owns its dedicated database to eliminate shared-state contention.

Before drawing a single line in a diagram, we bounded the ecosystem around Domain-Driven Design (DDD) principles. Every domain owns its own Postgres database. No cross-domain queries. Communication is exclusively through events or explicit gRPC contracts.

The 6 domains and their 21 services:

DomainServicesOwns
Commerce FlowCheckout, Order, PaymentThe money path — highest criticality
Product & ContentCatalog, Pricing, Promotion, SearchRead-heavy, sub-50ms latency
LogisticsWarehouse, Fulfillment, ShippingPhysical world integration
Post-PurchaseReturns, LoyaltyCustomer retention after delivery
Identity & AccessAuth, User, CustomerSecurity boundary between internal staff and external customers
Platform OperationsGateway, Analytics, NotificationShared infrastructure utilities

The reasoning behind separating User from Customer is worth stating explicitly: internal employees and external buyers have fundamentally different access patterns, data structures, and compliance requirements. Merging them creates a schema that serves neither well and creates a security surface where an internal RBAC bug could expose customer PII.

For the full breakdown of each service’s responsibilities, see Deconstructing the Ecosystem: Service Details by Domain.

The High-Level Architecture

The 21-service e-commerce blueprint uses Go for high-throughput edge APIs, Dapr for event pub/sub, and PostgreSQL for transactional storage. An Envoy-based API Gateway routes external traffic to the appropriate isolated domain service.

graph TD
    subgraph "🌐 External Clients"
        FE["Customer Website (Next.js)"]
        ADMIN["Admin Dashboard (React)"]
    end

    subgraph "🚪 API Gateway Layer"
        GW["Global Gateway\n(Auth · Rate Limit · Circuit Breaker · BFF)"]
    end

    subgraph "🔐 Identity & Access"
        AUTH["Auth Service\n(RS256 JWT · OAuth2 · MFA)"]
        USR["User Service\n(Internal RBAC)"]
        CUST["Customer Profile\n(LTV · Segmentation)"]
    end

    subgraph "📦 Product & Content"
        CAT["Catalog (PIM)\n(EAV → Normalized)"]
        PRC["Pricing Rules\n(Multi-currency · Tax)"]
        PROMO["Promotions\n(BOGO · Coupons)"]
        SEARCH["Search (CQRS)\n(Elasticsearch)"]
    end

    subgraph "🛒 Commerce Engine"
        CK["Checkout Orchestrator\n(Saga initiator)"]
        ORD["Order Lifecycle\n(8-state FSM)"]
        PAY["Payment Gateway\n(Stripe · VNPay · MoMo)"]
    end

    subgraph "🚚 Logistics & Fulfillment"
        WH["Warehouse (WMS)\n(OCC · Idempotency)"]
        FF["Fulfillment\n(Pick · Pack · Hand-off)"]
        SH["Shipping Hub\n(Carrier normalization)"]
    end

    subgraph "🎁 Post-Purchase"
        RET["Returns (RMA)\n(Restock · Refund gRPC)"]
        LR["Loyalty & Rewards\n(Outbox · Point ledger)"]
    end

    subgraph "📡 Platform Services"
        AN["Analytics Engine\n(Passive observer)"]
        NOTIF["Notification Hub\n(SendGrid · Twilio)"]
    end

    %% External traffic
    FE & ADMIN --> GW

    %% Synchronous paths (solid)
    GW --> AUTH & CUST & CAT & SEARCH & CK & ORD

    %% gRPC internal calls (dotted)
    CK -.->|"gRPC: CreateOrder"| ORD
    CK -.->|"gRPC: Authorize"| PAY
    RET -.->|"gRPC: Refund"| PAY

    %% Async event paths (double)
    ORD ==>|"order.confirmed"| WH
    ORD ==>|"order.paid"| FF
    FF ==>|"fulfillment.completed"| SH
    SH ==>|"shipping.delivered"| ORD
    ORD ==>|"order.cancelled"| LR & PROMO
    CAT ==>|"product.updated"| SEARCH & AN
    ORD ==>|"order.created"| AN & CUST & NOTIF

Solid lines = synchronous HTTP/gRPC. Double lines (==>) = asynchronous events via Dapr PubSub.

Traffic Anatomy: Three Distinct Flows

Traffic flows split into three paths: Synchronous HTTP/gRPC for user actions (checkout), Asynchronous Event-Driven Pub/Sub for background processing (order fulfillment), and CDC-based replication for search indexing and analytics aggregation.

Flow 1 — The Gateway Shield (Read Path)

All external traffic enters through the API Gateway. The Gateway enforces:

  • JWT validation — offloaded from downstream services; every request is authenticated at the edge
  • Rate limiting — IP-based and user-based, protecting against scraping and checkout abuse
  • Circuit breaking — if the Catalog service is degraded, the Gateway fails fast rather than stacking requests

Read-heavy operations (product listing, search, user profile) resolve here with sub-50ms latency because the Search service maintains a pre-indexed Elasticsearch CQRS read model, updated in near real-time via events from Catalog and Pricing.

Flow 2 — The Checkout Saga (Write Path)

The most critical and complex flow. When a customer checks out:

sequenceDiagram
    participant GW as API Gateway
    participant CK as Checkout
    participant WH as Warehouse
    participant PAY as Payment
    participant ORD as Order
    participant DAPR as Dapr PubSub

    GW->>CK: POST /checkout/confirm
    CK->>WH: gRPC: ReserveStock (TTL 15min)
    WH-->>CK: ✅ Reserved

    CK->>PAY: gRPC: Authorize payment
    PAY-->>CK: ✅ Authorized

    CK->>ORD: gRPC: CreateOrder
    ORD-->>CK: ✅ Order #10482 created

    ORD->>DAPR: Publish order.confirmed
    Note over DAPR: Async from here — Checkout returns 200 to user

    DAPR-->>WH: Deduct reserved stock permanently
    DAPR-->>CK: Trigger fulfillment workflow

Failure handling uses Compensating Transactions: if the Payment service declines the card after stock is reserved, the Checkout service triggers checkout.failed. The Warehouse service listens for this event and releases the reserved stock. No long-lived database transactions, no connection pool exhaustion under load.

For the complete implementation — including the Optimistic Concurrency Control SQL for inventory race conditions and the idempotency key pattern — see Architecting a 21-Service E-commerce Ecosystem with Golang & DDD.

Flow 3 — The Async Event Mesh (Post-Checkout)

Once order.paid fires into the Dapr event mesh, synchronous execution terminates from the customer’s perspective. Downstream services run in parallel:

  • Warehouse decrements permanently reserved stock
  • Fulfillment triggers the pick-pack-ship workflow
  • Analytics increments revenue dashboards
  • Customer updates LTV and purchase history
  • Notification fires the order confirmation email/SMS

A failure in any of these services (Notification is unreachable, Analytics is slow) does not affect the customer’s checkout experience or the order record. Isolation is enforced at the infrastructure level — each service owns its own database.

For the Dapr event naming conventions, Dead Letter Queue patterns, and idempotency design, see Mastering Event-Driven Architecture with Dapr Pub/Sub.

The Command Query Responsibility Segregation (CQRS) pattern separates read and write workloads. Product updates (Commands) write to PostgreSQL, which emits events via Kafka to update the read-optimized Elasticsearch index (Queries), preventing lock contention.

Product search deserves special attention because it solves a problem that Magento’s EAV model struggles with at scale: joining 5+ tables at query time to render a single product listing is too slow.

The Search service maintains an Elasticsearch read model that is rebuilt from events, not queried from the source database:

flowchart LR
    CAT["Catalog Service\n(source of truth)"] -- "catalog.product.updated" --> DAPR[Dapr PubSub]
    PRC["Pricing Service"] -- "pricing.price.updated" --> DAPR
    WH["Warehouse Service"] -- "warehouse.stock.changed" --> DAPR
    DAPR --> SW["Search Worker\n(subscriber)"]
    SW --> ES[("Elasticsearch\nFlat documents")]
    ES -- "sub-50ms reads" --> GW["API Gateway"]

When the Catalog team updates a product, they write to their own Postgres database and publish catalog.product.updated. The Search Worker receives the event and rebuilds the Elasticsearch document for that SKU — merging current price from the Pricing service and current stock from the Warehouse service into a single flat document.

No cron jobs. No full reindex. No stale data windows. Catalog, Pricing, and Warehouse each own their domain data; Search owns the read projection.

The Deployment Layer

The infrastructure deploys via GitOps using ArgoCD onto Kubernetes clusters. Services utilize Horizontal Pod Autoscaling (HPA) and node pools customized for workload profiles, with secrets injected securely via HashiCorp Vault.

The full 21-service platform is deployed via GitOps using ArgoCD with Kustomize overlays. No engineer touches the production cluster directly — all changes flow through Git, and ArgoCD enforces drift prevention via selfHeal: true on all production Applications.

For the complete GitOps setup — including the App-of-Apps pattern, Kustomize base/overlay structure, and the git revert rollback playbook — see GitOps at Scale: Orchestrating 21 Microservices with Kubernetes & ArgoCD.

Why Not a Distributed Monolith?

A distributed monolith occurs when microservices tightly couple via synchronous HTTP calls, meaning if the Auth service crashes, Checkout fails. True microservices use asynchronous event buses (Kafka) to ensure that downstream failures do not block upstream domains.

The most common failure mode when teams adopt microservices is building a Distributed Monolith: services that are deployed separately but remain tightly coupled through synchronous HTTP chains, shared databases, or shared deployment pipelines.

The architecture above avoids this through three hard rules:

  1. No cross-domain database access — ever. If the Order service needs product data, it either holds a denormalized copy or calls the Catalog service’s gRPC API.
  2. No synchronous calls in the async event path — once an event enters the Dapr mesh, it is processed independently. Event consumers do not call back into the producer.
  3. No shared deployment pipelines — each service has its own ArgoCD Application, its own container registry path, and its own release cycle. A bug in the Loyalty service cannot block a Checkout release.

For the full argument on when this complexity is justified — and when it isn’t — see Why You Should Migrate from Magento to Microservices (And When You Shouldn’t).

Series Navigation

PostWhat it covers
This postFull system blueprint, domain boundaries, traffic flows
Service Details by DomainEach service’s responsibilities and ownership
Golang DDD Deep-DiveKratos clean arch, Saga implementation, OCC, idempotency
Event-Driven with DaprNaming conventions, Saga pattern, DLQ design
GitOps with ArgoCDApp-of-Apps, Kustomize overlays, rollback playbook
Magento to Microservices: WhyDecision framework: when to migrate, when not to
Magento to Microservices: How3-phase Strangler Fig, Debezium CDC, bidirectional sync

FAQ

How does the API Gateway route traffic to 21+ backend services without becoming a bottleneck?

We use a lightweight, compiled gateway like Envoy or a custom Go gateway utilizing net/http reverse proxies. Path-based routing and JWT token verification are handled at the edge, while service-to-service communication is offloaded to a gRPC mesh (using Linkerd or Consul) for low-latency, mTLS-secured transport.

What strategies prevent cascading failures when a downstream microservice experiences latency spikes?

We implement the Circuit Breaker and bulkhead patterns at the service boundary. In Go, libraries like go-resiliency/breaker or Sentinel intercept outbound HTTP/gRPC client calls. If the failure rate exceeds 50%, the breaker trips immediately, returning a cached response or an architectural fallback rather than blocking goroutines.