Sealed-bid vehicle auction platform
A remarketing platform where a lender sells repossessed and off-lease vehicles to dealers by sealed bid: rounds open and close on a timer, nobody sees anyone else's price, and the system decides the winners — including a tie-breaker round when two dealers bid the same amount.
- Go
- Gin
- PostgreSQL
- RabbitMQ
- Redis
- Next.js
- React
- TypeScript
- Centrifugo
- MinIO
- Keycloak
- Docker
Overview
A finance company ends up owning cars it never wanted: repossessions, end-of-lease returns, insurance write-offs. Selling them one by one is slow, and an open auction invites collusion. This platform runs the sale as a sealed bid: dealers see the vehicle, not each other's offers, rounds close on a fixed timer, and the system works out the winners the moment a round ends.
Back-office staff import a vehicle list from a spreadsheet, and the system splits it into rounds and lanes across the auction day. Dealers browse, shortlist and bid. Winners get a payment deadline; the cars that nobody bid on roll into a later round automatically.
The parts that were interesting to build
Closing a round is not a countdown. A timer held in memory dies with the process and drifts across replicas. Instead, creating a round schedules a row in a job table with the time it is due. Workers poll that table and claim rows with SELECT … FOR UPDATE SKIP LOCKED, so several workers can run without ever handing the same job to two of them. Before a close job does anything it re-reads the round and checks that the end time still matches the one it was scheduled for — that single comparison is what makes "extend the round by ten minutes" safe, because the superseded job wakes up, sees a different end time and exits.
Ranking, ties, and an audit trail. Bids are never updated in place. Re-bidding soft-cancels the previous row with a reason and inserts a new one, so the log stays append-only and a dispute can be replayed. Order is not decided at write time either — at close, bids are ranked by amount, then by arrival time as the tie-break. One top bidder wins. Two or more at the same amount do not get resolved by a coin toss: the vehicle opens a sub-round whose floor price is the tied bid and whose whitelist is exactly those bidders.
Real time, used for the right thing. A push server sits in front of the browser, but not in the bid path. Bidding stays a plain request that is validated and written; the push channel carries per-user notifications, and the client responds by invalidating its cache and refetching. Prices are never pushed as payloads, so a dropped socket can never leave one dealer looking at a stale number while another sees the truth.
Shape of the system
Thirty-two deployables in four layers — a gateway per audience, an orchestration tier, business services, and data services that each own their tables. Time-triggered work travels by message queue; everything else is REST between layers. Images arrive through a two-step upload that puts a file in a temporary key first and only promotes it when the record is saved, so an abandoned form leaves nothing behind.
Scale
- 32 deployable services · 114 database tables · around 2,000 registered routes
- 102 screens across the staff and dealer applications
- Two languages, with the auction domain alone owning 17 tables and 37 migrations
- Status
- Published
- Published
- Sep 12, 2026
- Stack
- Go · Gin · PostgreSQL · RabbitMQ · Redis · Next.js · React · TypeScript · Centrifugo · MinIO · Keycloak · Docker
Keep exploring