██╗ ██╗ ██████╗ ██╗ ██████╗ ██████╗ ███████╗███████╗██╗ ██╗
██║ ██║██╔═══██╗██║ ██╔══██╗ ██╔══██╗██╔════╝██╔════╝██║ ██╔╝
███████║██║ ██║██║ ██║ ██║ ██║ ██║█████╗ ███████╗█████╔╝
██╔══██║██║ ██║██║ ██║ ██║ ██║ ██║██╔══╝ ╚════██║██╔═██╗
██║ ██║╚██████╔╝███████╗██████╔╝ ██████╔╝███████╗███████║██║ ██╗
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝
Multi-warehouse inventory reservation system — race-condition-safe, idempotent, production-ready.
Every e-commerce system eventually faces this cliff:
| Approach | Failure Mode |
|---|---|
| Reduce stock only after payment | Two users pay for the same last unit — one order fails post-payment |
| Reduce stock at add-to-cart | Abandoned carts permanently ghost your inventory |
| No reservation layer at all | Race conditions, overselling, customer trust erosion |
Hold Desk solves this with a temporary reservation layer — a battle-tested pattern used by Ticketmaster, Airbnb, and every high-traffic booking platform.
User adds to cart
│
▼
┌─────────────────────────────────────────┐
│ POST /api/reservations │
│ │
│ Atomic SQL: │
│ UPDATE Inventory │
│ SET reservedQty = reservedQty + n │
│ WHERE (totalQty - reservedQty) >= n │
│ │
│ ✅ Row updated → PENDING hold │
│ ❌ 0 rows → 409 Conflict │
└─────────────────────────────────────────┘
│
▼
Timer starts (TTL = 10 min)
│
├──── Payment succeeds ──▶ CONFIRMED (stock permanently sold)
│
├──── User cancels ──▶ RELEASED (stock freed immediately)
│
└──── Timer expires ──▶ RELEASED (lazy cleanup on next request)
The single atomic UPDATE with a row-count check is the core of the entire concurrency safety guarantee — no Redis locks, no application-level mutexes, no distributed coordination overhead.
- 🏭 Multi-warehouse inventory management — per-warehouse stock lanes with live visibility
- 🔒 Race-condition-safe reservations — atomic SQL guarantees exactly-once reservation
- ♻️ Full reservation lifecycle —
PENDING → CONFIRMED / RELEASED - ⏱️ Automatic expiry with lazy cleanup — expired holds release on next user activity
- 🔁 Idempotency support — safe retries via
Idempotency-Keyheader - 📉 Scarcity indicators — low-stock badges surface urgency in real time
- 🔔 Conflict toasts — immediate feedback on 409 collisions
- ⏳ Countdown timers — visible reservation expiry on the frontend
- 🔄 Live inventory polling — frontend refreshes every 5 seconds
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 15 App Router + TypeScript | Full-stack, edge-ready, type-safe |
| Styling | Tailwind CSS | Utility-first, zero runtime overhead |
| Database | Supabase PostgreSQL | Managed Postgres with row-level transactions |
| ORM | Prisma | Type-safe schema, migration-first workflow |
| Validation | Zod | Runtime schema validation on all API inputs |
| Client State | TanStack Query | Polling, cache invalidation, request deduplication |
| Hosting | Vercel | Zero-config deploys, edge network |
Returns all products with per-warehouse available quantities. Also triggers lazy expiry cleanup.
Returns list of all warehouses.
Creates a temporary inventory hold.
Headers:
Idempotency-Key: <uuid> // optional — enables safe retries
Body:
{
"productId": "string",
"warehouseId": "string",
"quantity": 1
}Responses:
201 Created— reservation created, returnsreservationId+expiresAt409 Conflict— insufficient stock available422 Unprocessable— validation error
Returns reservation status + triggers lazy cleanup on expired holds.
Confirms a PENDING reservation after successful payment.
Headers:
Idempotency-Key: <uuid> // recommended for payment flows
Responses:
200 OK— reservation confirmed, inventory permanently decremented404 Not Found— reservation does not exist409 Conflict— reservation already expired or released
IMPORTANT NOTE
Immediately releases a PENDING reservation and restores held inventory.
Cron-based expiry (implemented)
Originally implemented using:
Vercel cron job /api/cron/release-expired
The cron endpoint automatically released expired reservations every minute.
Why cron is disabled in production
The deployed project currently uses the Vercel ##Hobby plan, which has limitations around cron scheduling for this setup.
To keep deployment simple and stable, the project currently relies on lazy cleanup.
Lazy Cleanup (currently active) Expired reservations are automatically cleaned whenever users interact with the application.
Cleanup runs during:
GET /api/products GET /api/reservations/:id reservation confirmation flow
This means: expired holds still release correctly inventory returns automatically no manual cleanup is required
Difference: cleanup occurs during user activity not on a fixed background schedule
git clone https://github.com/YOUR_USERNAME/inventory-reservation-system.git
cd inventory-reservation-systemnpm installCreate .env in the root:
DATABASE_URL=... # Supabase pooled connection
DIRECT_URL=... # Supabase direct connection (for migrations)
RESERVATION_TTL_MINUTES=10 # How long holds last
CRON_SECRET=your-secret # Protects the cron endpoint
NEXT_PUBLIC_APP_URL=http://localhost:3000Get
DATABASE_URLandDIRECT_URLfrom:
Supabase Dashboard → Connect → ORMs → Prisma
npm run db:setupThis runs Prisma migrations and seeds warehouses, products, and demo inventory.
npm run devOpen http://localhost:3000.
Product HD-RACE-99 is seeded with exactly 1 unit in Bengaluru — intentionally scarce for race condition testing.
Send two simultaneous reservation requests:
# Terminal 1
curl -X POST http://localhost:3000/api/reservations \
-H "Content-Type: application/json" \
-d '{"productId":"HD-RACE-99","warehouseId":"bengaluru","quantity":1}'
# Terminal 2 (same time)
curl -X POST http://localhost:3000/api/reservations \
-H "Content-Type: application/json" \
-d '{"productId":"HD-RACE-99","warehouseId":"bengaluru","quantity":1}'Expected result: One 201 Created, one 409 Conflict. Always. Guaranteed by the atomic update.
Or use the included script:
npm run test:concurrency| Decision | Rationale |
|---|---|
| PostgreSQL atomic updates over Redis locks | Simpler architecture, fewer moving parts, sufficient for most workloads |
| Polling over WebSockets | Dramatically reduced infrastructure complexity with acceptable UX |
| Lazy expiry cleanup | Keeps the app fully functional on Vercel Hobby without paid cron |
| No authentication | Scope-focused — reservation logic is the deliverable, not auth |
- WebSocket / SSE for real-time inventory updates
- Redis distributed locking for ultra-high-concurrency scenarios
- Background worker for scheduled expiry cleanup
- Admin dashboard — reservation analytics, warehouse management
- User authentication + reservation history
- Warehouse prioritization / routing logic
- Reservation conversion analytics
.
├── src/
│ ├── app/
│ │ ├── api/ # Route handlers
│ │ │ ├── products/
│ │ │ ├── warehouses/
│ │ │ ├── reservations/
│ │ │ └── cron/
│ │ └── page.tsx # Frontend entry
│ ├── lib/
│ │ └── inventory/ # Reservation business logic
│ └── components/ # UI components
├── prisma/
│ ├── schema.prisma # Data model
│ ├── migrations/ # Migration history
│ └── seed.ts # Demo data seeder
├── scripts/
│ └── test-concurrency.ts # Parallel request test
└── .env.example
MIT — Built as a take-home assignment for Allo Engineering.
Built with precision. Designed for correctness.
The last unit always goes to exactly one customer.