A Sample Microservice Architecture: E-Commerce Platform on Kubernetes
Microservice Architecture: E-Commerce Platform on Kubernetes
Table of Contents
- High-Level Architecture
- Step 1: Ingress & Traffic Entry
- Step 2: Authentication with KeyCloak
- Step 3: Catalog Service
- Step 4: Basket Service
- Step 5: Order Service
- Step 6: Inventory Service
- Step 7: Event-Driven Communication with RabbitMQ
- Step 8: API Gateway
- Full End-to-End Flow
- 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:
- The user opens the browser and navigates to
https://shop.example.com. - DNS resolves to the Ingress Controller's external IP.
- 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.
| Path | Target Service | Port |
|---|---|---|
/ | frontend-service | 80 |
/auth/* | keycloak-service | 8080 |
/api/* | gateway-service | 8000 |
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:
- User clicks "Login" on the Frontend.
- Frontend redirects the user to KeyCloak's login page.
- User enters credentials.
- KeyCloak authenticates the user and issues a JWT Access Token + Refresh Token.
- Frontend stores the token and attaches it as
Authorization: Bearer <token>to every subsequent API call. - 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:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/catalog/products | List all products (with pagination) |
| GET | /api/catalog/products/:id | Get a single product |
| GET | /api/catalog/categories | List categories |
| POST | /api/catalog/products | Create a product (admin only) |
| PUT | /api/catalog/products/:id | Update a product (admin only) |
Database Tables (Catalog DB -- PostgreSQL)
Sample data in products table:
| id | name | price | category_id | is_active |
|---|---|---|---|---|
prod-001 | Wireless Mouse | 29.99 | cat-electronics | true |
prod-042 | Mechanical Keyboard | 59.99 | cat-electronics | true |
prod-108 | USB-C Hub | 39.99 | cat-accessories | true |
Typical flow:
- User opens the homepage.
- Frontend calls
GET /api/catalog/products?page=1&limit=20. - Catalog Service queries its PostgreSQL database.
- Returns product list as JSON.
- 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:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/basket | Get current user's basket |
| POST | /api/basket/items | Add item to basket |
| PUT | /api/basket/items/:id | Update item quantity |
| DELETE | /api/basket/items/:id | Remove item from basket |
| DELETE | /api/basket | Clear 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:
| Product | Name | Unit Price | Qty | Subtotal |
|---|---|---|---|---|
| prod-001 | Wireless Mouse | $29.99 | 2 | $59.98 |
| prod-042 | Mechanical Keyboard | $59.99 | 1 | $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) | |
|---|---|---|
| Coupling | Low -- services stay independent | Medium -- Basket depends on Catalog |
| Latency | Parallel calls from frontend | Sequential call chain |
| Complexity | Frontend does the merge logic | Backend handles it transparently |
| Resilience | Cart still shows IDs if Catalog is down | Basket call fails if Catalog is down |
| Best for | SPAs with smart frontends | Mobile apps / thin clients |
Typical flow:
- User clicks "Add to Cart" on a product.
- Frontend sends
POST /api/basket/itemswith{ productId, quantity, price }. - Basket Service writes to Redis using the user's ID as the key.
- Returns updated basket.
- 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:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/orders/checkout | Create an order from the current basket |
| GET | /api/orders | List user's order history |
| GET | /api/orders/:id | Get order details |
Database Tables (Order DB -- PostgreSQL)
Important: The
order_itemstable storesproduct_nameandunit_priceas 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:
| id | user_id | status | total_amount | created_at |
|---|---|---|---|---|
ord-9a8b7c6d | usr-1234 | Confirmed | 119.97 | 2026-04-10 14:32:00 |
Sample data in order_items table:
| id | order_id | product_id | product_name | unit_price | quantity | subtotal |
|---|---|---|---|---|---|---|
oi-001 | ord-9a8b7c6d | prod-001 | Wireless Mouse | 29.99 | 2 | 59.98 |
oi-002 | ord-9a8b7c6d | prod-042 | Mechanical Keyboard | 59.99 | 1 | 59.99 |
Typical flow:
- User clicks "Checkout."
- Frontend sends
POST /api/orders/checkout. - Order Service fetches the basket (calls Basket Service internally or receives basket data from Frontend).
- Validates items and prices against the Catalog Service.
- Creates an order record in its PostgreSQL database with status
Pending. - Publishes an
OrderPlacedevent to RabbitMQ. - 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:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/inventory/:productId | Check stock for a product |
| PUT | /api/inventory/:productId | Update stock level (admin) |
Database Tables (Inventory DB -- PostgreSQL)
Sample data in inventory table:
| id | product_id | total_stock | reserved_stock | available_stock | low_stock_threshold |
|---|---|---|---|---|---|
inv-001 | prod-001 | 500 | 12 | 488 | 50 |
inv-002 | prod-042 | 150 | 3 | 147 | 20 |
inv-003 | prod-108 | 0 | 0 | 0 | 10 |
Sample data in reservations table:
| id | order_id | product_id | quantity | status | expires_at |
|---|---|---|---|---|---|
res-001 | ord-9a8b7c6d | prod-001 | 2 | Reserved | 2026-04-10 15:02:00 |
res-002 | ord-9a8b7c6d | prod-042 | 1 | Reserved | 2026-04-10 15:02:00 |
Note:
product_idreferences 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):
- Inventory Service is subscribed to the
OrderPlacedevent on RabbitMQ. - When it receives the event, it checks if enough stock exists.
- If yes: reserves stock (decrements quantity), publishes
StockReservedevent. - If no: publishes
StockInsufficientevent (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
| Event | Published By | Consumed By | Purpose |
|---|---|---|---|
OrderPlaced | Order Service | Inventory Service, Basket Service | Notify that a new order was created |
StockReserved | Inventory Service | Order Service | Confirm stock was successfully reserved |
StockInsufficient | Inventory Service | Order Service | Notify that stock is not available |
OrderConfirmed | Order Service | (Notification Service, etc.) | Final confirmation after stock reserved |
OrderCancelled | Order Service | Inventory Service | Release 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?
| Concern | Ingress (NGINX) | API Gateway (Kong / Ocelot / YARP) |
|---|---|---|
| TLS termination | Yes | No (already terminated) |
| Basic path routing | Yes (simple) | Yes (advanced, with rewrites) |
| JWT validation | No | Yes |
| Rate limiting | Basic | Per-route, per-user, per-tier |
| Request/response transformation | No | Yes (add headers, rewrite bodies) |
| Circuit breaking | No | Yes |
| Request aggregation | No | Yes (combine multiple service calls) |
| Logging & metrics | Basic access logs | Detailed per-route observability |
| Canary / A-B routing | Limited | Yes |
| Protocol translation | No | Yes (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 Route | Internal Target | Auth Required | Rate Limit | Notes |
|---|---|---|---|---|
GET /api/catalog/** | http://catalog-svc:8081/** | Optional | 200/min | Public browsing, auth optional |
GET /api/basket/** | http://basket-svc:8082/** | Yes | 100/min | User must be logged in |
POST /api/basket/** | http://basket-svc:8082/** | Yes | 50/min | Stricter for writes |
POST /api/orders/checkout | http://order-svc:8083/checkout | Yes | 10/min | Very strict, prevents abuse |
GET /api/orders/** | http://order-svc:8083/** | Yes | 100/min | User sees own orders only |
GET /api/inventory/** | http://inventory-svc:8084/** | Yes (admin) | 50/min | Admin-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-Idheader 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
| Gateway | Language | Best For | K8s Native |
|---|---|---|---|
| Kong | Lua / Go | General purpose, plugin ecosystem | Yes (Kong Ingress Controller) |
| Ocelot | C# (.NET) | .NET microservice stacks | Via Deployment |
| YARP | C# (.NET) | .NET, high performance reverse proxy | Via Deployment |
| Spring Cloud Gateway | Java | Spring Boot microservices | Via Deployment |
| Envoy | C++ | Service mesh (Istio), high perf | Yes (sidecar or standalone) |
| Traefik | Go | Simple config, auto-discovery | Yes (Traefik Ingress) |
| AWS API Gateway | Managed | Cloud-native, serverless | No (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:
| Component | K8s Resource | Replicas | Why |
|---|---|---|---|
| Frontend | Deployment + Service | 2 | Stateless, scales horizontally |
| API Gateway | Deployment + Service | 2 | Single entry for all API traffic, must be HA |
| KeyCloak | Deployment + Service + PVC | 2 | Needs HA for auth |
| Catalog | Deployment + Service | 3 | High read traffic |
| Basket | Deployment + Service | 2 | Medium traffic, Redis-backed |
| Order | Deployment + Service | 3 | Critical path, needs availability |
| Inventory | Deployment + Service | 2 | Event-driven, moderate load |
| PostgreSQL | StatefulSet + PVC | 1-3 | Stateful, needs persistent storage |
| Redis | StatefulSet + PVC | 1-3 | Stateful cache |
| RabbitMQ | StatefulSet + PVC | 3 | Message 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
- Single Responsibility -- Each service owns one business domain and its own database.
- Database per Service -- No shared databases; services communicate via APIs or events.
- Ingress + API Gateway -- Ingress handles TLS termination and basic routing; the API Gateway adds JWT validation, rate limiting, circuit breaking, and request aggregation.
- Token-based Auth -- KeyCloak issues JWTs; each service validates independently.
- Event-Driven Architecture -- RabbitMQ decouples services for async workflows.
- Kubernetes-native -- Deployments for stateless services, StatefulSets for data stores, Services for discovery, Ingress for external access.
