Ми розробляємо аукціонні додатки, де кожна мілісекунда затримки — втрачений лот. Уявіть: на торгах за рідкісний предмет залишається 30 секунд, і раптом сервер не відповідає 2 секунди — користувач втрачає шанс зробити ставку. Такі сценарії ми закриваємо архітектурою на WebSocket та оптимістичним блокуванням. Нижче розберемо ключові технічні рішення: WebSocket з автоперепідключенням, обробку гонки умов, автоставки (proxy bidding) та anti-sniping.
Аукціонний додаток — це не просто каталог товарів. Його серце — реальний час, де узгодженість даних критична. Без правильної системи версій та бізнес-логіки користувачі бачитимуть застарілі ціни та втрачатимуть ставки. Розглянемо перевірену архітектуру, що витримує тисячі одночасних з'єднань.
Як розробити мобільний додаток для аукціону з нуля?
Проектування починається з вибору транспорту та протоколу синхронізації. Ми використовуємо WebSocket (протокол описаний у RFC 6455) та оптимістичне блокування на рівні бази даних. Визначаємо події: BID_PLACED, AUCTION_EXTENDED, AUCTION_ENDED, YOU_WON, YOU_WERE_OUTBID. Кожна подія містить версію лота для перевірки узгодженості.
Чому WebSocket, а не polling?
Polling для аукціону — неприйнятно: затримка в 5-10 секунд при опитуванні HTTP призводить до втрачених ставок. WebSocket з перепідключенням — стандарт, що забезпечує затримку <100 мс. Це в 50-100 разів швидше, ніж polling, що критично для аукціонів з високою активністю.
| Метод | Затримка | Надійність |
|---|---|---|
| Polling (HTTP) | 5-10 секунд | Середня — часті запити навантажують сервер |
| WebSocket | <100 мс | Висока — автоматичне відновлення з'єднання |
Реалізація на Swift та Kotlin:
Swift WebSocket (URLSessionWebSocketTask)
// iOS: WebSocket через 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() // продовжуємо слухати
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 ?? "")
}
}
}
Kotlin WebSocket (OkHttp)
// 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)
}
}
})
}
}
Як уникнути гонки умов при одночасних ставках?
Найскладніша бізнес-логіка: два користувачі роблять ставку одночасно. Приклад:
- Поточна ставка: 1000 ₴
- Користувач A бачить 1000 ₴, робить ставку 1100 ₴
- Користувач B бачить 1000 ₴, робить ставку 1050 ₴
- Обидві ставки приходять на сервер в один момент
Правильне рішення — оптимістичне блокування з версією лота. Воно краще за песимістичне блокування, оскільки не блокує рядок на читання та забезпечує високу пропускну здатність. Сервер перевіряє версію лота перед оновленням:
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:
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)
При LOT_UPDATED клієнт отримує актуальну ставку та може запропонувати користувачеві зробити нову з урахуванням поточної ціни. Так ми уникаємо подвійних списань та некоректного балансу.
Як працює автоставка?
Користувач встановлює максимальну суму, яку готовий заплатити. Система автоматично підвищує його ставку у відповідь на ставки конкурентів — до встановленого максимуму.
def process_autobid(lot_id: str, new_bid_amount: Decimal, new_bidder_id: str):
"""Перевіряємо автоставки після кожної нової ставки"""
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:
# Автоматично ставимо від імені власника автоставки
place_bid(autobid.user_id, lot_id, counter_amount, lot.version)
break
Автоставка — джерело конфліктів з користувачами, тому важлива детальна історія: «Ваша ставка 1 200 ₴ була автоматично підвищена у відповідь на ставку 1 100 ₴». Користувач завжди бачить, що сталося, і може скоригувати ліміт.
Що таке anti-sniping?
Снайпінг — ставка в останні секунди. Щоб його уникнути, аукціон продовжується при ставці в кінці:
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()
})
Таймер на клієнті синхронізується з серверним часом при отриманні AUCTION_EXTENDED. Це дає всім учасникам рівні шанси на відповідну ставку.
Як організувати платежі?
Переможець отримує push-сповіщення та має обмежений час (24–48 годин) для оплати. Якщо не оплатив — лот переходить наступному за ставкою або виставляється повторно.
Для цінних лотів — передоплата перед участю: депозит блокується при реєстрації на торги, повертається тим, хто програв.
Комісія з продажу (покупця або продавця) утримується при оплаті. Payouts продавцю — через Stripe Connect або аналог. Якщо вам потрібна нестандартна схема розрахунків, зв'яжіться з нами — підберемо рішення.
Орієнтири за термінами
| Scope | Термін |
|---|---|
| Перегляд лотів, WebSocket, ставки, push-сповіщення | 6–8 тижнів |
| Автоставки, anti-sniping, історія ставок | +2 тижні |
| Кабінет продавця, публікація лотів, payouts | +3–4 тижні |
| Депозити та escrow | +1–2 тижні |
Вартість розраховується індивідуально після аналізу вимог.
Що входить в роботу?
- Технічне завдання та прототип UI/UX
- Розробка під iOS (Swift, SwiftUI) та Android (Kotlin, Jetpack Compose)
- Backend на Python/FastAPI або Node.js
- Інтеграція WebSocket з перепідключенням
- Реалізація автоставок та anti-sniping
- Платіжна система (депозити, escrow, payouts)
- Тестування (unit, UI, load)
- Публікація в App Store та Google Play
- Документація та навчання адміністраторів
- Гарантійна підтримка 3 місяці
Отримайте консультацію по вашому проекту. Ми оцінимо терміни та запропонуємо оптимальне рішення. Наш досвід гарантує стабільну роботу аукціону навіть при високому навантаженні. Зв'яжіться з нами, щоб обговорити архітектуру та деталі реалізації.







