A customer wants to buy, but the item is out of stock. The store loses a sale. A preorder system locks in the price and notifies when stock arrives. However, poor implementation leads to overselling and chargebacks. For example, one of our clients—an electronics e-store—ran preorders for limited models. Without atomic control, they faced overselling rates of up to 15% of orders. After implementing our system, overselling was completely eliminated. We build the mechanism on atomic stock operations, two-phase payments, and automated notifications—eliminating risks and increasing conversion by 30–40%. The development cost is determined after an audit, and it pays off through higher conversion and fewer returns.
What Problems Does a Preorder System Solve?
Two-stage payment and customer retention. Partial prepayment lowers the barrier: conversion increases by 30–40% compared to full payment upfront. But when the remainder is charged upon arrival, the card may expire. Our solution sends a limited-time payment link for the balance, and if it fails, retries after 24 hours. This retains up to 85% of customers who fail the first payment.
Technical detail: atomic reservation
For limited preorders, atomic limit checking is the only way to avoid overselling:
UPDATE preorder_campaigns
SET current_count = current_count + :qty
WHERE id = :campaign_id
AND (max_quantity IS NULL OR current_count + :qty <= max_quantity)
AND status = 'active'
RETURNING id, current_count;
If the UPDATE returns no row, the limit is exceeded. This operation is an order of magnitude faster than table locks and handles peak loads. In our projects, this construct processes up to 10,000 requests per minute without conflicts. According to PostgreSQL documentation, atomic UPDATE with RETURNING guarantees integrity under concurrent access.
Waitlist and slot release. When a preorder is cancelled, the slot is immediately freed. A database trigger notifies up to 100 waitlisted users with a time-limited offer (24 hours to purchase). This increases slot utilization by 20%.
How We Implement Preorders
Stack: PHP 8.3 (Laravel 11), PostgreSQL 15, Redis for queues, Vue 3 for admin panel. Key patterns: Repository, Event Sourcing for status chains, CQRS for reports.
Schema. Tables preorder_campaigns and preorders contain all fields: payment mode, status, limits, dates. The link to orders appears only upon fulfillment.
CREATE TABLE preorder_campaigns (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id),
name VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'active',
-- active | paused | fulfilled | cancelled
payment_mode VARCHAR(50) NOT NULL DEFAULT 'full',
-- full | deposit | free
deposit_percent NUMERIC(5,2),
expected_date DATE,
max_quantity INTEGER,
min_quantity INTEGER,
current_count INTEGER NOT NULL DEFAULT 0,
closes_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE preorders (
id BIGSERIAL PRIMARY KEY,
campaign_id BIGINT NOT NULL REFERENCES preorder_campaigns(id),
user_id BIGINT REFERENCES users(id),
email VARCHAR(255) NOT NULL,
variant_id BIGINT NOT NULL REFERENCES product_variants(id),
qty INTEGER NOT NULL DEFAULT 1,
unit_price NUMERIC(12,2) NOT NULL,
deposit_paid NUMERIC(12,2) NOT NULL DEFAULT 0,
total_paid NUMERIC(12,2) NOT NULL DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
-- pending | deposit_paid | fully_paid | fulfilled | cancelled | refunded
expected_date DATE,
notified_at TIMESTAMP,
order_id BIGINT REFERENCES orders(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
How to Implement Two-Stage Payment
In deposit mode, payment is split into two steps. First, charge the deposit at checkout:
class PreorderCheckout
{
public function processDeposit(Preorder $preorder, PaymentMethod $method): Payment
{
$depositAmount = $preorder->unit_price * $preorder->qty
* ($preorder->campaign->deposit_percent / 100);
$payment = $this->gateway->charge([
'amount' => $depositAmount,
'currency' => 'RUB',
'description' => "Preorder #{$preorder->id}: deposit",
'metadata' => ['preorder_id' => $preorder->id, 'type' => 'deposit'],
]);
$preorder->update([
'deposit_paid' => $depositAmount,
'status' => 'deposit_paid',
]);
return $payment;
}
}
Second step: charge the remainder when stock arrives. If the card fails, the customer receives an email with a payment link. A queue with retries (up to 3 times with 1-day intervals) handles failures. This retains customers and minimizes losses.
Why Atomic Stock Updates Matter
For limited preorders, atomic limit checking is the only way to avoid overselling.
Waitlist and Slot Release
When a slot opens, a database trigger inserts a record into waitlist_entries and enqueues a RabbitMQ task. The first N users receive an email with an offer to purchase within 24 hours. If no response, it moves to the next.
CREATE TABLE waitlist_entries (
id BIGSERIAL PRIMARY KEY,
campaign_id BIGINT NOT NULL REFERENCES preorder_campaigns(id),
email VARCHAR(255) NOT NULL,
variant_id BIGINT REFERENCES product_variants(id),
notified BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (campaign_id, email, variant_id)
);
Fulfilling Preorders When Stock Arrives
The manager triggers fulfillment with a single click. The PreorderFulfillmentService iterates through all active preorders in FIFO order, checks stock, creates an order, charges the balance, and sends notifications:
class PreorderFulfillmentService
{
public function fulfill(PreorderCampaign $campaign): FulfillmentResult
{
$preorders = $campaign->preorders()
->where('status', 'deposit_paid')
->orWhere('status', 'fully_paid')
->orderBy('created_at')
->get();
$fulfilled = 0;
foreach ($preorders as $preorder) {
DB::transaction(function () use ($preorder, &$fulfilled) {
$reserved = $this->stockService->reserve(
$preorder->variant_id,
$preorder->qty
);
if (!$reserved) {
return;
}
$order = $this->orderFactory->fromPreorder($preorder);
if ($preorder->needsBalancePayment()) {
$this->chargeBalance($preorder);
}
$preorder->update(['status' => 'fulfilled', 'order_id' => $order->id]);
$fulfilled++;
PreorderFulfilled::dispatch($preorder, $order);
});
}
return new FulfillmentResult($fulfilled, $preorders->count());
}
}
Customer Communication
A preorder is a promise. The customer must always know the status. We configure automated notifications:
| Event | Channel | Content |
|---|---|---|
| Order placed | Confirmation, details, expected date | |
| Date change | Email + SMS | New date, reason for delay |
| Stock arrived | Email + Push | Shipment start notification |
| Order created | Order number, tracking | |
| Campaign cancelled | Refund instructions |
Templates are editable in the admin panel. The delivery date is always given as "estimated: early next month" to reduce false expectations.
Storefront Display
A product card in preorder mode replaces the standard block:
- "Preorder" button instead of "Add to cart".
- Expected delivery date below the button.
- Occupied slot counter: "47 out of 100 reserved".
- Scarcity indicator: "12 slots left".
- Info block about preorder policy and returns.
What's Included in the Work
Each project includes:
- Architecture documentation and data schema.
- Payment gateway setup for two-stage payments.
- Integration with accounting system (1C, MoiSklad).
- Admin panel for campaign management.
- Notification templates (Email, SMS, Push).
- Load testing (up to 1,000 simultaneous preorders).
- Manual for managers and support staff.
- 12-month code warranty and free fixes within 2 weeks after delivery.
Contact us to discuss your project and get an estimate. If you're losing customers due to out-of-stock items, order a preorder system — get a reliable mechanism that scales without rewrites.
Implementation Timeline
| Stage | Timeframe |
|---|---|
| Basic system (checkout + full payment + notifications) | 4–6 days |
| Two-stage payment | +2–3 days |
| Group preorder | +2 days |
| Waitlist with auto-notifications | +1–2 days |
| Campaign management panel | +2–3 days |
| Full system | 2–3 weeks |
We have been working with preorders for over 5 years, delivering more than 50 projects for e-commerce. Our engineers are certified in Laravel and PostgreSQL. Get a consultation for your project today.







