Skip to content
All projects

PublishedFeatured

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