12,000+ Businesses. Here's What I Got Wrong Building Their E-Commerce Stack.

## Why Most E-Commerce Projects Fail

Building an online store sounds simple until you actually do it. Payment processing, inventory management, multi-currency support, shipping calculations, tax compliance — the complexity compounds fast.

I've built e-commerce platforms that serve 12,000+ businesses across multiple countries. Here are the hard-won lessons that guides and tutorials won't teach you.

## Architecture Decisions That Matter

### Monolith First, Always

Every e-commerce platform I've built started as a monolith. Not because microservices are bad, but because:

- You ship faster with a monolith
- Debugging is simpler
- Deployment is one step
- You don't know your bottlenecks yet

Split into services only when you have a specific scaling problem. "We might need to scale" is not a specific problem.

### Database Design Is Everything

E-commerce has inherently relational data. Products belong to categories. Orders contain line items. Users have addresses. Carts reference products. This is where PostgreSQL shines.

Key tables I always start with:
- `users` — authentication and profile
- `products` — with JSONB for flexible attributes
- `categories` — hierarchical with parent references
- `orders` — with status tracking
- `order_items` — line items with price snapshot (never reference current product price)
- `addresses` — separate table, linked to users and orders

**Critical rule:** Always store the price at the time of purchase in the order item. Product prices change. An order placed at the old price should reflect the old price forever.

### Cart Strategy

Two approaches, both valid:

1. **Database carts** — Persistent, survive browser closures, work across devices. More server load.
2. **Local storage carts** — Fast, no server calls, but lost if the user clears their browser.

I typically use a hybrid: local storage for anonymous users, database cart for logged-in users, with a merge when they log in.

## Multi-Currency: Harder Than You Think

Supporting multiple currencies isn't just about multiplying prices by exchange rates.

### What you actually need:

- **Real-time exchange rates** — Use an API like Open Exchange Rates or Fixer.io. Cache rates for 1 hour.
- **Price rounding rules** — Different currencies have different rounding conventions. Japanese Yen has no decimal places. Kuwaiti Dinar has 3.
- **Display formatting** — `$1,234.56` vs `1.234,56 €` vs `¥1,234`. Use `Intl.NumberFormat`.
- **Settlement currency** — You might display prices in EUR but settle in USD. Your payment processor handles conversion, but you need to track both.

### The approach that worked:

Store all prices in the smallest currency unit (cents) in a base currency (USD). Convert for display only. This avoids floating-point issues and keeps your accounting clean.

## Checkout Optimization: Where Revenue Lives

A 40% reduction in checkout abandonment means 40% more revenue. Here's what moves the needle:

### 1. Guest Checkout Is Non-Negotiable

Forcing account creation before purchase kills conversions. Let people buy first, create an account after (with their order email pre-filled).

### 2. Minimize Form Fields

Every field you add increases abandonment. The minimum for checkout:
- Email
- Shipping address
- Payment info

Name can come from the payment method. Phone number can be optional. Don't ask for anything you don't strictly need.

### 3. Show Progress

A simple 3-step indicator (Shipping → Payment → Review) reduces anxiety and tells users how much longer the process takes.

### 4. Address Autocomplete

Use Google Places API or a similar service. Typing a full address is painful on mobile. Autocomplete reduces errors and speeds up the process significantly.

### 5. Multiple Payment Methods

In Europe, bank transfers and local payment methods are common. In India, UPI is dominant. In the USA, credit cards and PayPal lead. Support what your users actually use, not just Stripe's default card form.

## Payment Integration: The Careful Parts

### Webhook-First Architecture

Never rely on the client to confirm payment. Your flow should be:

1. Client initiates payment
2. Redirect to payment provider
3. Payment provider sends a webhook to your server
4. Your server updates the order status
5. Client polls or receives confirmation

The webhook is the source of truth. Client-side confirmations can be faked, interrupted, or lost.

### Idempotency

Payment webhooks can be delivered more than once. Your handler must be idempotent — processing the same webhook twice should not charge the customer twice or create duplicate orders.

Use a unique `idempotency_key` (usually the payment intent ID) and check if you've already processed it before taking action.

### Refund Handling

Build refund logic from day one, even if you think you won't need it. You will. The data model should track:
- Original payment reference
- Refund amount (partial refunds are common)
- Reason
- Status
- Who initiated it

## Inventory Management

### Prevent Overselling

When 100 people are buying the last item simultaneously, you need to prevent overselling. Two approaches:

1. **Pessimistic locking** — Lock the inventory row when someone adds to cart. Simple but can cause contention.
2. **Optimistic locking** — Check inventory at checkout time. If it changed since the user added it to cart, show an error. Better performance, slightly worse UX.

I prefer optimistic locking with a reservation system: when someone starts checkout, reserve the item for 15 minutes. If they don't complete, release it.

## Search and Filtering

For small catalogs (under 10,000 products), PostgreSQL full-text search works great. For larger catalogs, use a dedicated search engine like Meilisearch or Elasticsearch.

Key features users expect:
- **Faceted search** — Filter by size, color, price range, category simultaneously
- **Typo tolerance** — "bleu shirt" should find "blue shirt"
- **Instant results** — Search-as-you-type with debouncing

## Performance at Scale

### Caching Strategy

- **Product pages** — Cache at the CDN level with a 5-minute TTL. Revalidate on product update.
- **Cart** — Never cache. Always fresh.
- **Category pages** — Cache for 1 hour. Invalidate when products in the category change.
- **Search results** — Cache popular queries for 15 minutes.

### Image Optimization

Product images are the heaviest assets. Use:
- WebP format with JPEG fallback
- Multiple sizes (thumbnail, card, full) generated at upload time
- Lazy loading for below-the-fold images
- CDN delivery (Cloudinary, Imgix, or Next.js built-in optimizer)

## Conclusion

E-commerce development is a lesson in handling complexity without letting it overwhelm you. Start simple, handle money carefully, optimize checkout relentlessly, and always build for the user who's buying on a phone with one hand while holding a coffee in the other.

The platforms that succeed aren't the most feature-rich — they're the ones where buying something feels effortless.