A production-quality inventory reservation system built with Next.js App Router that prevents overselling through concurrency-safe atomic stock management across multiple warehouses.
- Product Listing — Browse products with real-time per-warehouse stock levels
- Concurrency-Safe Reservations — Atomic stock locking prevents two users from reserving the last unit
- Live Countdown Timer — Visual countdown shows time remaining before reservation expires
- Automatic Expiry — Unreserved items are automatically released back to inventory
- Idempotency Support — Duplicate requests return the original response (Idempotency-Key header)
- Error Handling — Clear 409 (conflict) and 410 (expired) error states in the UI
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (end-to-end) |
| Database | PostgreSQL (Neon / Supabase) |
| ORM | Prisma 7 with PrismaPg driver adapter |
| Validation | Zod |
| Styling | Tailwind CSS v4 + shadcn/ui |
| Deployment | Vercel-ready with cron configuration |
- Node.js 20+
- A PostgreSQL database (recommended: Neon free tier)
# 1. Clone and install
git clone <repo-url>
cd allo
npm install
# 2. Configure environment
cp .env.example .env
# Edit .env and paste your DATABASE_URL
# 3. Push schema to database
npm run db:push
# 4. Seed demo data
npm run db:seed
# 5. Start development server
npm run devOpen http://localhost:3000 to see the app.
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ | — | PostgreSQL connection string (e.g., Neon pooled URL) |
RESERVATION_TTL_SECONDS |
❌ | 600 |
How long a reservation is held before expiry (seconds) |
Returns all products with per-warehouse stock breakdown including available counts.
Returns all warehouses.
Creates a new reservation. Accepts:
{
"productId": "string",
"warehouseId": "string",
"quantity": 1
}Headers:
Idempotency-Key(optional): Prevents duplicate reservations on retries
Responses:
201— Reservation created with details andexpiresAt400— Validation error409— Insufficient stock (with human-readable message)
Confirms a pending reservation, permanently deducting stock.
200— Confirmed410— Expired (reservation was released)
Cancels a pending reservation, returning stock to inventory.
200— Released
The core problem: Two users simultaneously try to reserve the last unit. Both read available = 1, both try to reserve. Without protection, both succeed → oversold.
The solution: Atomic conditional UPDATE with row-level locking.
UPDATE "Inventory"
SET "reserved" = "reserved" + $quantity
WHERE "productId" = $productId
AND "warehouseId" = $warehouseId
AND ("quantity" - "reserved") >= $quantityThis single SQL statement is the entire concurrency mechanism:
- Atomicity — PostgreSQL guarantees the UPDATE is atomic
- Row-level locking — When two transactions hit the same row, PostgreSQL serializes them
- Conditional check — The
WHEREclause ensures the second request sees the updatedreservedvalue from the first - Affected row count — If
updated === 0, we know stock was insufficient and return 409
This approach is simpler than SELECT FOR UPDATE and equally safe because the conditional UPDATE both checks and modifies in one operation.
Reservations are automatically released back to available inventory using a Strictly Lazy Cleanup Strategy.
Before executing any stock-sensitive operation, the system automatically checks for and releases all expired reservations globally:
- GET /api/products: Cleans up expired reservations before calculating inventory, ensuring the storefront displays correct stock levels.
- POST /api/reservations: Cleans up expired reservations before verifying stock availability, ensuring new reservations can occupy previously held slots immediately.
- POST /api/reservations/:id/confirm: Cleans up expired reservations before confirming, preventing late confirmations of expired stock.
- Self-Contained & Cost-Effective: Eliminates dependencies on background worker processes, paid cron services, or external HTTP triggers (like Vercel Cron).
- Low Overhead: Uses indexed queries (
WHERE status = 'PENDING' AND expiresAt < NOW()) which are extremely fast and add negligible latency to requests. - Serverless-Friendly: Fits serverless scaling profiles perfectly, since it runs inline during the request lifecycle.
The POST /api/reservations and POST /api/reservations/:id/confirm endpoints support idempotency via the Idempotency-Key header.
How it works:
- Client sends
Idempotency-Key: <unique-key>in the request header - Before processing, the server checks the
IdempotencyKeytable for this key - If found, the cached response (status + body) is returned with
x-idempotent-replay: trueheader - If not found, the request is processed normally and the response is stored
The frontend generates keys using ${action}-${id}-${timestamp} to ensure uniqueness per action attempt while allowing retries of the same attempt.
Product ──┐
├── Inventory (quantity, reserved) ──┐
Warehouse ─┘ │
│
Reservation (productId, warehouseId, quantity, │
status, expiresAt) ──────────────────┘
IdempotencyKey (key, response, statusCode)
- Inventory.quantity: Total physical stock
- Inventory.reserved: Units held by PENDING reservations
- Available =
quantity - reserved(computed, not stored) - When confirmed: both
quantityandreservedare decremented (stock leaves the warehouse) - When released/expired: only
reservedis decremented (stock returns to available)
- Lazy expiry adds latency to product listing requests, but ensures accurate stock. In production, the cron handles most expirations proactively.
- No authentication — This is a demo. In production, reservations would be tied to user sessions.
- In-memory Prisma client — In serverless (Vercel), each cold start creates a new connection. Consider connection pooling (PgBouncer / Neon pooler) for production scale.
- Idempotency keys are not garbage-collected — In production, add a TTL-based cleanup job.
- WebSocket/SSE for real-time stock updates across browsers
- Rate limiting on reservation creation
- Reservation queue with fair ordering for high-demand products
- Multi-item cart with all-or-nothing reservation
- Admin dashboard for warehouse managers
- Distributed locking (Redis) for multi-region deployments
- Comprehensive test suite (unit + integration + concurrency stress tests)
- OpenAPI specification for the API
- Structured logging with correlation IDs
allo/
├── prisma/
│ ├── schema.prisma # Database schema
│ ├── seed.ts # Demo data seeder
│ └── migrations/ # Migration history
├── prisma.config.ts # Prisma 7 configuration
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ ├── products/route.ts # GET /api/products
│ │ │ ├── warehouses/route.ts # GET /api/warehouses
│ │ │ ├── reservations/
│ │ │ │ ├── route.ts # POST /api/reservations
│ │ │ │ └── [id]/
│ │ │ │ ├── confirm/route.ts # POST confirm
│ │ │ │ └── release/route.ts # POST release
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx # Main page
│ ├── components/
│ │ ├── product-card.tsx # Product display
│ │ ├── reservation-modal.tsx # Reserve dialog
│ │ ├── checkout-view.tsx # Checkout with countdown
│ │ └── ui/ # shadcn/ui primitives
│ └── lib/
│ ├── prisma.ts # DB client singleton
│ ├── expiry.ts # Expiry cleanup logic
│ ├── validations.ts # Zod schemas
│ ├── types.ts # API response types
│ └── utils.ts # shadcn utilities
├── .env.example # Env template
└── package.json
MIT