Blog/A Sample Microservice Architecture: E-Commerce Platform on Kubernetes

A Sample Microservice Architecture: E-Commerce Platform on Kubernetes

A.Yasar G.·Fri, Apr 10

Microservice Architecture: E-Commerce Platform on Kubernetes

Table of Contents

  1. High-Level Architecture
  2. Step 1: Ingress & Traffic Entry
  3. Step 2: Authentication with KeyCloak
  4. Step 3: Catalog Service
  5. Step 4: Basket Service
  6. Step 5: Order Service
  7. Step 6: Inventory Service
  8. Step 7: Event-Driven Communication with RabbitMQ
  9. Step 8: API Gateway
  10. Full End-to-End Flow
  11. Summary

1. High-Level Architecture

Below is the full picture of our Kubernetes cluster. Every box is a Pod running inside the cluster, traffic enters through the Ingress Controller, and all API calls pass through the API Gateway before reaching any microservice.


Step 1: Ingress & Traffic Entry

What is Ingress?

An Ingress is the single entry point into the Kubernetes cluster. It acts as a reverse proxy / load balancer that routes external HTTP/HTTPS requests to the correct internal Service based on the URL path or hostname.

How it works:

  1. The user opens the browser and navigates to https://shop.example.com.
  2. DNS resolves to the Ingress Controller's external IP.
  3. The Ingress Controller reads its routing rules and forwards the request to the appropriate Kubernetes Service.

Ingress Routing Rules (simplified):

The Ingress now only has 3 rules instead of 6. All API traffic goes to the Gateway, which handles the fine-grained routing internally.

PathTarget ServicePort
/frontend-service80
/auth/*keycloak-service8080
/api/*gateway-service8000

Step 2: Authentication with KeyCloak

What is KeyCloak?

KeyCloak is an open-source Identity and Access Management (IAM) solution. It handles user registration, login, token issuance (JWT), and role-based access control. Every microservice validates the JWT token before processing a request.

How it works:

  1. User clicks "Login" on the Frontend.
  2. Frontend redirects the user to KeyCloak's login page.
  3. User enters credentials.
  4. KeyCloak authenticates the user and issues a JWT Access Token + Refresh Token.
  5. Frontend stores the token and attaches it as Authorization: Bearer <token> to every subsequent API call.
  6. Each microservice validates the token (either by calling KeyCloak's introspection endpoint or by verifying the JWT signature locally using KeyCloak's public key).

KeyCloak Data Model (managed internally by KeyCloak)

KeyCloak manages its own database schema, but conceptually the key entities are:

Note: You don't create these tables manually -- KeyCloak manages them. You configure realms, users, roles, and clients through the KeyCloak Admin Console or its REST API.

Key points:

  • KeyCloak uses OpenID Connect (OIDC) protocol.
  • Tokens are short-lived (e.g., 5 minutes); the Refresh Token is used to get new Access Tokens without re-login.
  • Each microservice can verify tokens independently (no need to call KeyCloak on every request if using JWT signature verification).

Step 3: Catalog Service

Responsibility: Manages product listings, categories, pricing, and product search.

What it owns:

  • Product CRUD (Create, Read, Update, Delete)
  • Category management
  • Product search and filtering
  • Its own PostgreSQL database

Example API endpoints:

MethodEndpointDescription
GET/api/catalog/productsList all products (with pagination)
GET/api/catalog/products/:idGet a single product
GET/api/catalog/categoriesList categories
POST/api/catalog/productsCreate a product (admin only)
PUT/api/catalog/products/:idUpdate a product (admin only)

Database Tables (Catalog DB -- PostgreSQL)

Sample data in products table:

idnamepricecategory_idis_active
prod-001Wireless Mouse29.99cat-electronicstrue
prod-042Mechanical Keyboard59.99cat-electronicstrue
prod-108USB-C Hub39.99cat-accessoriestrue

Typical flow:

  1. User opens the homepage.
  2. Frontend calls GET /api/catalog/products?page=1&limit=20.
  3. Catalog Service queries its PostgreSQL database.
  4. Returns product list as JSON.
  5. Frontend renders the product grid.

Step 4: Basket Service

Responsibility: Manages per-user shopping carts. Because baskets are temporary and accessed frequently, this service uses Redis as its data store for fast read/write performance.

What it owns:

  • Add / remove / update items in the basket
  • Get current basket for a user
  • Its own Redis cache

Example API endpoints:

MethodEndpointDescription
GET/api/basketGet current user's basket
POST/api/basket/itemsAdd item to basket
PUT/api/basket/items/:idUpdate item quantity
DELETE/api/basket/items/:idRemove item from basket
DELETE/api/basketClear the entire basket

Data Structure (Basket -- Redis)

The Basket Service stores data in Redis as a hash per user. Since Redis is a key-value store, there are no relational tables -- the basket is stored as a JSON document.

Redis key pattern: basket:{userId}

{ "userId": "usr-1234", "items": [ { "productId": "prod-001", "quantity": 2, "unitPrice": 29.99 }, { "productId": "prod-042", "quantity": 1, "unitPrice": 59.99 } ], "createdAt": "2026-04-10T10:00:00Z", "updatedAt": "2026-04-10T14:25:00Z" }

Notice: The basket only stores productId -- it does NOT store product names, descriptions, or images. This is intentional. The Basket Service should not duplicate data owned by the Catalog Service. So how does the user see product names in their cart?

How the Basket Shows Product Names

This is a classic microservice challenge: the Basket owns cart state, but the Catalog owns product details. There are two common patterns to resolve this:

Pattern A: Frontend Aggregation (BFF Pattern)

The Frontend (or a Backend-For-Frontend layer) calls both services and joins the data client-side.

What the user sees after the merge:

ProductNameUnit PriceQtySubtotal
prod-001Wireless Mouse$29.992$59.98
prod-042Mechanical Keyboard$59.991$59.99
Total$119.97

Pattern B: Basket Service Calls Catalog Internally (Service-to-Service)

The Basket Service itself calls the Catalog Service via internal Kubernetes DNS to enrich the response before sending it to the Frontend.

Enriched API response from Basket Service:

{ "userId": "usr-1234", "items": [ { "productId": "prod-001", "productName": "Wireless Mouse", "imageUrl": "/images/wireless-mouse.jpg", "unitPrice": 29.99, "quantity": 2, "subtotal": 59.98 }, { "productId": "prod-042", "productName": "Mechanical Keyboard", "imageUrl": "/images/mech-keyboard.jpg", "unitPrice": 59.99, "quantity": 1, "subtotal": 59.99 } ], "totalAmount": 119.97 }

Which Pattern to Choose?

Pattern A (Frontend Aggregation)Pattern B (Service-to-Service)
CouplingLow -- services stay independentMedium -- Basket depends on Catalog
LatencyParallel calls from frontendSequential call chain
ComplexityFrontend does the merge logicBackend handles it transparently
ResilienceCart still shows IDs if Catalog is downBasket call fails if Catalog is down
Best forSPAs with smart frontendsMobile apps / thin clients

Typical flow:

  1. User clicks "Add to Cart" on a product.
  2. Frontend sends POST /api/basket/items with { productId, quantity, price }.
  3. Basket Service writes to Redis using the user's ID as the key.
  4. Returns updated basket.
  5. Frontend updates the cart icon badge.

Step 5: Order Service

Responsibility: Handles the checkout process, persists orders, and tracks order status. This is the most critical service because it orchestrates the transition from "basket" to "confirmed order."

What it owns:

  • Checkout / order creation
  • Order history
  • Order status tracking
  • Its own PostgreSQL database

Example API endpoints:

MethodEndpointDescription
POST/api/orders/checkoutCreate an order from the current basket
GET/api/ordersList user's order history
GET/api/orders/:idGet order details

Database Tables (Order DB -- PostgreSQL)

Important: The order_items table stores product_name and unit_price as snapshots. Even if the Catalog Service changes a product's name or price later, the order retains what the customer actually purchased. This is a fundamental pattern in e-commerce microservices.

Sample data in orders table:

iduser_idstatustotal_amountcreated_at
ord-9a8b7c6dusr-1234Confirmed119.972026-04-10 14:32:00

Sample data in order_items table:

idorder_idproduct_idproduct_nameunit_pricequantitysubtotal
oi-001ord-9a8b7c6dprod-001Wireless Mouse29.99259.98
oi-002ord-9a8b7c6dprod-042Mechanical Keyboard59.99159.99

Typical flow:

  1. User clicks "Checkout."
  2. Frontend sends POST /api/orders/checkout.
  3. Order Service fetches the basket (calls Basket Service internally or receives basket data from Frontend).
  4. Validates items and prices against the Catalog Service.
  5. Creates an order record in its PostgreSQL database with status Pending.
  6. Publishes an OrderPlaced event to RabbitMQ.
  7. Returns the order confirmation to the user.

Step 6: Inventory Service

Responsibility: Tracks stock levels for every product. Listens for events to reserve or release stock.

What it owns:

  • Stock level per product
  • Stock reservation and release
  • Low-stock alerts
  • Its own PostgreSQL database

Example API endpoints:

MethodEndpointDescription
GET/api/inventory/:productIdCheck stock for a product
PUT/api/inventory/:productIdUpdate stock level (admin)

Database Tables (Inventory DB -- PostgreSQL)

Sample data in inventory table:

idproduct_idtotal_stockreserved_stockavailable_stocklow_stock_threshold
inv-001prod-0015001248850
inv-002prod-042150314720
inv-003prod-10800010

Sample data in reservations table:

idorder_idproduct_idquantitystatusexpires_at
res-001ord-9a8b7c6dprod-0012Reserved2026-04-10 15:02:00
res-002ord-9a8b7c6dprod-0421Reserved2026-04-10 15:02:00

Note: product_id references the same product IDs from the Catalog Service, but there is no foreign key constraint because this is a completely separate database. Data consistency is maintained through events, not database joins.

Typical flow (event-driven):

  1. Inventory Service is subscribed to the OrderPlaced event on RabbitMQ.
  2. When it receives the event, it checks if enough stock exists.
  3. If yes: reserves stock (decrements quantity), publishes StockReserved event.
  4. If no: publishes StockInsufficient event (Order Service will then mark the order as failed).

Step 7: Event-Driven Communication with RabbitMQ

Why events?

In a microservice architecture, services should be loosely coupled. Instead of the Order Service directly calling the Inventory Service (synchronous HTTP), we use an event bus (RabbitMQ) so that:

  • Services don't need to know about each other.
  • If Inventory Service is temporarily down, the message waits in the queue.
  • Multiple services can react to the same event independently.

RabbitMQ sits in the middle as a message broker:

Events in the System

EventPublished ByConsumed ByPurpose
OrderPlacedOrder ServiceInventory Service, Basket ServiceNotify that a new order was created
StockReservedInventory ServiceOrder ServiceConfirm stock was successfully reserved
StockInsufficientInventory ServiceOrder ServiceNotify that stock is not available
OrderConfirmedOrder Service(Notification Service, etc.)Final confirmation after stock reserved
OrderCancelledOrder ServiceInventory ServiceRelease reserved stock

Event Payload Example (OrderPlaced)

{ "eventType": "OrderPlaced", "timestamp": "2026-04-10T14:32:00Z", "data": { "orderId": "ord-9a8b7c6d", "userId": "usr-1234", "items": [ { "productId": "prod-001", "quantity": 2, "price": 29.99 }, { "productId": "prod-042", "quantity": 1, "price": 59.99 } ], "totalAmount": 119.97 } }

Step 8: API Gateway

What is an API Gateway?

An API Gateway is an application-level proxy that sits between the Ingress and your microservices. While the Ingress handles basic path routing and TLS termination at the infrastructure level, the API Gateway adds application-aware cross-cutting concerns that would otherwise need to be duplicated in every single microservice.

Ingress vs. API Gateway -- why do we need both?

ConcernIngress (NGINX)API Gateway (Kong / Ocelot / YARP)
TLS terminationYesNo (already terminated)
Basic path routingYes (simple)Yes (advanced, with rewrites)
JWT validationNoYes
Rate limitingBasicPer-route, per-user, per-tier
Request/response transformationNoYes (add headers, rewrite bodies)
Circuit breakingNoYes
Request aggregationNoYes (combine multiple service calls)
Logging & metricsBasic access logsDetailed per-route observability
Canary / A-B routingLimitedYes
Protocol translationNoYes (REST to gRPC, etc.)

Think of it this way: Ingress is a bouncer at the door (checks the address, lets you in). The API Gateway is the receptionist inside (checks your ID, rate-limits how often you can visit, logs your visit, and directs you to the right department).

Where the Gateway Sits

What the Gateway Does on Every Request

Gateway Routing Configuration

The Gateway maintains a routing table that maps external paths to internal Kubernetes service addresses:

External RouteInternal TargetAuth RequiredRate LimitNotes
GET /api/catalog/**http://catalog-svc:8081/**Optional200/minPublic browsing, auth optional
GET /api/basket/**http://basket-svc:8082/**Yes100/minUser must be logged in
POST /api/basket/**http://basket-svc:8082/**Yes50/minStricter for writes
POST /api/orders/checkouthttp://order-svc:8083/checkoutYes10/minVery strict, prevents abuse
GET /api/orders/**http://order-svc:8083/**Yes100/minUser sees own orders only
GET /api/inventory/**http://inventory-svc:8084/**Yes (admin)50/minAdmin-only stock checks

Cross-Cutting Concerns the Gateway Handles

1. Centralized Authentication

Without the gateway, every microservice would need to implement JWT validation:

With the gateway, microservices trust that any request reaching them has already been authenticated. They receive a clean X-User-Id header and focus purely on business logic.

2. Circuit Breaking

If the Inventory Service goes down, the gateway can stop sending requests to it after a threshold of failures, returning a fast error instead of making the caller wait for timeouts:

3. Request Aggregation (BFF Pattern)

The gateway can combine multiple service calls into a single response, reducing frontend round-trips:

Popular API Gateway Options

GatewayLanguageBest ForK8s Native
KongLua / GoGeneral purpose, plugin ecosystemYes (Kong Ingress Controller)
OcelotC# (.NET).NET microservice stacksVia Deployment
YARPC# (.NET).NET, high performance reverse proxyVia Deployment
Spring Cloud GatewayJavaSpring Boot microservicesVia Deployment
EnvoyC++Service mesh (Istio), high perfYes (sidecar or standalone)
TraefikGoSimple config, auto-discoveryYes (Traefik Ingress)
AWS API GatewayManagedCloud-native, serverlessNo (external)

Full End-to-End Flow

This is the complete journey: a user browses products, adds them to the cart, checks out, and the system processes the order asynchronously.


Kubernetes Resource Overview

Every component above maps to Kubernetes resources:

ComponentK8s ResourceReplicasWhy
FrontendDeployment + Service2Stateless, scales horizontally
API GatewayDeployment + Service2Single entry for all API traffic, must be HA
KeyCloakDeployment + Service + PVC2Needs HA for auth
CatalogDeployment + Service3High read traffic
BasketDeployment + Service2Medium traffic, Redis-backed
OrderDeployment + Service3Critical path, needs availability
InventoryDeployment + Service2Event-driven, moderate load
PostgreSQLStatefulSet + PVC1-3Stateful, needs persistent storage
RedisStatefulSet + PVC1-3Stateful cache
RabbitMQStatefulSet + PVC3Message durability, clustering

Summary

Synchronous Communication (HTTP)

  • User -> Ingress -> Frontend: Static assets and SPA
  • Frontend -> KeyCloak: Authentication (OIDC)
  • Frontend -> API Gateway -> Microservices: REST API calls with JWT (gateway handles auth, rate limiting, routing)

Asynchronous Communication (RabbitMQ)

  • Order Service -> RabbitMQ -> Inventory Service: Stock reservation
  • Order Service -> RabbitMQ -> Basket Service: Basket cleanup
  • Inventory Service -> RabbitMQ -> Order Service: Stock confirmation

Key Principles Demonstrated

  1. Single Responsibility -- Each service owns one business domain and its own database.
  2. Database per Service -- No shared databases; services communicate via APIs or events.
  3. Ingress + API Gateway -- Ingress handles TLS termination and basic routing; the API Gateway adds JWT validation, rate limiting, circuit breaking, and request aggregation.
  4. Token-based Auth -- KeyCloak issues JWTs; each service validates independently.
  5. Event-Driven Architecture -- RabbitMQ decouples services for async workflows.
  6. Kubernetes-native -- Deployments for stateless services, StatefulSets for data stores, Services for discovery, Ingress for external access.