Introduction
In this article, we walk through the design of a modern, scalable e-commerce system.
The goal: to create an architecture that is resilient, high-performing, extensible, and can handle the realities of a busy, dynamic online store.
Our task is to design:
- Product Management (browse/search/add products)
- Inventory Control (manage stock levels)
- Order Processing (cart, checkout)
- Payment Processing (external credit card integration)
- Shipping Management (select address, prepare shipment)
We use Domain-Driven Design (DDD) principles, Event-Driven Architecture (EDA), and a microservices approach for decoupling.
Let’s dive into the journey!
System Design: Step-by-Step
1. Defining Bounded Contexts
We divide our system into bounded contexts for clear responsibility separation:

Each service owns its own database and communicates via asynchronous events using a message broker like Kafka.
2. Modeling Aggregates and Entities
Following DDD, we model our key entities:

3. Event-Driven Checkout Flow
We implement a choreographed event-driven flow during checkout:

This flow allows services to scale independently while ensuring a resilient checkout process.
4. Commands, Events, and Event Payloads
Here are some sample event payloads:
OrderCheckoutStarted
{
"eventType": "OrderCheckoutStarted",
"orderId": "order-123",
"items": [
{ "productId": "prod-456", "quantity": 2 }
],
"customerId": "cust-789"
}
StockReserved
{
"eventType": "StockReserved",
"orderId": "order-123",
"success": true
}
PaymentSucceeded
{
"eventType": "PaymentSucceeded",
"paymentId": "pay-001",
"orderId": "order-123",
"amountPaid": 199.99
}
5. Database Schema Highlights
Each service manages its own tables:
Product Service – products Table
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Product unique ID |
name | VARCHAR(255) | Product name |
description | TEXT | Product description |
image_url | VARCHAR(500) | Image link |
price | DECIMAL(10,2) | Product price |
created_at | TIMESTAMP | Created timestamp |
updated_at | TIMESTAMP | Updated timestamp |
CREATE TABLE products (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
image_url VARCHAR(500),
price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
Searchable by name & description. Recommend indexing name, description for faster search.
Inventory Service – inventory_items Table
| Column | Type | Description |
|---|---|---|
product_id | UUID (PK, FK to products) | Links inventory to product |
available_quantity | INTEGER | How much stock is available |
reserved_quantity | INTEGER | Items temporarily reserved |
last_refill_at | TIMESTAMP | Last refill time |
CREATE TABLE inventory_items (
product_id UUID PRIMARY KEY,
available_quantity INTEGER NOT NULL,
reserved_quantity INTEGER DEFAULT 0,
last_refill_at TIMESTAMP DEFAULT now()
);
Handle stock updates atomically. available_quantity must decrease when reserved.
Order Service – orders Table
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Order ID |
customer_id | UUID | Customer reference |
status | ENUM(‘CREATED’, ‘PAID’, ‘CANCELLED’) | Current status |
total_amount | DECIMAL(10,2) | Order total price |
shipping_address | JSONB | Embedded shipping address |
created_at | TIMESTAMP | When order created |
updated_at | TIMESTAMP | Last update time |
Order Service – order_items Table
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Order item ID |
order_id | UUID (FK to orders) | Related order |
product_id | UUID (FK to products) | Ordered product |
quantity | INTEGER | Number of items |
price_per_unit | DECIMAL(10,2) | Price at the time of order |
Payment Service – payments Table
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Payment ID |
order_id | UUID (FK to orders) | Related order |
amount | DECIMAL(10,2) | Payment amount |
status | ENUM(‘PENDING’, ‘SUCCEEDED’, ‘FAILED’) | Payment state |
created_at | TIMESTAMP | Created timestamp |
Shipping Service – shipments Table
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Shipment ID |
order_id | UUID (FK to orders) | Related order |
address | JSONB | Shipping address |
shipment_status | ENUM(‘PENDING’, ‘SHIPPED’, ‘DELIVERED’, ‘FAILED’) | Tracking status |
created_at | TIMESTAMP | Created timestamp |
And so on for payments and shipments.
Use UUIDs everywhere for easy service boundaries (no clashing PKs).
Add indexes on product_id, order_id, customer_id for fast joins.
Partition orders table if traffic is huge (e.g., by creation year).
Use JSONB for flexible address storage (avoids needing a separate Address table).
6. Event-Driven Architecture:
Choreography vs Orchestration
Quick Definitions
| Term | Meaning |
|---|---|
| Choreography | Each service reacts to events and performs its action independently. No single service coordinates everything. |
| Orchestration | A central service (Orchestrator) controls the flow, asking each service what to do next, waiting for replies. |
Example with our E-Commerce Checkout
If Choreography:
Order ServiceemitsOrderCheckoutStarted.Inventory Servicelistens → reserves stock → emitsStockReserved.Payment Servicelistens → initiates payment → emitsPaymentSucceeded.Shipping Servicelistens → ships the order.- NO master controller.
Each service only listens and reacts.
✅ Pros:
- Low coupling between services.
- System is very scalable.
- Easy to add new behaviors (new services can listen to events).
❌ Cons:
- Harder to debug.
- No central place that knows the whole flow.
- Failure recovery (e.g., retry stock reservation) is complex.
If Orchestration:
Order Servicebecomes the orchestrator.- It:
- Calls
InventoryService.ReserveStock() - If success, calls
PaymentService.InitiatePayment() - If success, calls
ShippingService.InitiateShipment()
- Calls
- Synchronous or callback-based communication.
✅ Pros:
- Easy to debug (Order Service sees everything).
- Central control over flow and retries.
- Explicit logic for recovery paths.
❌ Cons:
- Tighter coupling between services.
- Scalability bottleneck if OrderService is overloaded.
- Harder to evolve new flows dynamically.
7. Why Choreography (Not Orchestration)?
In our case, choreography was better because:
- Low coupling = easier to evolve services
- High scalability (no single orchestrator bottleneck)
- Simpler for distributed transactions
Recovery is handled using:
- Dead letter queues (DLQ)
- Retry mechanisms
- Idempotent processing
8. Realistic Timeline: Checkout to Shipment
| Step | Service | Action | Latency |
|---|---|---|---|
| 1 | Order Service | Validate + emit event | ~50ms |
| 2 | Inventory Service | Reserve stock | ~100ms |
| 3 | Payment Service | Process payment | ~500ms |
| 4 | Shipping Service | Start shipping | ~100ms |
Total time to confirm checkout: ~850ms to 1.2 seconds.
Smooth, real-time user experience.
Conclusion
Through careful domain modeling, event-driven design, and clean service separation,
we created an e-commerce architecture that is:
- Highly scalable
- Resilient to failures
- Extendable for new features
- Aligned with real-world latency expectations
This approach isn’t just theoretical — it’s the foundation for modern platforms like Amazon, Shopify, and Walmart’s e-commerce engines.
Event-Driven + Domain-Driven = Futureproof Architecture.

Leave a Reply