
When building an e-commerce, food delivery, logistics, or appointment-booking system, one common problem appears again and again:
How can we allocate a limited resource to multiple customers without overbooking it?
A delivery slot is a perfect example of a shared resource.
Imagine that a delivery slot can handle only 5 orders:
10:00 - 12:00
Capacity: 5
Booked: 4
There is only one remaining position.
If two customers try to book the slot at exactly the same time, our backend must guarantee that only one of them gets the final position.
This article explains how to design such a system using:
- Capacity management
- Availability checking
- Atomic database operations
- Race-condition prevention
- Alternative slot selection
- Transactions and rollback
- Concurrent request handling
- Idempotency
- Error handling
1. Understanding the Problem
Let's start with the delivery slot model.
A slot contains:
Slot
├── id
├── startTime
├── endTime
├── capacity
└── bookedCount
For example:
Slot ID: SLOT_01
Start: 10:00
End: 12:00
Capacity: 5
Booked Count: 3
The fundamental availability rule is:
bookedCount < capacity
Therefore:
3 < 5 → Available
But:
5 < 5 → False
So the slot is fully booked.
2. The Booking Request
A customer might send:
{
"customerId": "C001",
"orderId": "O001",
"preferredSlotId": "SLOT_01"
}
The backend needs to:
- Validate the request.
- Find the requested slot.
- Check availability.
- Reserve capacity.
- Create the reservation.
- Return a confirmation.
The basic flow is:
Client
|
v
Validate Request
|
v
Find Slot
|
v
Check Availability
|
v
Reserve Capacity
|
v
Create Reservation
|
v
Return Confirmation
3. Why a Simple Availability Check Is Not Enough
A naive implementation might look like this:
const slot = await Slot.findById(slotId);
if (slot.bookedCount >= slot.capacity) {
throw new Error("Slot is full");
}
slot.bookedCount += 1;
await slot.save();
At first glance, this looks correct.
But there is a serious concurrency problem.
4. The Race Condition Problem
Suppose we have:
Capacity = 5
Booked = 4
There is only one available position.
Now two requests arrive almost simultaneously:
Customer A
Customer B
The operations could happen like this:
Customer A → READ → bookedCount = 4
Customer B → READ → bookedCount = 4
Customer A → available
Customer B → available
Customer A → bookedCount = 5
Customer B → bookedCount = 6
Now we have:
Capacity = 5
Booked = 6
This is overbooking.
The problem isn't the availability condition itself.
The problem is that:
CHECK
and
UPDATE
are separate operations.
Between the check and the update, another request can modify the data.
This is called a:
Race Condition
5. What Is a Race Condition?
A race condition occurs when the result of an operation depends on the timing or ordering of concurrent operations.
In our case:
Request A
|
| Check availability
|
|--------+
|
Request B |
| |
| Check |
| |
| Update |
| |
|--------+
|
| Update
Both requests saw the same state.
That's why simply checking:
if (bookedCount < capacity)
is not enough.
We need to make the critical operation atomic.
6. Atomic Operations
An atomic operation is an operation that happens as one indivisible unit.
For our problem, we want the database to perform:
Check:
bookedCount < capacity
AND
Increment:
bookedCount = bookedCount + 1
as one operation.
Conceptually:
UPDATE delivery_slots
SET booked_count = booked_count + 1
WHERE id = ?
AND booked_count < capacity;
The important part is:
WHERE booked_count < capacity
The database itself decides whether capacity is still available.
7. Why Atomic Updates Solve the Problem
Consider:
Capacity = 5
Booked = 4
Two customers attempt to reserve the last position.
Request A executes:
UPDATE delivery_slots
SET booked_count = booked_count + 1
WHERE id = 'SLOT_01'
AND booked_count < capacity;
The condition is true.
The database updates:
Booked = 5
Now Request B executes the same operation.
The condition becomes:
5 < 5
which is false.
Therefore:
Rows affected = 0
Request B knows that the slot is no longer available.
The final state remains:
Capacity = 5
Booked = 5
No overbooking occurs.
8. A Generic Reservation Algorithm
The core algorithm can be represented as:
FUNCTION reserveSlot(slotId):
UPDATE slot
WHERE id = slotId
AND bookedCount < capacity
SET bookedCount = bookedCount + 1
IF no row was updated:
RETURN "Slot unavailable"
RETURN "Slot reserved"
The database becomes responsible for enforcing the capacity condition.
9. Implementing It With Node.js
Let's assume we are using Node.js with a SQL database.
A repository method could look like:
async function reserveSlot(slotId) {
const result = await db.query(
`
UPDATE delivery_slots
SET booked_count = booked_count + 1
WHERE id = ?
AND booked_count < capacity
`,
[slotId]
);
return result.affectedRows === 1;
}
Now:
const reserved = await reserveSlot(slotId);
if (!reserved) {
return {
success: false,
message: "Slot is no longer available"
};
}
This is much safer than:
SELECT → check → UPDATE
10. The Complete Booking Service
A service layer can coordinate the entire process.
async function bookDeliverySlot({
customerId,
orderId,
preferredSlotId
}) {
if (!customerId) {
throw new Error("Customer ID is required");
}
if (!orderId) {
throw new Error("Order ID is required");
}
if (!preferredSlotId) {
throw new Error("Preferred slot ID is required");
}
const slot = await findSlotById(preferredSlotId);
if (!slot) {
throw new Error("Delivery slot not found");
}
const reserved = await reserveSlot(preferredSlotId);
if (!reserved) {
const alternatives =
await findAvailableAlternativeSlots(
preferredSlotId
);
return {
success: false,
message: "The selected delivery slot is unavailable",
alternatives
};
}
const reservation = await createReservation({
customerId,
orderId,
slotId: preferredSlotId
});
return {
success: true,
message: "Delivery slot booked successfully",
reservation
};
}
However, there is another important problem.
11. What Happens If Reservation Creation Fails?
Imagine this sequence:
1. Increment bookedCount
2. Create reservation
The database state becomes:
bookedCount = 5
But then:
createReservation()
fails.
Now the system says:
Booked = 5
even though the customer doesn't actually have a reservation.
We have consumed capacity without creating the booking.
This creates an inconsistent state.
12. Transactions
This is where a database transaction becomes useful.
We want:
Reserve slot
+
Create reservation
to behave as one logical operation.
Either:
Both succeed
or:
Both fail
Conceptually:
BEGIN TRANSACTION
Reserve slot
Create reservation
COMMIT
If something fails:
BEGIN TRANSACTION
Reserve slot
Create reservation ❌
ROLLBACK
The rollback restores the previous state.
13. Transaction-Based Implementation
A simplified SQL-style implementation:
async function bookDeliverySlot({
customerId,
orderId,
preferredSlotId
}) {
const transaction = await db.beginTransaction();
try {
const slot = await findSlotById(
preferredSlotId,
transaction
);
if (!slot) {
throw new Error("Delivery slot not found");
}
const result = await transaction.query(
`
UPDATE delivery_slots
SET booked_count = booked_count + 1
WHERE id = ?
AND booked_count < capacity
`,
[preferredSlotId]
);
if (result.affectedRows === 0) {
await transaction.rollback();
const alternatives =
await findAvailableAlternativeSlots(
preferredSlotId
);
return {
success: false,
message: "The selected delivery slot is unavailable",
alternatives
};
}
const reservation =
await createReservation(
{
customerId,
orderId,
slotId: preferredSlotId
},
transaction
);
await transaction.commit();
return {
success: true,
message: "Delivery slot booked successfully",
reservation
};
} catch (error) {
await transaction.rollback();
throw error;
}
}
The exact transaction API depends on the database driver or ORM.
14. Finding Alternative Slots
A good booking system shouldn't simply tell the customer:
Slot unavailable.
It can improve the user experience by suggesting alternatives.
For example:
Requested:
12:00 - 14:00
Alternatives:
10:00 - 12:00
14:00 - 16:00
16:00 - 18:00
The query could be conceptually:
SELECT *
FROM delivery_slots
WHERE id != ?
AND booked_count < capacity
AND start_time > CURRENT_TIMESTAMP
ORDER BY start_time;
15. Ranking Alternative Slots
Not every available slot is equally useful.
We could rank alternatives according to:
Option 1 — Closest time
Requested: 12:00
10:00
14:00
16:00
18:00
Option 2 — Earliest available
10:00
14:00
16:00
18:00
Option 3 — Business priority
For example:
Same day
↓
Nearest time
↓
Lowest delivery cost
The business rules determine the sorting strategy.
16. Alternative Slot Function
A simple implementation:
async function findAvailableAlternativeSlots(
preferredSlotId
) {
const preferredSlot =
await findSlotById(preferredSlotId);
if (!preferredSlot) {
return [];
}
return await Slot.find({
_id: {
$ne: preferredSlotId
},
bookedCount: {
$lt: "$capacity"
},
startTime: {
$gt: new Date()
}
}).sort({
startTime: 1
});
}
The exact syntax depends on the database.
For SQL databases, the equivalent condition would typically be:
booked_count < capacity
17. MongoDB Version
If the backend uses MongoDB, the same principle can be implemented with a conditional atomic update.
For example:
const slot = await DeliverySlot.findOneAndUpdate(
{
_id: slotId,
$expr: {
$lt: ["$bookedCount", "$capacity"]
}
},
{
$inc: {
bookedCount: 1
}
},
{
new: true
}
);
The important part is:
$expr: {
$lt: ["$bookedCount", "$capacity"]
}
combined with:
$inc: {
bookedCount: 1
}
The operation is performed atomically for the document.
If the result is:
null
the slot could not be reserved.
18. Handling Concurrent Requests With MongoDB
Suppose:
Capacity = 5
Booked = 4
Two requests execute:
findOneAndUpdate(...)
Request A succeeds:
Booked = 5
Request B evaluates:
5 < 5
which is false.
Therefore:
slot === null
Request B can then search for alternatives.
This protects the shared resource from overbooking.
19. The Complete Flow
The complete system can be represented as:
Customer
|
v
Booking Request
|
v
Validate Input
|
+---------+---------+
| |
Invalid Valid
| |
v v
Return Error Find Requested Slot
|
+-------+-------+
| |
Not Found Found
| |
v v
Return Error Attempt Atomic
Reservation
|
+----------+----------+
| |
Success Failed
| |
v v
Create Booking Find Alternatives
| |
v +------+------+
Commit | |
| Found None
v | |
Success v v
Return Options Return Error
20. API Design
A REST API might expose:
POST /api/delivery-slots/book
Request:
{
"customerId": "C001",
"orderId": "O001",
"preferredSlotId": "SLOT_01"
}
Successful response:
{
"success": true,
"message": "Delivery slot booked successfully",
"reservation": {
"id": "RES_001",
"slotId": "SLOT_01",
"customerId": "C001",
"orderId": "O001"
}
}
If the slot becomes unavailable:
{
"success": false,
"message": "The selected delivery slot is unavailable",
"alternatives": [
{
"id": "SLOT_02",
"startTime": "14:00",
"endTime": "16:00"
},
{
"id": "SLOT_03",
"startTime": "16:00",
"endTime": "18:00"
}
]
}
21. HTTP Status Codes
The API can also use appropriate HTTP status codes.
For successful booking:
201 Created
Invalid request:
400 Bad Request
Unknown slot:
404 Not Found
Slot unavailable:
409 Conflict
Server/database failure:
500 Internal Server Error
Using:
409 Conflict
for an unavailable shared resource is particularly useful because the request itself can be valid while the resource state has changed.
22. Idempotency
There is another important issue.
Imagine the customer clicks:
Book
and the request succeeds.
But the network is slow.
The frontend doesn't receive the response.
The customer clicks again.
Now the backend receives:
Request 1
Request 2
Both may attempt to reserve capacity.
This can create duplicate reservations.
A common solution is an idempotency key.
For example:
Idempotency-Key: 8d7f9c21
The backend stores the result associated with that key.
If the same request arrives again:
Same Idempotency Key
|
v
Existing result?
|
Yes
|
v
Return previous result
Instead of creating another reservation.
23. Database Constraints
Application logic should not be the only protection.
The database should also enforce important invariants where possible.
For example, we want:
bookedCount <= capacity
The exact constraint strategy depends on the database.
This leads to an important backend principle:
Business-critical invariants should be protected as close to the data as practical.
The application controls the workflow, while the database protects data integrity.
24. Why "Read Then Write" Is Dangerous
This pattern is common:
const slot = await getSlot();
if (slot.bookedCount < slot.capacity) {
slot.bookedCount++;
await updateSlot(slot);
}
It looks reasonable, but under concurrency it becomes:
READ
↓
CHECK
↓
WAIT
↓
WRITE
Another request can modify the resource during that window.
A safer pattern is:
CONDITIONAL UPDATE
For example:
UPDATE delivery_slots
SET booked_count = booked_count + 1
WHERE id = ?
AND booked_count < capacity;
This moves the critical decision into the database operation itself.
25. Edge Cases
A production-ready implementation should consider more than the happy path.
Missing customer
Customer ID is required
Missing order
Order ID is required
Missing slot
Preferred slot ID is required
Unknown slot
Delivery slot not found
Full slot
Find alternatives
No alternatives
No available delivery slots
Concurrent booking
Use atomic reservation
Reservation creation failure
Rollback transaction
Duplicate booking
Use idempotency / unique constraints
Slot in the past
The system should reject expired slots:
startTime <= currentTime
Cancelled reservation
If a customer cancels:
bookedCount = bookedCount - 1
This operation should also be performed safely.
26. Cancellation
Booking isn't the only operation that affects capacity.
Suppose:
Capacity = 5
Booked = 5
A customer cancels.
We need:
Booked = 4
A safe operation could be:
UPDATE delivery_slots
SET booked_count = booked_count - 1
WHERE id = ?
AND booked_count > 0;
Again, the condition protects the invariant:
bookedCount >= 0
27. Testing Concurrent Booking
Concurrency should not only be discussed theoretically.
It should also be tested.
Suppose:
Capacity = 100
Booked = 99
Send:
10 concurrent requests
Expected result:
1 successful reservation
9 rejected requests
The final state must be:
Capacity = 100
Booked = 100
Never:
Booked = 101
A conceptual test:
const requests = Array.from(
{ length: 10 },
(_, index) =>
bookDeliverySlot({
customerId: `C${index}`,
orderId: `O${index}`,
preferredSlotId: "SLOT_01"
})
);
const results = await Promise.allSettled(requests);
const successfulBookings =
results.filter(
result =>
result.status === "fulfilled" &&
result.value.success
);
console.log(
successfulBookings.length
);
For a slot with one remaining position, the expected successful booking count is:
1
28. Important Architecture Principle
This problem demonstrates an important backend concept:
A delivery slot is not just a database record. It is a shared, limited resource.
Whenever multiple requests can consume the same limited resource, we need to think about:
Concurrency
+
Consistency
+
Atomicity
+
Data Integrity
The same concepts appear in:
- Inventory systems
- Seat booking
- Hotel rooms
- Appointment scheduling
- Warehouse capacity
- Payment processing
- Ticket booking
- Restaurant reservations
- Rate-limit counters
The resource changes, but the concurrency problem remains similar.
29. A Better Production Architecture
A production system might separate responsibilities into layers:
Controller
|
v
Booking Service
|
v
Slot Repository
|
v
Database
Controller
Responsible for:
HTTP request
HTTP response
Input extraction
Service
Responsible for:
Business rules
Booking workflow
Alternative selection
Transactions
Repository
Responsible for:
Database queries
Atomic updates
Persistence
This separation makes the system easier to test and maintain.
30. Example Service Architecture
class DeliverySlotService {
async book({
customerId,
orderId,
preferredSlotId
}) {
this.validateRequest({
customerId,
orderId,
preferredSlotId
});
const slot =
await this.slotRepository.findById(
preferredSlotId
);
if (!slot) {
throw new Error(
"Delivery slot not found"
);
}
const reservation =
await this.slotRepository.reserve(
preferredSlotId,
customerId,
orderId
);
if (!reservation) {
const alternatives =
await this.slotRepository
.findAlternatives(
preferredSlotId
);
return {
success: false,
alternatives
};
}
return {
success: true,
reservation
};
}
}
The service doesn't need to know the details of SQL or MongoDB.
That's the responsibility of the repository.
31. Key Lessons
The most important lessons from delivery-slot allocation are:
1. Checking availability isn't enough
This:
if (bookedCount < capacity)
doesn't guarantee safe booking under concurrency.
2. Protect the critical section
Use an atomic operation:
UPDATE ...
WHERE booked_count < capacity
3. Use transactions when multiple changes must succeed together
For example:
Reserve capacity
+
Create reservation
4. Handle concurrency explicitly
Assume that multiple users can request the same resource simultaneously.
Don't design the system as if requests are processed one at a time.
5. Provide alternatives
A failed booking doesn't have to mean a bad user experience.
Return useful alternatives.
6. Protect against duplicate requests
Idempotency is important for booking and payment-like operations.
7. Test the race condition
Concurrency bugs often don't appear during normal testing.
Stress the critical operation with simultaneous requests.
32. Final Implementation Strategy
The complete strategy can be summarized as:
Receive Request
↓
Validate Input
↓
Find Slot
↓
Is Slot Valid?
↓ ↓
No Yes
↓ ↓
Error Atomic Reservation
↓
Reservation Successful?
↓ ↓
No Yes
↓ ↓
Find Alternatives Create Reservation
↓ ↓
Return Result Commit Transaction
↓
Booking Success
The core invariant is:
bookedCount <= capacity
And the critical rule is:
Do not separate the availability check
from the capacity update when concurrency matters.
Instead of:
READ
↓
CHECK
↓
UPDATE
prefer:
CONDITIONAL ATOMIC UPDATE
For example:
UPDATE delivery_slots
SET booked_count = booked_count + 1
WHERE id = ?
AND booked_count < capacity;
Then combine the reservation creation with a transaction when both operations must remain consistent.
Conclusion
Delivery slot allocation may initially look like a simple CRUD operation:
Find slot
→ Check capacity
→ Increment booked count
But once multiple customers can access the same slot concurrently, the problem becomes a concurrency and data-consistency problem.
A robust implementation therefore needs:
Validation
+
Atomic operations
+
Transactions
+
Concurrency control
+
Alternative slots
+
Idempotency
+
Database integrity
The most important principle is simple:
When multiple requests compete for a limited shared resource, the operation that consumes that resource must be concurrency-safe.
This principle goes far beyond delivery slots. It is one of the fundamental ideas behind reliable backend systems.
Top comments (0)