This module demonstrates business logic, database operations, and transaction management for a reservation system. It implements atomic operations to prevent race conditions.
Without atomic operations:
Time | Request A | Request B
------+-----------------------+-----------------------
t1 | Read: qty = 3 |
t2 | | Read: qty = 3
t3 | Reserve 2 |
t4 | Write: qty = 1 |
t5 | | Reserve 2
t6 | | Write: qty = 1
t7 | Success! | Success!
Result: Both reserved, but we oversold by 1!
Use atomic database operations:
UPDATE items
SET availableQty = availableQty - ?
WHERE id = ? AND availableQty >= ?This UPDATE only succeeds if there's enough stock. If not, changes === 0.
Use ACID transactions for multi-step operations:
export function transaction<T>(fn: () => T): T {
const tx = db.transaction(fn);
return tx();
}All operations succeed or all rollback together.
Reservation business logic:
- Reserve - Create reservation with atomic stock decrement
- Confirm - Confirm reservation (status transition)
- Cancel - Cancel reservation and restore stock
- Expire - Expire old reservations
- Query - List/get items and reservations
Creates a reservation and atomically decrements stock:
export function reserveItem(request: ReserveRequest): ReserveResult {
return transaction(() => {
// 1. Check if item exists
const item = db.prepare('SELECT * FROM items WHERE id = ?').get(itemId);
if (!item) {
return { kind: 'NOT_FOUND' };
}
// 2. Atomically decrement stock (only if enough available)
const updateResult = db.prepare(
'UPDATE items SET availableQty = availableQty - ? WHERE id = ? AND availableQty >= ?'
).run(qty, itemId, qty);
if (updateResult.changes === 0) {
// Conditional update failed = not enough stock
return { kind: 'OUT_OF_STOCK', available: item.availableQty };
}
// 3. Create reservation
const reservation = {
id: `res_${crypto.randomUUID()}`,
userId,
itemId,
qty,
status: 'reserved',
expiresAt: Date.now() + timeout,
createdAt: Date.now()
};
db.prepare('INSERT INTO reservations ...').run(reservation);
return { kind: 'OK', reservation };
});
}Result types:
OK- Reservation createdNOT_FOUND- Item doesn't existOUT_OF_STOCK- Not enough stock availableINVALID_QUANTITY- Invalid quantity (too small/large)
Confirms a reservation (status transition: reserved → confirmed):
export function confirmReservation(request: ConfirmRequest): ConfirmResult {
return transaction(() => {
// 1. Find reservation
const reservation = db.prepare(
'SELECT * FROM reservations WHERE id = ? AND userId = ?'
).get(reservationId, userId);
if (!reservation) {
return { kind: 'NOT_FOUND' };
}
// 2. Check status transitions
if (reservation.status === 'confirmed') {
return { kind: 'ALREADY_CONFIRMED' };
}
if (reservation.status === 'cancelled') {
return { kind: 'CANCELLED' };
}
// 3. Check if expired
if (now > reservation.expiresAt) {
// Restore stock and mark as expired
db.prepare('UPDATE items SET availableQty = availableQty + ? WHERE id = ?').run(qty, itemId);
db.prepare('UPDATE reservations SET status = ? WHERE id = ?').run('expired', reservationId);
return { kind: 'EXPIRED' };
}
// 4. Confirm reservation
db.prepare('UPDATE reservations SET status = ? WHERE id = ?').run('confirmed', reservationId);
return { kind: 'OK' };
});
}Result types:
OK- ConfirmedNOT_FOUND- Reservation doesn't existALREADY_CONFIRMED- Already confirmed (idempotent)CANCELLED- Reservation was cancelledEXPIRED- Reservation has expired
Cancels a reservation and restores stock:
export function cancelReservation(request: CancelRequest): CancelResult {
return transaction(() => {
// 1. Find reservation
const reservation = db.prepare(
'SELECT * FROM reservations WHERE id = ? AND userId = ?'
).get(reservationId, userId);
if (!reservation) {
return { kind: 'NOT_FOUND' };
}
// 2. Check status
if (reservation.status === 'cancelled') {
return { kind: 'ALREADY_CANCELLED' };
}
if (reservation.status === 'confirmed') {
return { kind: 'ALREADY_CONFIRMED' };
}
// 3. Restore stock (if not already expired)
if (reservation.status !== 'expired') {
db.prepare('UPDATE items SET availableQty = availableQty + ? WHERE id = ?').run(qty, itemId);
}
// 4. Update status
db.prepare('UPDATE reservations SET status = ? WHERE id = ?').run('cancelled', reservationId);
return { kind: 'OK' };
});
}Result types:
OK- CancelledNOT_FOUND- Reservation doesn't existALREADY_CANCELLED- Already cancelled (idempotent)ALREADY_CONFIRMED- Can't cancel confirmed reservations
Expires old reservations and restores stock:
export function expireReservations(): ExpireResult {
return transaction(() => {
// 1. Find all expired reservations
const expiredReservations = db.prepare(`
SELECT id, itemId, qty
FROM reservations
WHERE status = 'reserved' AND expiresAt < ?
`).all(now);
if (expiredReservations.length === 0) {
return { kind: 'OK', expired: 0 };
}
// 2. Process each expired reservation
for (const resv of expiredReservations) {
// Restore stock
db.prepare('UPDATE items SET availableQty = availableQty + ? WHERE id = ?').run(
resv.qty,
resv.itemId
);
// Mark as expired
db.prepare('UPDATE reservations SET status = ? WHERE id = ?').run('expired', resv.id);
}
return { kind: 'OK', expired: expiredReservations.length };
});
}export function listItems(options: {
sortBy?: 'name' | 'availableQty';
sortOrder?: 'asc' | 'desc';
} = {}): Item[] {
const { sortBy = 'name', sortOrder = 'asc' } = options;
const items = db.prepare(`
SELECT id, name, availableQty FROM items
ORDER BY ${sortBy} ${sortOrder.toUpperCase()}
`).all();
return items;
}export function getItem(itemId: string): Item | null {
const item = db.prepare(
'SELECT id, name, availableQty FROM items WHERE id = ?'
).get(itemId);
return item || null;
}export function listReservationsForUser(
userId: string,
options: { status?: string } = {}
): Reservation[] {
const { status } = options;
let query = 'SELECT * FROM reservations WHERE userId = ?';
const params = [userId];
if (status) {
query += ' AND status = ?';
params.push(status);
}
query += ' ORDER BY createdAt DESC';
return db.prepare(query).all(...params);
}All operations return discriminated unions for type safety:
type ReserveResult =
| { kind: 'OK'; reservation: Reservation }
| { kind: 'NOT_FOUND' }
| { kind: 'OUT_OF_STOCK'; available: number }
| { kind: 'INVALID_QUANTITY'; min: number; max: number };
// Usage with type narrowing
const result = reserveItem(request);
if (result.kind === 'OK') {
console.log(result.reservation.id);
} else if (result.kind === 'OUT_OF_STOCK') {
console.log(`Only ${result.available} available`);
}Valid status transitions:
reserved ────────→ confirmed
│ │
↓ │
expired (can't go back)
│
↓
cancelled
Rules:
reserved→confirmed: OKreserved→cancelled: OKreserved→expired: OK (automatic)cancelled→reserved: Not allowedconfirmed→cancelled: Not allowedexpired→confirmed: Not allowed
All succeed or all fail together:
export function reserveMultipleItems(
userId: string,
items: Array<{ itemId: string; qty: number }>
): { kind: 'OK'; reservations: Reservation[] } | { kind: 'ERROR'; message: string } {
return transaction(() => {
const reservations: Reservation[] = [];
for (const item of items) {
const result = reserveItem({ userId, itemId: item.itemId, qty: item.qty });
if (result.kind !== 'OK') {
return {
kind: 'ERROR',
message: `Failed to reserve item ${item.itemId}`
};
}
if ('reservation' in result) {
reservations.push(result.reservation);
}
}
return { kind: 'OK', reservations };
});
}After modifying data, invalidate cache:
// After reserving
invalidateItemCaches(itemId);
// After confirming
invalidateReservationCaches(reservationId, userId);export function getReservationStats(): {
total: number;
byStatus: Record<string, number>;
expiredCount: number;
expiringSoon: number;
} {
const total = db.prepare('SELECT COUNT(*) as count FROM reservations').get().count;
const byStatus = {};
const statusRows = db.prepare(
'SELECT status, COUNT(*) as count FROM reservations GROUP BY status'
).all();
for (const row of statusRows) {
byStatus[row.status] = row.count;
}
const now = Date.now();
const expiredCount = db.prepare(
'SELECT COUNT(*) as count FROM reservations WHERE status = ? AND expiresAt < ?'
).get('reserved', now).count;
const expiringSoon = db.prepare(
'SELECT COUNT(*) as count FROM reservations WHERE status = ? AND expiresAt >= ? AND expiresAt < ?'
).get('reserved', now, now + 5 * 60 * 1000).count;
return { total, byStatus, expiredCount, expiringSoon };
}../database/index.ts- Database connection and transactions../types/index.ts- Result type definitions../routes/index.ts- API endpoint handlers../cache/index.ts- Cache invalidation
- Use transactions for multi-step operations
- Use conditional updates to prevent race conditions
- Return discriminated unions for type safety
- Restore stock on expiry/cancellation
- Validate status transitions
- Log all state changes
- Read-then-write (race condition)
- Forget transaction rollback
- Allow invalid status transitions
- Forget to restore stock
- Ignore edge cases
💡 Tip: The WHERE availableQty >= ? condition is what prevents overselling - it's all about atomicity!