Прямой обмен почти никогда не складывается: что мы поняли, пока строили бартерную платформу
Цепочки из трёх-четырёх участников работают лучше, чем прямые сделки. Переговоры без денег требуют большей инженерной дисциплины, чем с деньгами. И ещё несколько открытий, сделанных по ходу разработки.
Когда начинаешь делать бартерную платформу, кажется, что главное — дать пользователю выложить товар, а другому — предложить обмен. Каталог, поиск, кнопка «предложить обмен». Всё как в обычном маркетплейсе, только без денег.
Потом оказывается, что два человека с взаимным интересом к товарам друг друга — это статистическая редкость. Что переговоры без платёжного эскроу требуют более жёсткой архитектуры, чем переговоры с ним. И что избранное — это не фича для удобства пользователя, а топливо для поиска обменных цепочек.
Barterri — платформа для бартерного обмена товарами и услугами. Люди публикуют объявления, находят предложения, договариваются и завершают сделки. Никаких денег, только взаимная полезность. Звучит просто. Но внутри это оказалось совсем не маркетплейсом.
Прямой обмен — это исключение, а не правило
Когда мы начинали, логика была простая: пользователь A хочет товар пользователя B, пользователь B хочет товар пользователя A. Сделка.
Проблема в том, что такое совпадение — редкость. В реальности один хочет велосипед, но предлагает книги. Другой хочет книги, но предлагает ремонт техники. Третий хочет ремонт, но предлагает велосипед. По отдельности ни одна пара не сходится. А вместе — замкнутый круг, где каждый отдаёт своё и получает чужое.
Именно это открытие и определило архитектуру. Платформа должна не просто сводить пары, а находить циклы в графе желаний.
Избранное стало не списком «посмотреть позже». Каждый добавленный в избранное лот — это ребро графа от одного пользователя к товару другого. На этом графе мы запускаем поиск цепочек из трёх-четырёх участников. Нашли цикл — предложили цепочку всем участникам. Каждый подтверждает — сделка резервируется.
Здесь вылезла своя сложность: цепочек на одном графе может быть много, и одна и та же комбинация участников с одними и теми же лотами не должна предлагаться дважды. Пришлось вводить стабильную сигнатуру цепочки — хеш от идентификаторов участников и лотов — и дедуплицировать по ней. Без этого платформа заспамила бы пользователей одинаковыми предложениями.
Переговоры без денег требуют большей дисциплины, чем с деньгами
В обычном маркетплейсе есть платёж. Покупатель заплатил — продавец отправил. Если что-то пошло не так, есть чек, банк, арбитраж. Деньги сами по себе держат сделку.
В бартере этого якоря нет. Никто никому не платит. Единственное, на чём держится сделка — это договорённость сторон. И если платформа позволяет эту договорённость случайно нарушить, доверие исчезает мгновенно.
Первая мысль была простая: сделаем оффер — предложение обмена — и пусть стороны его редактируют, пока не договорятся. Но редактируемый оффер — это проблема. Допустим, я предложил вам обменять велосипед на книги. Вы подумали и согласились. Но пока вы думали, я изменил состав оффера — добавил ещё один товар. Вы нажали «принять», даже не заметив изменений.
Чья версия сделки теперь действует? На что вы на самом деле согласились?
Мы пришли к тому, что каждая версия оффера должна быть неизменяемой. Не «редактировать предложение», а «создать новую ревизию». Предыдущая версия заморожена навсегда. Когда сторона принимает оффер, она принимает конкретную ревизию. Если за время раздумий появилась новая — платформа обязана сказать: «Вы принимаете устаревшую версию, подтвердите ещё раз».
Это не overengineering. Это единственный способ построить доверие в системе, где нет денег, которые могли бы это доверие заменить. Когда единственная гарантия сделки — это взаимное согласие, платформа не имеет права путать, на какую версию согласия вы дали.
И да, это замедлило разработку. Зато теперь каждая сделка имеет недвусмысленную, зафиксированную историю: кто что предложил, кто что принял и в какой момент. Без этого бартер — не платформа, а доска объявлений.
Фотографии не должны проходить через backend
В маркетплейсе объявлений фотография — это не украшение. Это главный аргумент. Пользователь принимает решение по картинке, а не по описанию. И картинок может быть много — у каждого объявления до десятка снимков в хорошем разрешении.
Если гнать этот трафик через backend — принимать файлы в API, обрабатывать, складывать в хранилище — сервер начинает работать файловым хостингом, а не бизнес-логикой. Масштабировать такое больно, а пользы от проксирования — ноль.
Мы вынесли загрузку на сторону клиента. Backend выдаёт presigned-разрешение на прямую загрузку в объектное хранилище. Фронт отправляет файлы напрямую, минуя сервер приложения. Ключи привязаны к пользователю, формат и размер проверяются до подписания, разрешение действует десять минут.
Но самое интересное — не сам механизм presigned-загрузки. Он известный и документированный. Интересно следствие для продакшен-инфраструктуры: маршрут /media/ не должен вести в backend. Если reverse proxy направит запрос к /media/ в приложение, а приложение не хранит медиа локально, получится либо 404, либо попытка отдать файл из несуществующего пути. И то и другое — ошибка, которую не видно при разработке, где медиа часто лежат на диске.
Поэтому в production-конфигурации роутер backend-сервиса сознательно не принимает запросы к /media/. Это не забыли. Это часть архитектуры.
Реальное время не должно жить в отдельном сервисе
Чат и уведомления в бартерной платформе — не самостоятельные продукты. Чат привязан к конкретному офферу или цепочке. Уведомление сообщает об изменении статуса сделки. Сообщение в чате и смена ревизии оффера — это события одного процесса переговоров.
Первая мысль: вынести realtime в отдельный сервис. Микросервис для чата, свой WebSocket, своя база, свой деплой. Красивая схема, знакомая по многим проектам.
Но тогда чат и доменная логика живут в разных мирах. Сообщение в чате «я согласен на эти условия» не знает, что в доменной модели только что создалась новая ревизия оффера. Уведомление о принятии оффера летит из другого сервиса и может опередить или отстать от реального изменения в базе. А главное — транзакционная целостность между переговорным процессом и его обсуждаемой версией теряется.
Мы оставили всё в монолите. Приложение через ASGI обслуживает и HTTP, и WebSocket. Чаты, уведомления и бизнес-логика разделяют одну сессию, одну модель прав, одну базу. Сообщение в чате и смена статуса оффера могут жить в одной транзакции. Redis-канал доставляет события подписчикам, но источник истины — один.
Это не значит, что монолит всегда лучше микросервисов. Это значит, что для задачи, где чат и есть часть сделки, а не отдельный продукт, разделение на сервисы создало бы больше проблем, чем решило.
Что получилось
В Barterri в итоге есть:
- каталог объявлений с поиском, картой, категориями и модерацией;
- автоматический подбор цепочек обмена по графу избранного;
- офферы с неизменяемыми ревизиями для безопасных переговоров;
- чат и уведомления, работающие внутри той же транзакционной модели;
- прямая загрузка изображений в объектное хранилище;
- прод-инфраструктура с раздельной маршрутизацией SPA и API через общий reverse proxy.
Но список фич никогда не расскажет, почему прямой обмен — это исключение, а цепочки — правило. Почему редактируемый оффер страшнее, чем отсутствие оффера вообще. Почему фотографии не должны трогать backend, а чат не должен из него уходить.
Мне кажется, это и есть самое интересное в разработке — не то, что мы сделали. А то, что мы поняли, пока делали.