Розробка аукціону в реальному часі: сервер, клієнт, антиснайпінг

На минулому тижні до нас прийшов клієнт: його аукціон «зависав», коли два користувачі одночасно ставили на один лот — одна ставка зникала, інша дублювалася. Втрати становили до $5000 за лот. Така ситуація знайома багатьом власникам аукціонів. Ми розробляємо аукціонні системи, які виключають подібні

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка аукціону в реальному часі: сервер, клієнт, антиснайпінг
Середній
~5 днів

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1422
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    984
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1250
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    986
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    1000

На минулому тижні до нас прийшов клієнт: його аукціон «зависав», коли два користувачі одночасно ставили на один лот — одна ставка зникала, інша дублювалася. Втрати становили до $5000 за лот. Така ситуація знайома багатьом власникам аукціонів. Ми розробляємо аукціонні системи, які виключають подібні помилки «з коробки». За 7 років ми реалізували понад 30 проєктів для e-commerce та торгових майданчиків.

Проблеми, які вирішуємо

Race condition при паралельних ставках — найпоширеніша біда. Два запити обробляються одночасно, кожен бачить поточну ціну $100, обидва записують $110 — перемагає перший, другий має бути відхилений. Без блокування клієнт отримує хибне підтвердження, а аукціон — спірну ситуацію. Ми усуваємо це через песимістичне блокування Redis, виключаючи подвійні записи.

Хибне визначення переможця — проблема через несинхронізовані таймери. Сервер показує 5 секунд до кінця, але через затримку мережі клієнт вважає, що залишилося 10. Ставка йде вчасно, але сервер вже закрив лот. Втрати для організатора — тисячі доларів. Наш підхід — синхронізація часу через WebSocket із серверним таймером.

Антиснайпінг — коли учасник чекає останньої секунди і «краде» лот. Без механізму продовження аукціон втрачає чесність. Ми додаємо автоматичне продовження на 2 хвилини після кожної ставки у фінальній фазі.

Як ми це робимо: стек та ключові рішення

Обираємо Node.js (NestJS для строгої архітектури) та PostgreSQL — перевірена зв'язка для транзакційних навантажень. Песимістичне блокування через Redis SETNX — щоб жоден запит не проскочив без блокування. WebSocket (Socket.IO) — для миттєвих update всіх клієнтів; fallback на long-polling, якщо мережа блокує WebSocket. Фронтенд — React 18 з Next.js (SSR для SEO) та TypeScript, компоненти на Tailwind.

Порівняємо два підходи до блокування:

Метод Продуктивність Надійність Складність
Оптимістичне (retry) Висока, але повторні запити Низька при високій конкуренції Низька
Песимістичне (Redis lock) Середня, блокування на 5 с Висока, виключає race condition Середня

Ми використовуємо песимістичне блокування як більш надійне для аукціонів з високою конкуренцією.

Як запобігти конфлікту ставок?

Песимістичне блокування через Redis — найнадійніше. Алгоритм:

class AuctionService { async placeBid(auctionId: string, userId: string, amount: number): Promise<BidResult> { const lockKey = `lock:auction:${auctionId}`; const lockAcquired = await redis.set(lockKey, userId, 'NX', 'PX', 5000); if (!lockAcquired) { throw new Error('Auction is processing another bid, try again'); } try { const auction = await auctionRepo.findById(auctionId); if (auction.status !== 'active') throw new BidError('Auction is not active'); if (new Date() > auction.endsAt) throw new BidError('Auction has ended'); if (amount <= auction.currentPrice) { throw new BidError(`Bid must be higher than ${auction.currentPrice}`); } if (amount < auction.currentPrice + auction.minIncrement) { throw new BidError(`Minimum increment is ${auction.minIncrement}`); } if (auction.currentLeaderId === userId) { throw new BidError('You are already the highest bidder'); } const bid = await bidRepo.create({ auctionId, userId, amount }); await auctionRepo.updateCurrentPrice(auctionId, amount, userId); // Anti-sniping const timeLeft = auction.endsAt.getTime() - Date.now(); if (timeLeft < 2 * 60 * 1000) { const newEndTime = new Date(Date.now() + 2 * 60 * 1000); await auctionRepo.extendTime(auctionId, newEndTime); } await this.broadcastBid(auctionId, bid, auction); return { success: true, bid }; } finally { await redis.del(lockKey); } } async broadcastBid(auctionId: string, bid: Bid, auction: Auction) { io.to(`auction:${auctionId}`).emit('bid:new', { bidId: bid.id, amount: bid.amount, bidderId: bid.userId, bidderName: anonymizeBidder(bid.userId), timestamp: bid.createdAt, totalBids: auction.bidCount + 1, newEndTime: auction.endsAt }); } } 

Чому антиснайпінг критичний для аукціону?

Без нього будь-який учасник може дочекатися останніх мілісекунд і виграти без конкуренції. Наш таймер автоматично продовжується на 2 хвилини, даючи шанс відповісти. Сервер транслює оновлений час усім клієнтам через WebSocket.

class AuctionTimer { async startTimer(auctionId: string, endTime: Date): Promise<void> { const msUntilEnd = endTime.getTime() - Date.now(); setTimeout(async () => { await this.finalizeAuction(auctionId); }, msUntilEnd); const broadcastInterval = setInterval(async () => { const remaining = endTime.getTime() - Date.now(); if (remaining <= 0) { clearInterval(broadcastInterval); return; } if (remaining <= 60000) { io.to(`auction:${auctionId}`).emit('timer:tick', { remaining: Math.ceil(remaining / 1000) }); } }, 1000); } async finalizeAuction(auctionId: string): Promise<void> { const auction = await auctionRepo.findById(auctionId); if (auction.status !== 'active') return; await auctionRepo.finalize(auctionId); io.to(`auction:${auctionId}`).emit('auction:ended', { winnerId: auction.currentLeaderId, winnerName: await getUserName(auction.currentLeaderId), finalPrice: auction.currentPrice }); await this.notifyParticipants(auction); } } 

Клієнтський компонент на React

Усі дані надходять по WebSocket — компонент реагує миттєво. Жодних зайвих re-render, лише точкові оновлення.

function AuctionRoom({ auctionId }) { const [auction, setAuction] = useState<AuctionState>(); const [bids, setBids] = useState<Bid[]>([]); const [timeLeft, setTimeLeft] = useState<number>(0); const socket = useSocket(); useEffect(() => { if (!socket) return; socket.emit('auction:join', { auctionId }); socket.on('auction:state', (state) => setAuction(state)); socket.on('bid:new', (bid) => { setBids(prev => [bid, ...prev].slice(0, 50)); setAuction(prev => prev ? { ...prev, currentPrice: bid.amount } : prev); }); socket.on('timer:tick', ({ remaining }) => setTimeLeft(remaining)); socket.on('auction:ended', ({ winnerId, finalPrice }) => { setAuction(prev => prev ? { ...prev, status: 'ended' } : prev); if (winnerId === currentUserId) { showCongratulations(finalPrice); } }); return () => socket.emit('auction:leave', { auctionId }); }, [socket, auctionId]); const placeBid = async (amount: number) => { try { await fetch(`/api/auctions/${auctionId}/bids`, { method: 'POST', body: JSON.stringify({ amount }) }); } catch (e) { showError(e.message); } }; return ( <div className="auction-room"> <CurrentPrice price={auction?.currentPrice} /> <AuctionTimer seconds={timeLeft} critical={timeLeft < 30} /> <BidForm minBid={(auction?.currentPrice ?? 0) + (auction?.minIncrement ?? 100)} onBid={placeBid} disabled={auction?.status !== 'active'} /> <BidHistory bids={bids} currentUserId={currentUserId} /> </div> ); } 

Процес роботи

  1. Аналітика — дивимося на бізнес-логіку, визначаємо типи аукціонів (англійський, голландський, закритий), навантаження, інтеграції з платіжними системами.
  2. Проектування — малюємо архітектуру: схему БД, протокол WebSocket, стратегію блокувань, anti-sniping, таймери.
  3. Реалізація — пишемо сервер і клієнт паралельно. На кожен спринт — демо.
  4. Тестування — unit-тести, інтеграційні тести, навантажувальне тестування (до 2000 RPS). Перевіряємо race condition і таймінги.
  5. Деплой — налаштовуємо CI/CD, контейнеризацію, моніторинг (Grafana, Prometheus). Передаємо документацію та доступи.

Що входить в роботу

  • Повна кодова база сервера (Node.js + TypeScript) та клієнта (React/Next.js).
  • Документація API (Swagger) та архітектурна схема.
  • Навантажувальне тестування зі звітом.
  • Інструкція з розгортання та налаштування моніторингу.
  • Один місяць підтримки після запуску (виправлення багів, консультації).

Строки орієнтовно

Комплектація Строк
Базовий аукціон: ставки, таймер, історія 2–3 тижні
+ Антиснайпінг, сповіщення, email-розсилки +1 тиждень
+ Мультилот, інтеграція з платіжними системами, особистий кабінет 4–6 тижнів

Точні строки оцінюємо після аналізу вашого ТЗ. Зв'яжіться з нами — надішлемо оцінку за 1 день.

Типові помилки при розробці аукціонів

Помилка Наслідок Наше рішення
Відсутність блокування ставок Race condition, подвійні ставки Песимістичне блокування Redis
Реалізація таймера лише на клієнті Розсинхронізація часу Серверний таймер з broadcast
Немає anti-sniping Лоти йдуть за мінімальною ціною Автоматичне продовження на 2 хв
Ігнорування затримок мережі Запізнілі ставки WebSocket з підтвердженням

«Після впровадження системи кількість спірних ставок знизилася до нуля» — клієнт з e-commerce.

Приклад конфлікту ставок та його вирішення

Одного разу на аукціоні два учасники одночасно подали ставки по $1000. Без блокування обидва запити пройшли, і система зафіксувала дві ставки. Після впровадження нашого блокування другий запит отримує помилку і клієнт повторює ставку з більшою сумою. У результаті — чесна перемога першого.

Уникнути цих граблів допомагає наш досвід — 7 років у розробці real-time систем, понад 30 завершених проєктів для e-commerce та аукціонів. Гарантуємо стабільність та масштабованість.

Якщо ви хочете таку саму систему, замовте консультацію — обговоримо деталі та приступимо. Отримайте консультацію — розкажіть про ваш аукціон, і ми запропонуємо оптимальне рішення.