Handling Black Friday traffic spikes, managing inventory across warehouses, processing thousands of orders per minute—e-commerce architecture is deceptively complex. Here's what we learned building platforms that scale.
The E-Commerce Challenge
The typical e-commerce platform needs to:
- Handle 10x-100x traffic spikes during sales
- Process payments reliably (no lost transactions)
- Keep inventory accurate across channels
- Search millions of products in milliseconds
- Deliver personalized recommendations
- Work flawlessly on mobile (70%+ of traffic)
Most platforms break under this pressure. We've learned why—and how to fix it.
Architecture: The Foundation
Separation of Concerns
Modern e-commerce isn't one monolith. It's multiple specialized services:
┌─────────────────────────────────────────────┐
│ CDN (CloudFlare, Akamai) │
│ (Static content, API caching) │
└────────────────────┬────────────────────────┘
│
┌─────────────────────────────────────────────┐
│ API Gateway (Kong, AWS API Gateway) │
│ (Rate limiting, auth) │
└────┬─────────────┬──────────────┬───────────┘
│ │ │
┌────▼──┐ ┌──────▼──┐ ┌───────▼──┐
│Product│ │ Checkout│ │ Inventory│
│Service│ │ Service │ │ Service │
└────┬──┘ └────┬────┘ └───┬──────┘
│ │ │
└────┬──────────┬───────────┘
│
┌────▼──────────────────────┐
│ Message Queue (Kafka) │
└────┬──────────────────────┘
│
┌────▼──────────────────────┐
│ Background Jobs/Workers │
│ (Email, Analytics, etc) │
└───────────────────────────┘
Key services:
- Product Service - catalog, search, recommendations
- Shopping Cart Service - quick add/remove, persistence
- Checkout Service - payment processing, order creation
- Inventory Service - stock levels, reservations
- Order Service - order management, fulfillment
- User Service - authentication, profiles
Each service:
- ✅ Has its own database
- ✅ Scales independently
- ✅ Deploys separately
- ✅ Owned by one team
Database Architecture
The Problem: One Database Won't Work
A single PostgreSQL instance will:
- Struggle with concurrent writes (lock contention)
- Slow down under complex joins
- Become a bottleneck
Our Solution: Database per Service + Read Replicas
┌─────────────────┐
│ Product Service │
├─────────────────┤
│ PostgreSQL │
│ (Products DB) │
│ + 2 Read Reps │
└────────┬────────┘
│
┌────────▼────────┐
│ Checkout Service│
├─────────────────┤
│ PostgreSQL │
│ (Orders DB) │
│ + 1 Read Rep │
└────────┬────────┘
│
┌────────▼────────┐
│ Inventory Svc │
├─────────────────┤
│ PostgreSQL │
│ (Inventory DB) │
│ + 3 Read Reps │
└─────────────────┘
Write to primary, read from replicas:
// Product Service - Heavy read workload
const getProducts = async (filters) => {
// Read from replica (faster, no write locks)
return await readReplicaDB.query(`
SELECT * FROM products
WHERE category = $1 AND price < $2
LIMIT 100
`, [filters.category, filters.maxPrice]);
};
const updatePrice = async (productId, newPrice) => {
// Write to primary (ensures consistency)
await primaryDB.query(
'UPDATE products SET price = $1 WHERE id = $2',
[newPrice, productId]
);
// Invalidate cache
await redis.del(`product:${productId}`);
};
Why this matters: Your product searches don't interfere with order processing.
Caching: The Secret Weapon
Most e-commerce performance issues = inadequate caching.
Our caching strategy (in order):
- CDN (CloudFlare) - Static assets, API responses
- Redis - Hot data (top 100 products, popular searches)
- Application memory - Lightweight cache for request lifetime
- Database - Last resort
// Multi-layer cache example
async getProduct(productId: string) {
// Layer 1: Memory cache (ultra-fast)
const cached = this.memoryCache.get(`product:${productId}`);
if (cached) return cached;
// Layer 2: Redis (fast, shared across servers)
let product = await this.redis.get(`product:${productId}`);
if (product) {
this.memoryCache.set(`product:${productId}`, product, { ttl: 300 });
return product;
}
// Layer 3: Database (slow)
product = await this.db.products.findById(productId);
// Cache it
await this.redis.setex(
`product:${productId}`,
3600, // 1 hour TTL
JSON.stringify(product)
);
return product;
}
Cache invalidation on updates:
async updateProduct(productId: string, updates: any) {
// Update database
const product = await this.db.products.update(productId, updates);
// Invalidate all cache layers
this.memoryCache.delete(`product:${productId}`);
await this.redis.del(`product:${productId}`);
// Notify all services via event
await this.messageQueue.publish('product.updated', { productId });
return product;
}
Search: Beyond Basic SQL
You can't SELECT * FROM products WHERE at scale. You need specialized search.
Elasticsearch for Product Search
// Index products in Elasticsearch
const indexProduct = async (product) => {
await elasticsearch.index({
index: 'products',
id: product.id,
body: {
name: product.name,
description: product.description,
category: product.category,
price: product.price,
tags: product.tags,
rating: product.averageRating,
inStock: product.quantity > 0
}
});
};
// Search with facets
const searchProducts = async (query, filters) => {
const results = await elasticsearch.search({
index: 'products',
body: {
query: {
bool: {
must: [
{ multi_match: {
query: query,
fields: ['name^2', 'description']
}}
],
filter: [
{ range: { price: { gte: filters.minPrice, lte: filters.maxPrice } } },
{ term: { category: filters.category } },
{ term: { inStock: true } }
]
}
},
aggs: {
categories: { terms: { field: 'category' } },
priceRange: { range: { field: 'price', ranges: [...] } }
}
}
});
return {
products: results.hits.hits.map(h => h._source),
facets: results.aggregations
};
};
Why Elasticsearch > SQL for search:
- ✅ Full-text search with typo tolerance
- ✅ Faceted search (filters, categories)
- ✅ Relevance ranking
- ✅ Autocomplete with suggestions
- ✅ Scales to 100M+ products
Payment Processing: Get This Right
Never handle credit cards yourself. Use PCI-compliant payment processors.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
async processPayment(order: Order, paymentMethod: string) {
try {
// Create payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: order.totalAmount * 100, // cents
currency: 'usd',
payment_method: paymentMethod,
confirmation_method: 'manual',
confirm: true,
metadata: { orderId: order.id }
});
if (paymentIntent.status === 'succeeded') {
// Update order status
await this.orderService.markPaid(order.id);
// Publish event for downstream processing
await this.messageQueue.publish('payment.succeeded', {
orderId: order.id,
amount: order.totalAmount,
timestamp: new Date()
});
return { success: true, transactionId: paymentIntent.id };
}
} catch (error) {
// Payment failed - don't retry automatically
await this.orderService.markFailed(order.id, error.message);
throw error;
}
}
Key principles:
- Use established payment gateways (Stripe, PayPal, Square)
- Store payment tokens, never raw card data
- Implement idempotency (prevent double-charging)
- Handle webhooks for async confirmation
Inventory Management: The Tricky Part
Inventory seems simple. It's not.
The problem: Race conditions.
Customer A: Check stock → 1 item left
Customer B: Check stock → 1 item left
Customer A: Buy → Reserved
Customer B: Buy → OVERSOLD! (2 sold, 1 in stock)
Solution: Pessimistic Locking
async reserveInventory(orderId: string, items: OrderItem[]) {
const connection = await this.db.getConnection();
try {
// Lock inventory rows for this transaction
await connection.query('BEGIN TRANSACTION');
for (const item of items) {
// Lock the row
const stock = await connection.query(
'SELECT * FROM inventory WHERE sku = $1 FOR UPDATE',
[item.sku]
);
if (stock.quantity < item.quantity) {
throw new Error(`Insufficient stock for ${item.sku}`);
}
// Deduct stock
await connection.query(
'UPDATE inventory SET quantity = quantity - $1 WHERE sku = $2',
[item.quantity, item.sku]
);
// Create reservation record
await connection.query(
'INSERT INTO reservations (order_id, sku, quantity) VALUES ($1, $2, $3)',
[orderId, item.sku, item.quantity]
);
}
await connection.query('COMMIT');
} catch (error) {
await connection.query('ROLLBACK');
throw error;
}
}
Result: Only one customer gets the item. Guaranteed.
Performance Optimization Checklist
Frontend
- ✅ Lazy load images (Intersection Observer)
- ✅ Code split by route
- ✅ Compress images (WebP, AVIF)
- ✅ Minimize JavaScript bundles
- ✅ Service worker for offline support
API Layer
- ✅ GraphQL (over-fetching prevention) or REST with sparse fields
- ✅ API response compression (gzip)
- ✅ HTTP/2 or HTTP/3
- ✅ Proper cache headers (ETag, Last-Modified)
Database
- ✅ Indexes on frequently filtered columns
- ✅ Denormalization for common queries
- ✅ Partitioning for large tables (orders by date)
- ✅ Connection pooling
Infrastructure
- ✅ Auto-scaling (CPU/memory-based)
- ✅ Load balancing across regions
- ✅ Database read replicas
- ✅ Content delivery networks (CDN)
Real-World Metrics
Our e-commerce platform handles:
| Metric | Value |
|---|---|
| Peak traffic | 500K requests/second |
| Product catalog | 50M+ items |
| Search latency (p95) | 150ms |
| Checkout success rate | 99.7% |
| Page load time (p75) | 1.2 seconds |
| Uptime | 99.99% |
Common Pitfalls to Avoid
1. Synchronous Everything
❌ Wrong: Wait for email to send before confirming order
✅ Right: Queue email job, confirm order immediately
2. No Circuit Breakers
❌ Wrong: Call payment processor, wait forever if it's down
✅ Right: Fail fast, queue for retry, notify customer
3. Undersizing for Peaks
❌ Wrong: Size for average load (crashes on sale day)
✅ Right: Plan for 10x peak load, use auto-scaling
4. Trusting User Input
❌ Wrong: ORDER BY ${req.query.sortBy}
✅ Right: Validate against whitelist, sanitize everything
5. No Monitoring
❌ Wrong: "It works for me" in development
✅ Right: Monitor latency, errors, queue depth 24/7
Tech Stack Summary
Frontend
- React/Vue/Next.js with TypeScript
- TailwindCSS or Chakra UI
- React Query for state management
Backend
- Node.js (Express, Fastify) or Python (FastAPI)
- PostgreSQL + Redis
- Elasticsearch for search
- Apache Kafka or RabbitMQ for events
Infrastructure
- Docker + Kubernetes
- AWS/GCP/Azure
- CloudFlare CDN
- Stripe/PayPal for payments
Why Architecture Matters
A poorly architected e-commerce platform will:
- ❌ Lose sales during traffic spikes
- ❌ Have inventory inconsistencies
- ❌ Suffer from slow search
- ❌ Frustrate customers with checkout delays
A well-architected platform will:
- ✅ Handle 10x traffic seamlessly
- ✅ Keep inventory accurate
- ✅ Search 50M+ products in 150ms
- ✅ Convert more visitors to customers
The difference? System design.
Building vs. Buying
You might be thinking: "Can't I just use Shopify or WooCommerce?"
You can—if:
- Your business fits the standard mold
- You don't have custom workflows
- You can live with vendor limitations
- You want minimal technical overhead
You need custom: if you have:
- Complex fulfillment workflows
- Multi-channel inventory
- Unique product attributes
- Specific performance requirements
- Integration needs
At Vexio, we build custom e-commerce platforms for companies that outgrew their off-the-shelf solutions. We handle the architecture, scaling, payment integrations, and complex workflows so you can focus on growth.
If you're building or scaling an e-commerce platform, explore how Vexio can help with enterprise e-commerce solutions tailored to your business.
Next Steps
If you're building an e-commerce platform:
- Identify your bottlenecks - Where will traffic spike? Search? Checkout?
- Design for scale early - Database architecture matters
- Implement caching - It's not optional
- Use managed services - Don't write your own payment processor
- Monitor everything - You can't fix what you can't measure
Questions?
Building an e-commerce platform? Struggling with scaling?
Drop a comment or reach out. We've solved these problems and love talking architecture.
Related Resources:
- Designing Data-Intensive Applications - Martin Kleppmann
- High Scalability Blog
- Stripe Documentation
- Elasticsearch Guide
- PostgreSQL Performance Tuning
Learn more about building scalable e-commerce systems:
👉 Visit Vexio →
Connect with us to discuss your e-commerce architecture needs.
Top comments (0)