A highly concurrent, event-driven backend architecture for a real-time auction system. This API handles secure user authentication, precise background task scheduling, row-level database locking to prevent bid race conditions, and real-time WebSockets to broadcast live price updates.
This project is built with a robust, production-ready microservices approach, fully containerized using Docker.
| Layer | Technology | Role |
|---|---|---|
| Core Framework | Django & Django Rest Framework | REST API, ORM, Admin |
| Database | PostgreSQL 15 | Transactional storage with row-level locking |
| Authentication | djangorestframework-simplejwt |
JWT access & refresh tokens |
| Task Queue | Celery + Redis | Deferred auction settlement |
| WebSockets / ASGI | Django Channels + Daphne | Real-time bid broadcasts |
| Message Broker | Redis | Channels layer & Celery broker |
| Reverse Proxy | Nginx | Route /api/ β Gunicorn, /ws/ β Daphne |
| Containerization | Docker & Docker Compose | 6-service orchestration |
Our backend adheres to a strictly decoupled, event-driven architecture. Service boundaries are enforced at the container level via Docker Compose, replicating the isolation guarantees of a production microservices deployment. Nginx acts as the single entry point, deterministically routing traffic to the appropriate application server based on protocol.
graph TD
A[Client / Frontend] --> B(Nginx Reverse Proxy)
B -->|/api/ /admin/| C(Gunicorn / Django REST API)
B -->|/ws/| D(Daphne / Django Channels)
C <--> E(PostgreSQL)
C -->|Publish Events| F(Redis)
C -->|Enqueue Tasks| F
D <--> F
D <-->|Read Models| E
F --> G[Celery Worker]
G <-->|Settle Auctions| E
G -->|Broadcast auction_ended| F
E -.->|SELECT ... FOR UPDATE| C
F -.->|WebSocket Push| D
D -.->|Live Price Broadcast| A
- Custom User & Wallet System: Extended Django User model with
wallet_balance. Every new user starts with βΉ1000.00 for immediate bidding. - Concurrency Control: Utilizes PostgreSQL's
SELECT ... FOR UPDATEwithintransaction.atomic()to strictly serialize concurrent bids, ensuring zero race conditions when multiple users bid at the exact same millisecond. - Automatic Wallet Refunds: When a new highest bid is placed, the previous highest bidder's wallet balance is atomically refunded inside the same database transaction.
- Automated Auction Settlement: Celery tasks are scheduled with
apply_async(eta=end_time)at auction creation, automatically closing auctions at their exact expiry without polling. - Real-Time Price Broadcasts: Daphne and Redis publish instant WebSocket updates (
new_bidandauction_endedevents) to all connected clients the moment a bid clears or an auction concludes. - Admin Dashboard: Fully customized Django admin interface for managing users, balances, and active auctions.
BidStream/
βββ docker-compose.yml # 6-service orchestration
βββ stress_test.py # Concurrency stress test (50 simultaneous bids)
βββ nginx/
β βββ default.conf # Reverse proxy routing rules
βββ bid/ # Django project root
βββ Dockerfile
βββ requirements.txt
βββ manage.py
βββ bid/ # Project config
β βββ settings.py
β βββ urls.py
β βββ wsgi.py # Gunicorn entrypoint
β βββ asgi.py # Daphne entrypoint
β βββ celery.py # Celery app config
βββ auctions/ # Core application
βββ models.py # User, Auction, Bid models
βββ views.py # API views with row-level locking
βββ serializers.py # DRF serializers
βββ urls.py # REST endpoint routing
βββ consumers.py # WebSocket consumers
βββ routing.py # WebSocket URL patterns
βββ tasks.py # Celery settlement task
βββ admin.py # Admin customization
Ensure you have Docker and Docker Compose installed on your machine.
-
Clone the repository:
https://github.com/VIDIT45AGARWAL/BidStream.git cd BidStream -
Spin up the Docker containers:
docker compose up --build
This command will build the images and spin up all 6 services: PostgreSQL, Redis, Gunicorn (Django), Daphne (WebSockets), Celery Worker, and Nginx.
-
Apply Database Migrations: Open a new terminal window and run:
docker compose exec web python manage.py migrate -
Create a Superuser (Admin):
docker compose exec web python manage.py createsuperuser -
Access the Application:
Endpoint URL API Base http://localhost/api/Admin Panel http://localhost/admin/WebSocket ws://localhost/ws/auctions/<auction_id>/
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/auth/register/ |
Register a new user account |
POST |
/api/auth/login/ |
Obtain JWT access and refresh tokens |
POST |
/api/auth/token/refresh/ |
Refresh an expired access token |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/auctions/ |
List all active auctions (sorted by end_time) |
POST |
/api/auctions/ |
Create a new auction item (Auth Required) |
Create Auction Payload:
{
"title": "Vintage Watch",
"description": "A rare 1960s timepiece in mint condition.",
"starting_price": "100.00",
"end_time": "2026-06-25T18:00:00Z"
}| Method | Endpoint | Description |
|---|---|---|
POST |
/api/bids/ |
Place a bid on an active auction (Auth Required) |
Place Bid Payload:
{
"auction": 1,
"amount": "250.00"
}Guardrails: Rejects bids lower than current price, self-bidding, bids on expired/closed auctions, and bids exceeding wallet balance.
| Method | Endpoint | Description |
|---|---|---|
DELETE |
/api/users/<id>/ |
Delete a user account (Requires is_staff=True) |
Connect your frontend to receive live updates when bids are placed or auctions conclude:
URL: ws://localhost/ws/auctions/<auction_id>/
Broadcast to all connected clients when a successful bid is placed:
{
"event": "new_bid",
"current_price": "250.00",
"bidder_username": "buyer_bob"
}Broadcast when the Celery worker settles an expired auction:
{
"event": "auction_ended",
"winner": "buyer_bob",
"final_price": "250.00",
"message": "This auction has concluded."
}This project includes a purpose-built stress test that proves the PostgreSQL row-level locking mechanism prevents race conditions. The script uses asyncio + aiohttp to fire 50 identical bid requests at the exact same millisecond from a single user, then verifies that exactly 1 bid is accepted and the remaining 49 are safely rejected.
sequenceDiagram
participant Script as stress_test.py
participant API as Django API
participant DB as PostgreSQL
Script->>API: 50Γ POST /api/bids/ (βΉ1000, same user)
API->>DB: Thread 1: SELECT ... FOR UPDATE (acquires lock)
API--xDB: Thread 2β50: BLOCKED (waiting for lock)
DB-->>API: Thread 1: Validates & inserts bid
API-->>Script: Thread 1: 201 Created β
DB-->>API: Thread 2: Acquires lock, reads updated price
API-->>Script: Thread 2: 400 Rejected β (bid β€ current_price)
API-->>Script: Thread 3β50: 400 Rejected β
-
Start the stack:
docker compose up --build docker compose exec web python manage.py migrate -
Setup test data:
- Create a seller account and a buyer account via
/api/auth/register/ - Login as the seller via
/api/auth/login/and create an auction via/api/auctions/ - Login as the buyer and copy the
accesstoken
- Create a seller account and a buyer account via
-
Configure and run:
pip install aiohttp
Update
stress_test.pywith yourTOKENandAUCTION_ID, then:python stress_test.py
Firing 50 bids for $1000.00 at the exact same millisecond
----------------------------------------
Completed in 0.45 seconds.
Successful bids (201 Created): 1
Rejected bids (400 Bad Request): 49
----------------------------------------
SUCCESS! Your database locks worked perfectly.
1 bid went through, and 49 were safely rejected to prevent a race condition.
Why this matters: Without
SELECT ... FOR UPDATE, all 50 threads would read the samecurrent_price, all 50 would pass validation, and the wallet would be debited 50Γ β a classic race condition. The row-level lock serializes access so only one thread can validate and write at a time.