DEV Community

abhishek pundir
abhishek pundir

Posted on

Building Scalable E-Commerce Platforms: Architecture & Best Practices

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)   │
└───────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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   │
└─────────────────┘
Enter fullscreen mode Exit fullscreen mode

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}`);
};
Enter fullscreen mode Exit fullscreen mode

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):

  1. CDN (CloudFlare) - Static assets, API responses
  2. Redis - Hot data (top 100 products, popular searches)
  3. Application memory - Lightweight cache for request lifetime
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Identify your bottlenecks - Where will traffic spike? Search? Checkout?
  2. Design for scale early - Database architecture matters
  3. Implement caching - It's not optional
  4. Use managed services - Don't write your own payment processor
  5. 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:


Learn more about building scalable e-commerce systems:

👉 Visit Vexio →

Connect with us to discuss your e-commerce architecture needs.

Top comments (0)