Case study

Fake E-commerce — Redis cart, Postgres orders

A production-minded Next.js storefront where I started with Redis JSON blobs for order speed, then migrated to Postgres + Prisma on Neon for relational integrity—without changing the public API.

Mini E-commerce storefront

1. Problem

v1 stored orders as Redis JSON blobs keyed by Clerk userId (orders:user:{userId}). That was fast to ship and fine for a demo checkout—but it broke down when I needed relational integrity, per-line-item queries, and an admin view over all orders.

Cart data stayed the right fit for Redis: session-scoped, high-churn key/value with no joins. Orders needed rows, foreign keys, and migrations. The migration had to swap the persistence adapter behind order-store.ts while keeping REST and GraphQL contracts identical.

2. Architecture

Two stores on purpose—each matches its access pattern:

3. The migration

Four commits, same day—schema first, adapter swap, production bug fix, then docs:

  1. f7440c3 — Bootstrap Prisma 6 with Order + OrderItem schema, Docker Compose Postgres for local dev, initial migration.
  2. 50fd84b — Replace Redis order-store with Prisma adapter; map models to existing Order type; Redis stays for cart only. 45/45 tests green.
  3. 385f4a4 — Fix duplicate order creation on /checkout/success (race condition deep-dive below).
  4. 4575a38 — README documents Redis cart vs Postgres orders and the migration narrative.

Before the adapter swap I locked Redis behavior with commit 9668a49 so the test suite would catch regressions during the Prisma cutover. 9668a49

4. Race condition deep-dive

After Postgres went live, checkout success sometimes created two orders for one cart. The bug lived in a useEffect on /checkout/success—not in Prisma. That’s the kind of issue that separates “it works on my machine” from production-aware React.

Root cause

Fix

// Guard before async work — not in .finally()
hasSubmitted.current = true;

const items = cart.map((item) => ({ ...item }));
const orderTotal = computeOrderTotal(items);

// Effect deps: [cart.length] only — intentional

View full diff on GitHub →

5. Outcomes