A backend service that allows users to reserve cars for a limited time (hold/lock), then confirm or cancel bookings. The system ensures only one active reservation can exist for a car at a time.
- Create a reservation (place a hold on a car)
- Confirm a reservation
- Cancel a reservation
- Automatic expiration of stale holds
- Get reservation details by reservation ID
- Get current reservation for a car
- Concurrency handling (prevents double reservations)
- Idempotency support via
requestId - State machine validation (only valid transitions allowed)
- Runtime: Node.js with TypeScript
- Framework: Express.js
- Database: PostgreSQL
- Validation: Zod
- Node.js (v18 or higher)
- PostgreSQL (v12 or higher)
- npm or yarn
-
Clone the repository and install dependencies:
npm install
-
Set up PostgreSQL database:
createdb onelot
-
Configure environment variables: Create a
.envfile in the root directory:DB_HOST=localhost DB_PORT=5432 DB_NAME=onelot DB_USER=postgres DB_PASSWORD=postgres PORT=3000 NODE_ENV=development -
Run database migrations:
psql -d onelot -f migrations/001_initial_schema.sql
-
Start the server:
npm run dev
Or build and run:
npm run build npm start
POST /reservations
Places a hold on a car for a specified duration.
Request Body:
{
"carId": 1,
"buyerId": 123,
"requestId": "unique-request-id-123", // Optional: for idempotency
"holdDurationMinutes": 15 // Optional: defaults to 15 minutes
}Response (201 Created):
{
"id": 1,
"carId": 1,
"buyerId": 123,
"state": "HELD",
"expiresAt": "2024-01-15T10:30:00.000Z",
"requestId": "unique-request-id-123",
"createdAt": "2024-01-15T10:15:00.000Z",
"updatedAt": "2024-01-15T10:15:00.000Z"
}Error Responses:
400: Validation error404: Car not found409: Car already has an active reservation
POST /reservations/:id/confirm
Confirms a HELD reservation.
Request Body:
{
"requestId": "confirm-request-id-456" // Optional: for idempotency
}Response (200 OK):
{
"id": 1,
"carId": 1,
"buyerId": 123,
"state": "CONFIRMED",
"expiresAt": "2024-01-15T10:30:00.000Z",
"requestId": "unique-request-id-123",
"createdAt": "2024-01-15T10:15:00.000Z",
"updatedAt": "2024-01-15T10:20:00.000Z"
}Error Responses:
400: Invalid state transition or validation error404: Reservation not found
POST /reservations/:id/cancel
Cancels a HELD reservation.
Request Body:
{
"requestId": "cancel-request-id-789" // Optional: for idempotency
}Response (200 OK):
{
"id": 1,
"carId": 1,
"buyerId": 123,
"state": "CANCELLED",
"expiresAt": "2024-01-15T10:30:00.000Z",
"requestId": "unique-request-id-123",
"createdAt": "2024-01-15T10:15:00.000Z",
"updatedAt": "2024-01-15T10:25:00.000Z"
}Error Responses:
400: Invalid state transition or validation error404: Reservation not found
GET /reservations/:id
Retrieves reservation details by ID.
Response (200 OK):
{
"id": 1,
"carId": 1,
"buyerId": 123,
"state": "HELD",
"expiresAt": "2024-01-15T10:30:00.000Z",
"requestId": "unique-request-id-123",
"createdAt": "2024-01-15T10:15:00.000Z",
"updatedAt": "2024-01-15T10:15:00.000Z"
}Error Responses:
404: Reservation not found
GET /reservations/car/:carId
Retrieves the current active reservation (HELD or CONFIRMED) for a car.
Response (200 OK):
{
"id": 1,
"carId": 1,
"buyerId": 123,
"state": "HELD",
"expiresAt": "2024-01-15T10:30:00.000Z",
"requestId": "unique-request-id-123",
"createdAt": "2024-01-15T10:15:00.000Z",
"updatedAt": "2024-01-15T10:15:00.000Z"
}Error Responses:
404: No active reservation found for this car
id(SERIAL PRIMARY KEY)dealer_id(INTEGER)status(VARCHAR)created_at(TIMESTAMP)updated_at(TIMESTAMP)
id(SERIAL PRIMARY KEY)car_id(INTEGER, FOREIGN KEY)buyer_id(INTEGER)state(VARCHAR): HELD, CONFIRMED, CANCELLED, or EXPIREDexpires_at(TIMESTAMP)request_id(VARCHAR, UNIQUE): For idempotencycreated_at(TIMESTAMP)updated_at(TIMESTAMP)
id(SERIAL PRIMARY KEY)reservation_id(INTEGER, FOREIGN KEY)event_type(VARCHAR): CREATED, CONFIRMED, CANCELLED, EXPIREDfrom_state(VARCHAR, nullable)to_state(VARCHAR)metadata(JSONB)created_at(TIMESTAMP)
Reservation states and valid transitions:
HELD → CONFIRMED
HELD → CANCELLED
HELD → EXPIRED (automatic)
CONFIRMED → (terminal state)
CANCELLED → (terminal state)
EXPIRED → (terminal state)
The system uses PostgreSQL FOR UPDATE row-level locking to prevent concurrent reservation attempts. When creating a reservation, the car and any active reservations are locked, ensuring only one reservation can be created at a time.
All mutating operations (create, confirm, cancel) support an optional requestId parameter. When provided, repeated calls with the same requestId will return the same result without creating duplicate actions.
A background worker runs every minute to automatically expire holds that have passed their expiresAt timestamp. The worker updates the state from HELD to EXPIRED and creates an audit event.
# 1. Create a reservation
curl -X POST http://localhost:3000/reservations \
-H "Content-Type: application/json" \
-d '{
"carId": 1,
"buyerId": 123,
"requestId": "req-123",
"holdDurationMinutes": 15
}'
# 2. Confirm the reservation
curl -X POST http://localhost:3000/reservations/1/confirm \
-H "Content-Type: application/json" \
-d '{
"requestId": "confirm-req-123"
}'# First call
curl -X POST http://localhost:3000/reservations \
-H "Content-Type: application/json" \
-d '{
"carId": 1,
"buyerId": 123,
"requestId": "unique-id-123"
}'
# Second call with same requestId - returns same reservation
curl -X POST http://localhost:3000/reservations \
-H "Content-Type: application/json" \
-d '{
"carId": 1,
"buyerId": 123,
"requestId": "unique-id-123"
}'