A real-time, multi-phase auction platform supporting descending (Dutch) and ascending (English) auction mechanics with round-based elimination.
- Akarsh Malik
- Hashmmath Shaik
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser (Client) β
β β
β Next.js 16 + React 19 + Tailwind CSS β
β βββ HTTP REST βββββββββββββββββββββββββββββββββββββββΊ β
β βββ WebSocket (STOMP over SockJS) βββββββββββββββββββΊ β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β :3000
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spring Boot API (Java 25) β
β β
β Controllers (REST) β
β βββ EngagementController /api/engagements β
β βββ SubmissionController /api/engagements/:id/* β
β βββ UserController /api/users β
β β
β Services (Strategy Pattern) β
β βββ DescendingAuctionService - Dutch auction logic β
β βββ AscendingAuctionService - Closed English auction logic β
β βββ OpenAscendingAuctionService - Open English auction logic β
β β
β WebSocket β
β βββ /topic/engagements/:id/status - phase events β
β βββ /topic/engagements/:id - live rate updates β
β β
β Scheduler β
β βββ AuctionScheduler - auto-transition auctions: β
β β’ CLOSED (PHASE_1_SEALED for Descending, PENDING for β
β Ascending + phase2StartTime past) β PHASE_2_LIVE β
β β’ OPEN (PENDING + openStartTime past) β auto-start β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β :8080
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL 16 (snatch_db) β
β β
β Tables: users, engagements, submissions, registrations β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Closed Auctions (single winner for the entire auction)
- Descending (Dutch) Auction: Two-phase structure with sealed bids followed by live round-based elimination.
- Ascending (English) Auction: Single-phase live bidding starting from a base rate, with bids increasing until the target price is met or time expires.
Open Auctions (multiple items, multiple winners)
- Ascending (English) Auction: Single-phase live bidding per item, with items auctioned sequentially in real-time.
Phase 1 - Sealed Bids
- Bidders register and submit sealed offers blindly (no visibility into other bids)
- The auctioneer sets a Phase 1 deadline; no bids are accepted after it expires
- The auctioneer manually triggers the transition to Phase 2
Phase 2 - Live Round-Based Bidding
- A countdown timer runs for each round (configurable, default 30s)
- Each bidder submits their own rate independently; their bid must beat their own previous bid
- Only the best bid across all bidders updates the global live rate
- Bidders see a signal (CLOSE / MID / FAR) indicating how competitive they are
- When the round timer expires:
- 0 bids submitted β auction closes
- 1+ bids submitted β non-bidders are eliminated; a new round starts
- Bidders can quit at any time; the auction ends only when all bidders have quit
- Auction starts immediately after registration period (for Closed) or at scheduled time (for Open)
- Bidders place live bids in real-time
- For Closed Ascending: Bids must be higher than the current rate; auction ends when target price is met or time expires
- For Open Ascending: Items are auctioned sequentially; each item has a grace period for first bid, then live bidding with silence resets
- Starting rate =
min(maxStartingRate, lowestPhase1Bid) - Phase 2 bids must be strictly lower than the bidder's own last bid
- Winner = bidder with the lowest final rate when the auction closes
- Closed Format: Starting rate set by auctioneer; bids increase until target price is met
- Open Format: Per-item bidding with configurable grace periods and silence resets
- Winner(s) = bidder(s) with the highest final rate(s) meeting reserve prices
ScheduledExecutorService- per-auction countdown timers for Phase 2 rounds, scheduled independently so auctions don't block each otherConcurrentHashMap- thread-safe in-memory state for active bidders, round participants, and scheduled timersConcurrentHashMap.newKeySet()- lock-free concurrentSetfor per-round bidder tracking- Pessimistic Locking -
@Lock(LockModeType.PESSIMISTIC_WRITE)on the engagement fetch insideprocessLiveRate, preventing race conditions when multiple bidders submit simultaneously
- Strategy Pattern -
AuctionEngineinterface implemented byDescendingAuctionServiceandAscendingAuctionService; the controller resolves the correct engine at runtime based on auction type - Dependency Injection with
@Qualifier- both engines are Spring beans;@Qualifier("descendingEngine")/@Qualifier("ascendingEngine")disambiguates injection
- Spring WebSocket + STOMP -
SimpMessagingTemplate.convertAndSend()broadcasts real-time events (phase transitions, rate updates, round starts, auction close) to all connected subscribers @Transactional- ensures bid saves and live rate updates are atomic; rolled back on failure@Scheduled-AuctionSchedulerpolls every 5 seconds to auto-transition auctions whosephase2StartTimehas elapsed- Spring Data JPA - repository pattern with derived query methods and custom
@Queryannotations
- BCrypt (jbcrypt) - passwords are hashed with
BCrypt.hashpw()at registration and verified withBCrypt.checkpw()at login; plaintext passwords are never stored
- Hibernate + PostgreSQL dialect - JPA entities mapped to tables with
@Entity,@ManyToOne,@Enumerated - Lombok
@Data- auto-generates getters, setters,equals,hashCode, andtoStringat compile time via annotation processing
| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS v4 |
| Backend | Spring Boot 4.0.5, Java 25 |
| WebSocket | STOMP over SockJS (@stomp/stompjs) |
| Database | PostgreSQL 16 |
| ORM | Hibernate / Spring Data JPA |
| Auth | BCrypt password hashing |
| Build | Maven (Spring Boot Maven Plugin) |
| Container | Docker, Docker Compose |
All database credentials live in a single .env file at the project root. Both docker-compose.yml (which reads it automatically) and the Spring Boot app (via environment variable placeholders) use the same values.
Setup:
cp .env.example .envThen edit .env to set your credentials:
DB_NAME=DB_NAME
DB_USER=DB_USER
DB_PASSWORD=DB_PASSWORD
.envis gitignored. Never commit it. Commit.env.exampleinstead (already included).
How the values flow:
| Variable | Used by |
|---|---|
DB_NAME |
PostgreSQL container (POSTGRES_DB) + Spring Boot (spring.datasource.url) |
DB_USER |
PostgreSQL container (POSTGRES_USER) + Spring Boot (spring.datasource.username) |
DB_PASSWORD |
PostgreSQL container (POSTGRES_PASSWORD) + Spring Boot (spring.datasource.password) |
Spring Boot reads these via placeholders in application.properties:
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:snatch_db}
spring.datasource.username=${DB_USER:root}
spring.datasource.password=${DB_PASSWORD:snatch_it}The DB_HOST variable is set to postgres (the Docker service name) by docker-compose.yml for containerised runs, and defaults to localhost for local development.
Requires Docker and Docker Compose v2.
# Clone the repo
git clone https://github.com/malikakarsh/Snatch.git
cd Snatch
# Configure credentials
cp .env.example .env
# Edit .env if you want non-default values
# Build and start all three services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop everything
docker-compose down
# Stop and remove the database volume (full reset)
docker-compose down -v| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| API | http://localhost:8080 |
| PostgreSQL | localhost:5432 |
The first build takes a few minutes β Maven downloads dependencies and Next.js compiles the app. Subsequent builds are faster due to Docker layer caching.
- Java 25
- Node.js 22+
- PostgreSQL 16 running locally on port 5432
cp .env.example .env
# Edit .env with your local Postgres credentials if different from defaultsThe Spring Boot app reads DB_NAME, DB_USER, and DB_PASSWORD directly from the environment, with defaults matching .env.example. For local dev you can either:
- Export them in your shell:
export $(cat .env | xargs) - Or leave the defaults as-is (
snatch_db/root/snatch_it) and create a matching Postgres user
Create the database and user to match your .env:
CREATE USER root WITH PASSWORD 'snatch_it';
CREATE DATABASE snatch_db OWNER root;cd api
./mvnw spring-boot:runAPI is available at http://localhost:8080.
cd client
npm install
npm run devFrontend is available at http://localhost:3000.
Snatch/
βββ api/ # Spring Boot backend
β βββ src/main/java/com/snatch/api/
β β βββ config/ # WebSocket, CORS config
β β βββ controllers/ # REST endpoints
β β βββ models/ # JPA entities
β β βββ repositories/ # Spring Data JPA repos
β β βββ services/ # Auction engines + scheduler
β β βββ AuctionScheduler.java
β β βββ DescendingAuctionService.java
β β βββ AscendingAuctionService.java
β β βββ OpenAscendingAuctionService.java
β βββ src/main/resources/
β β βββ application.properties # All config via env vars
β βββ Dockerfile
β βββ pom.xml
βββ client/ # Next.js frontend
β βββ app/
β β βββ components/ # AuctionCard, OfferForm, AuthProvider
β β βββ login/
β β βββ register/
β β βββ page.tsx # Main dashboard
β βββ lib/api.ts # Typed API client
β βββ Dockerfile
β βββ package.json
βββ .env # Local credentials (gitignored)
βββ .env.example # Template to copy from
βββ docker-compose.yml
βββ README.md
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/users/register |
Register with email, password, role |
| POST | /api/users/login |
Login, returns user object |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/engagements |
List all auctions |
| POST | /api/engagements |
Create auction (BEARER only) |
| POST | /api/engagements/:id/register |
Register as bidder |
| POST | /api/engagements/:id/sealed-offers |
Submit Phase 1 bid |
| POST | /api/engagements/:id/transition |
Start Phase 2 (BEARER only) |
| POST | /api/engagements/:id/live-offers |
Submit Phase 2 bid |
| POST | /api/engagements/:id/quit |
Withdraw from auction |
| GET | /api/engagements/:id/my-status |
Get bidder's own status + signal |
| Topic | Event |
|---|---|
/topic/engagements/:id/status |
PHASE_2_LIVE, ROUND_START, CLOSED, CANCELLED |
/topic/engagements/:id |
Live rate update { currentLiveRate } |









