A backend system that demonstrates atomic resource reservation, payment authorization (hold), automatice expiration, and idempotent payment handling using Stripe.
- Atomic Reservation + Payment Hold
- A resource is only reserved if a payment hold succeeds.
- No unpaid or dangling reservations.
- Stripe Manual Capture Workflow
- PaymentIntent created with
capture_method: manual. - Payment is captured only after reservation confirmation.
- PaymentIntent created with
- Automatic Reservation Expiry
- Pending reservations expire after a configurable TTL.
- Expired reservations automatically release the resource and payment hold.
- Idempotent Payment Creation
- Stripe idempotency keys prevent duplicate PaymentIntents.
- Database Concurrency Safety
- Uses row-level locking and partial indexes to prevent double booking.
- Node.js
- Express.js
- PostgreSQL
- Stripe Payment Integration
- Node-cron
- Docker + docker-compose
- Zod Validation
src/
├── app.js
├── server.js
├── db/
│ ├── 001_migrations.sql
│ ├── seed.sql
│ └── index.js
├── routes/
│ └── reservations.routes.js
├── controllers/
│ └── reservations.controller.js
├── services/
│ ├── reservations.service.js
│ └── payments.service.js
├── jobs/
│ ├── index.js
│ └── expireReservations.job.js
├── middleware/
│ ├── validate.js
│ ├── errorHandler.js
│ └── notFound.js
├── validators/
│ └── reservations.schema.js
└── utils/
└── time.js
- Clone & Install
git clone <repo-url>
cd reservation-api
npm install- Environment Variables
Create a .env file based on .env.example and configure the required environment variables with your own values.
- Start PostgreSQL via Docker
docker compose up --build- Copy file into DB container
docker cp src/db/001_migrations.sql reservation-db:/001_migrations.sql- Run Database Migrations
docker compose exec db psql -U postgres -d reservation_db -f 001_migrations.sqlPOST /reservations
{
"userId": "uuid",
"resourceId": "uuid"
}Response
{
"reservation": {
"id": "uuid",
"userId": "uuid",
"resourceId": "uuid",
"status": "pending",
"expiresAt": "2026-01-30T12:00:00Z"
},
"payment": {
"status": "on_hold",
"provider": "stripe",
"providerRef": "pi_xxx"
}
}POST /reservations/:id/confirm
- Captures the Stripe PaymentIntent.
- Updates reservation status to
confirmed.
POST /reservations/:id/delete
- Cancels the reservation.
- Releases the Stripe payment hold.
PENDING → CONFIRMED
↓
(cron job) → EXPIRED
↓
CANCELLED
Rules:
- A reservation starts as
pendingonly after a payment hold succeeds. - Pending reservations expire automatically after TTL.
- Only
pendingreservations can be confirmed or cancelled.
Runs every minutes using node-cron.
cron.schedule("* * * * *", async () => {
await expireReservations();
});The jobs:
- Finds expired pending reservations.
- Releases Stripe payment holds.
- Marks reservations as
expired.