A flash-sale backend that prevents oversells under extreme load by making Redis the fast, atomic source-of-truth for inventory and offloading durable work to background queues. Designed to show how to safely accept bursts of concurrent orders while keeping the database protected, maintainable, and auditable.
PHP 8.5 · Laravel 12 · PostgreSQL · Redis · PHPUnit · Shell scripts (ab/joined clients)
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/products/{id} |
Get product details |
POST |
/api/orders |
Make order request |
POST |
/api/payments/webhook |
Handle payment webhook |
The initial version relied heavily on direct database interactions with pessimistic locking. While this ensured strong data consistency, it introduced a significant bottleneck during high-traffic bursts. Each request tied up a database connection, and sequential row locking meant that requests queued up, leading to high latency and reduced throughput (~56 req/sec).
This approach is available in the before-redis-queues branch.
To solve the concurrency bottleneck, the system was refactored to decouple stock reservation from order persistence.
- During the flash sale window, Redis is the temporary source of truth for inventory.
- PostgreSQL becomes the source of truth after payment confirmation.
- Redis stock is reconciled back to the database through background jobs and webhook finalization.
- Queues process the "heavy" database writes in the background.
A side-by-side comparison using Apache Benchmark (ab) demonstrating the impact of refactoring a synchronous database-heavy architecture to an asynchronous model using Redis and Queues.
Key Results:
- Requests per second (RPS) jumped from
56.44to933.61, representing a16.5ximprovement. - Average time per request dropped from
1771msto107ms. - The total time required to process 1000 concurrent requests decreased from
17.71seconds to1.07seconds.
Benchmarks were run locally on a single application instance on nginx with Redis and PostgreSQL running locally.
The flow diagram illustrates the asynchronous logic:
- Incoming
POST /ordersrequests hit theOrderController, which attempts an atomic decrement on a Redis Stock key. - If the atomic operation is successful, a
ProcessOrderCreationJob is dispatched to the queue to persist the order in the Orders table. - If Redis stock reaches zero, the system immediately returns a 409 Conflict error without hitting the database.
- Reconciliation Orders:
- Runs a
CancelUnpaidOrderstask every minute to find old pending orders and restore the stock in Redis. - The
WebhookControllerlistens for payment provider updates to finalize order status in the database.
- Runs a
| Scenario | Handling / Guarantee |
|---|---|
| Redis Crash | If Redis keys are evicted or the service restarts, RedisStockService automatically rebuilds the stock cache from the database on the next request (Lazily), ensuring high availability. |
| Worker Failure | If the queue worker crashes while processing an order, Laravel's queue retries ensure the final consistency of the system. |
| Race Conditions (Early Webhook) | If a payment webhook arrives before the order is created in the database (due to async lag), the system records the webhook as pending. When the order job finally runs, it checks for this pending webhook and immediately updates the status to 'paid' or 'cancelled'. |
| Overselling Protection | Redis Lua scripts ensure that stock decrements are strictly atomic. Even with 1000 concurrent requests, it is impossible for the stock counter to drop below zero. |
| Stale Reservations | If a user reserves stock but never pays, the orders:release scheduled command (running every minute) cancels the order and restores the inventory to Redis. |
git clone https://github.com/r6mez/Flash-Sale-Task
cd Flash-Sale-TaskCopy .env.example into a .env file, and change postgres configurations to meet yours.
Create a database called flash_sale_api or whatever name you put in the .env file then run
composer setup
php artisan db:seed- this will add 2 products to the db.
php artisan app:sync-stock-to-redis- this will sync the stock of products from db to redis, run before running the server.
composer devThis runs concurrently:
- Laravel server at
http://localhost:8000 - Queue worker for canceling expired orders as a background job.
directly with PHPUnit (for sequential tests):
php artisan testFirst, create a JSON file named order_data.json with the following content:
{
"product_id": 1,
"qty": 2,
}Then, run the following command:
ab -n 1000 -c 100 -p ~/order_data.json -T application/json -k http://flash-sale.test/api/orders- for the older version without redis and queues, checkout the branch
before-redis-queues
If you find this project useful, a star is appreciated.


