Developing a Mobile App for Auction
Auction app is one of most technically demanding mobile products. Reason: lot state changes in real-time, new bids appear every few seconds, user on other end expects instant results. 2-second delay showing someone else's bid—user bids on already-lost position.
Real-time: WebSocket as Foundation
Polling for auction unacceptable. WebSocket with reconnect standard:
// iOS: WebSocket via URLSessionWebSocketTask
class AuctionWebSocket {
private var webSocketTask: URLSessionWebSocketTask?
private var reconnectTimer: Timer?
func connect(auctionId: String) {
let url = URL(string: "wss://api.yourauction.com/auctions/\(auctionId)/live")!
webSocketTask = URLSession.shared.webSocketTask(with: url)
webSocketTask?.resume()
receive()
}
private func receive() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
if case .string(let text) = message {
self?.handleMessage(text)
}
self?.receive() // keep listening
case .failure:
self?.scheduleReconnect()
}
}
}
private func scheduleReconnect() {
reconnectTimer?.invalidate()
reconnectTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
self?.connect(auctionId: self?.currentAuctionId ?? "")
}
}
}
// Android: OkHttp WebSocket
class AuctionWebSocketManager(
private val client: OkHttpClient,
private val scope: CoroutineScope
) {
private val _events = MutableSharedFlow<AuctionEvent>()
val events: SharedFlow<AuctionEvent> = _events.asSharedFlow()
fun connect(auctionId: String) {
val request = Request.Builder()
.url("wss://api.yourauction.com/auctions/$auctionId/live")
.build()
client.newWebSocket(request, object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
scope.launch {
val event = json.decodeFromString<AuctionEvent>(text)
_events.emit(event)
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
scope.launch {
delay(3000)
connect(auctionId)
}
}
})
}
}
Event types from server: BID_PLACED (new bid), AUCTION_EXTENDED (time extended), AUCTION_ENDED, YOU_WON, YOU_WERE_OUTBID.
Race Condition with Concurrent Bids
Toughest business logic: two users bid simultaneously. Example:
- Current bid: 1000 ₽
- User A sees 1000 ₽, bids 1100 ₽
- User B sees 1000 ₽, bids 1050 ₽
- Both bids hit server at same time
Correct solution—optimistic locking with lot version:
def place_bid(user_id: str, lot_id: str, amount: Decimal, expected_version: int) -> BidResult:
with db.transaction():
lot = db.select_for_update(f"SELECT * FROM lots WHERE id = %s", lot_id)
if lot.version != expected_version:
# Someone already raised bid
return BidResult(
success=False,
reason="LOT_UPDATED",
current_bid=lot.current_bid,
new_version=lot.version
)
if amount <= lot.current_bid:
return BidResult(
success=False,
reason="BID_TOO_LOW",
current_bid=lot.current_bid,
new_version=lot.version
)
db.execute(
"UPDATE lots SET current_bid=%s, current_bidder=%s, version=version+1 WHERE id=%s",
(amount, user_id, lot_id)
)
db.execute(
"INSERT INTO bids (lot_id, user_id, amount) VALUES (%s, %s, %s)",
(lot_id, user_id, amount)
)
broadcast_to_websockets(lot_id, {
"type": "BID_PLACED",
"amount": str(amount),
"bidder": mask_username(user_id),
"version": lot.version + 1
})
return BidResult(success=True, new_version=lot.version + 1)
On LOT_UPDATED client gets actual bid and can offer user new bid considering current price.
Proxy Bidding
User sets max amount willing to pay. System auto-raises their bid against competitors—up to set maximum.
def process_autobid(lot_id: str, new_bid_amount: Decimal, new_bidder_id: str):
"""Check autobids after each new bid"""
autobids = db.get_active_autobids(lot_id, exclude_user=new_bidder_id)
for autobid in sorted(autobids, key=lambda x: x.max_amount, reverse=True):
counter_amount = new_bid_amount + lot.bid_step
if counter_amount <= autobid.max_amount:
# Auto-bid from autobid holder
place_bid(autobid.user_id, lot_id, counter_amount, lot.version)
break
Proxy bidding—conflict source with users, so detailed history vital: "Your bid 1,200 ₽ auto-raised responding to 1,100 ₽ bid."
Anti-Sniping Time Extension
Sniping—last-second bid. Prevent by extending when bid near end:
ANTI_SNIPING_THRESHOLD = timedelta(minutes=2)
ANTI_SNIPING_EXTENSION = timedelta(minutes=2)
def after_bid_placed(lot_id: str):
lot = db.get_lot(lot_id)
time_remaining = lot.ends_at - datetime.utcnow()
if time_remaining < ANTI_SNIPING_THRESHOLD:
new_end_time = lot.ends_at + ANTI_SNIPING_EXTENSION
db.update_lot_end_time(lot_id, new_end_time)
broadcast_to_websockets(lot_id, {
"type": "AUCTION_EXTENDED",
"new_end_time": new_end_time.isoformat()
})
Client timer syncs with server time on AUCTION_EXTENDED.
Payment Logic
Winner gets push-notification and limited time (24–48 hours) to pay. No payment—lot goes to next bidder or re-lists.
For valuable lots—prepaid registration: deposit blocked before bidding, returned to losers.
Sales commission (buyer or seller) retained on payment. Seller payouts via Stripe Connect or equivalent.
Timeline Estimates
| Scope | Timeline |
|---|---|
| Lot viewing, WebSocket, bidding, push-notifications | 6–8 weeks |
| Proxy bidding, anti-sniping, bid history | +2 weeks |
| Seller account, lot publishing, payouts | +3–4 weeks |
| Deposits and escrow | +1–2 weeks |
Pricing is calculated individually after requirements analysis.







