Обратно в блог
  • backend
  • DevOps

Будьте в курсе всех событий

Как устроена архитектура подключения партнёров в Яндекс Еде: тогда и сейчас

Если попросить разработчика представить, как отображается ресторан в базе данных, первое, что приходит на ум — простая таблица с полями id, name и address. В целом недалеко от истины. Вот только в реальности агрегатора доставки, особенно такого крупного, как Яндекс Еда, ресторан — это нечто большее, чем просто одна таблица с парой строчек в БД.

Ресторан или магазин представляют из себя некую сущность, которую мы внутри называем Place. Это разветвлённый граф объектов: зоны доставки, сложноструктурированное меню, расписания, юридические лица, налоговые ставки, фотографии и десятки других параметров, разбросанных по разным сервисам.

Чтобы ресторан начал принимать заказы, все кусочки этого непростого пазла должны быть сложены идеально.

01.png

С 2019 года на нашем сервисе появилось более 600 000 сущностей Place. Только за 2025 год мы создали более 100 000 новых точек. С каждым годом мир вокруг требует всё больших и больших скоростей. И если раньше вполне нормально было потратить на добавление одного ресторана в систему до нескольких недель, то сейчас мы уже не можем себе такого позволить.

Всем привет, меня зовут Николай Митрофанов, я руководитель службы разработки в Яндексе Еде. В этой статье я расскажу, как мы перешли от простых транзакций в PHP-монолите к асинхронным очередям, почему это решение тоже оказалось временным и как мы в итоге пришли к управлению состоянием через конечные автоматы в отдельном микросервисе. Всё это ради того, чтобы сократить среднее время запуска одного Place с 31 дня в 2022 году до 11 дней в 2025-м.

Этап 1. Монолит и «Большой JSON» (2019 год)

Начнём с того, как всё выглядело, когда деревья были большими, а Еда — монолитной.

До 2019 года архитектура Еды была классической для того времени — монолит на PHP. Процесс создания ресторана выглядел весьма тривиально: оператор заходил в административную панель, заполнял несколько полей в веб-форме, и эти данные синхронно раскладывались по соответствующим таблицам в базе данных. Это работало, пока поток заявок был небольшим, а бизнес-процессы — линейными.

Переломным моментом стал 2019 год. Бизнес тогда поверил в модель агрегатора, началась активная экспансия, которая запустила механизмы масштабирования и интеграции с другими сервисами Яндекса. Например в тот год мы интегрировались с биллингом. Стало очевидно, что руками через админку больше масштабироваться не получится. Команды продаж и контента нуждались в профессиональном инструменте для работы с воронкой и лидами. И таким инструментом стала для нас CRM Salesforce.

Это изменило весь поток данных на архитектурном уровне. Теперь мастером данных о ресторане становилась CRM. В ней менеджеры заводили всё: от адреса и юридических лиц до комиссий. В нашу систему эта информация прилетала уже не через формочки, а через API в виде огромного, глубоко вложенного JSON-объекта из более чем 100 полей.

Паттерн Visitor и транзакционная целостность

Структура Place — это не плоский список полей. Даже простое меню ресторана — это дерево из категорий, товаров, опций и групп опций. Чтобы переварить такой массив разнородной информации внутри PHP-монолита, мы выбрали паттерн Visitor.

Логика была следующей:

  1. На вход поступает контейнер с данными — условный place.json.
  2. Мы запускаем конвейер из специализированных классов-визиторов. Каждый визитор отвечает строго за свой домен: один парсит и сохраняет адрес, другой — рассчитывает и пишет комиссии, третий — обрабатывает меню.
  3. Весь процесс оборачивается в одну большую транзакцию в базе данных.

Схематично это выглядело так: открываем транзакцию → применяем AddressVisitor → применяем CommissionVisitor → … → применяем N-Visitor → коммит.

02.png

У такого подхода были железобетонные плюсы, критически важные для целостности данных:

  • Детерминированность: порядок выполнения визиторов всегда строго определён в коде.
  • Идемпотентность: благодаря уникальным индексам и транзакционной природе мы были защищены от создания дублей.
  • Атомарность: мы не могли получить недозаполненный Place, у которого есть адрес, но нет комиссии. Либо создаётся полноценная сущность, либо ничего.

Ловушка строгой валидации и походы во внешние сервисы

Ирония в том, что именно главный плюс архитектуры — принцип «всё или ничего» — стал её главным минусом.

Передав ввод данных в CRM, мы потеряли контроль над валидацией на фронтенде. В нашей старой админке мы могли навесить жёсткие регулярки на каждое поле. В Salesforce такой гибкости валидации из коробки не было, или она была настроена недостаточно строго.

В итоге к нам начали поступать невалидные данные. Представьте ситуацию: менеджер заполнил огромную анкету ресторана, потратил на это огромное количество времени, но допустил всего лишь одну маленькую опечатку в формате расписания рабочих часов. На уровне нашего монолита транзакция доходила до проблемного места, падала с ошибкой, и весь созданный прогресс откатывался.

Дополнительную проблему создавали вызовы внешних сервисов внутри транзакции. Мы не могли повлиять на время исполнения внешнего запроса, что в свою очередь периодически приводило к дедлокам, поскольку транзакция продолжительное время оставалась открытой.

Ресторан физически не мог попасть в сервис из-за одной небольшой ошибки в десятках полей. Да, такая система была стабильной и предсказуемой, но получилась слишком хрупкой для работы с внешними, не всегда качественными данными.

Этап 2. Асинхронность против пандемии (2020 год)

Весной 2020 года бизнес столкнулся с экзистенциальным вызовом невиданного ранее масштаба. Локдаун обрушил офлайн-рынок общепита, и рестораны массово кинулись в доставку, чтобы выжить. Для нас это означало не просто рост нагрузки, а необходимость переработать сам процесс подключения партнёров.

Старая схема, где контент-менеджеры вручную проверяли и заводили каждый ресторан, стала тем самым бутылочным горлышком. Людей физически не хватало, чтобы обрабатывать все поступающие заявки. Бизнес поставил жёсткий дедлайн: за два месяца реализовать систему Self-Registration — инструмент, где ресторан мог сам заполнять меню, рисовать зоны доставки и загружать документы.

Очереди вместо транзакций

Мы прекрасно понимали, что качество данных, которые будут вводить сами рестораторы, неизбежно окажется ниже, чем из CRM. И они 100% будут ошибаться гораздо чаще, чем обученные менеджеры. Жёсткая транзакционная модель «всё или ничего» в такой ситуации просто парализует весь процесс: малейшая ошибка в одной форме заблокирует создание всего аккаунта.

Пришлось полностью менять архитектуру. Мы сохранили логику визиторов, но разорвали единую транзакцию.

В новой схеме v2:

  • Базовая сущность: сначала создаётся минимальный каркас сущности Place с базовыми полями. Это делается быстро и просто.
  • Асинхронное насыщение: дальше каждый визитор (AddressVisitor, MenuVisitor, ZoneVisitor) запускается как отдельная задача в очереди. Для этого мы использовали STQ/RabbitMQ.
  • Изоляция контекстов: ошибка при сохранении расписания больше не влияет на создание юридического лица или загрузку меню потому что эти процессы стали независимыми.
03.png

Для пользователей (в данном случае — ресторанов) мы внедрили систему «семафоров»: интерфейс показывал статус заполненности каждого блока. Если меню загружено, зоны отрисованы, а договор подписан — загорался зелёный свет, и ресторан мог самостоятельно нажать кнопку «Запустить», отправив себе транзакционное письмо об активации.

Потеря детерминизма

Плюсы перехода на очереди были очевидны: система стала горизонтально масштабируемой. Мы могли просто добавить воркеров, чтобы разгрести пиковую нагрузку, не блокируя базу длинными транзакциями.

Но за всё приходится платить. И нам пришлось пойти на усложнение логики и потерю прозрачности:

  • Недетерминированный порядок. В очередях мы не могли гарантировать, что зона доставки будет создана после того, как создан регион, к которому она привязана.
  • Зависимости данных. Пришлось писать дополнительный код для валидации целостности. Например, чтобы проверять: есть ли у нас бренд перед созданием меню.
  • Вредные ретраи. Если задача падала из-за отсутствия зависимых данных, она уходила в ретрай. Это создавало паразитный трафик и усложняло отладку. Приходилось выяснять, почему визитор зоны упал 5 раз подряд? Потому что база тормозила или потому что визитор региона ещё не отработал?

Система не была идеальной, но она выжила и дала возможность спасти бизнес в пандемию. Впрочем, мы понимали, что это техдолг, который придётся возвращать.

Этап 3. Миграция Delivery Club и новый контракт данных (2022 год)

К лету 2022 года нас ждал новый вызов — интеграция бизнеса Delivery Club. Задача стояла конкретная: мигрировать около 120 000 активных ресторанов в нашу систему, плюс какое-то количество архивных. При этом мы ещё параллельно переезжали с Salesforce на внутреннюю YaCRM.

Анализ данных Delivery Club показал, что их структура сущности ресторана очень похожа на ту JSON-схему, с которой мы уже работали при интеграции Salesforce. Это навело нас на мысль: вместо того чтобы писать кастомные скрипты миграции на один раз, нужно создать универсальный контракт данных.

Рождение сервиса eats-place-onboarding

Реализовывать такой контракт в старом PHP-монолите было бы как минимум неправильно. Поэтому мы выделили логику приёма и адаптации данных в отдельный микросервис — eats-place-onboarding.

04.png

Этот сервис стал единой точкой входа для любых внешних систем: будь то YaCRM или дампы из Delivery Club. Он взял на себя две ключевые функции:

  1. Трансформация: приведение разнородных данных к каноническому виду.
  2. Валидация: проверка обязательных полей и бизнес-правил до того, как данные попадут в ядро системы.

Внутри сервиса мы ввели чёткое разделение состояний заявки, которого нам так не хватало в монолите:

  • Draft. Представляет из себя заготовку сущности Place с множеством опциональных полей. При создании Draft выполняется базовая валидация на соответствие типов данных.
  • Candidate. Данные из Draft перекладываются 1 в 1 или трансформируются по заданной бизнес логике. Если Draft может быть из разных сервисов (внешний сервис/crm), то Candidate — это нормализованное представление заготовки сущности Place.
  • Place. Финальная сущность в мастер-системе, готовая к работе.

Такая архитектура позволила нам провести миграцию базы Delivery Club относительно безболезненно. Мы загружали данные как черновики, прогоняли их через пайплайн валидации и трансформации в eats-place-onboarding, и только корректные записи отправляли в API админки Еды для создания ресторанов.

Это решение, кроме всего прочего, стало фундаментом для дальнейшего распила монолита. Мы увидели, что сервис онбординга отлично справляется с ролью оркестратора создания ресторана, и решили передать ему ещё больше ответственности.

Этап 4. Распил монолита и State Machine (2024–2025)

К 2024 году бизнес поставил новую задачу: монолит больше не должен быть мастером данных. PHP-код стало сложно развивать, а логика создания ресторана всё ещё оставалась там. Мы решили перенести эту ответственность в eats-place-onboarding, который отлично показал себя во время миграции Delivery Club.

Однако просто переписать визиторы с PHP на другой язык было недостаточно. Нам нужно было решить архитектурную проблему, которую мы создали сами себе в 2020 году — недетерминированность очередей. Бесконечные ретраи из-за того, что «бренд ещё не создан», отнимали ресурсы и усложняли мониторинг.

Оркестрация через ProcaaS

Вместо очередей мы внедрили управление состоянием через ProcaaS. Это внутренняя реализация конечного автомата, которая позволяет описывать бизнес-процесс как набор строгих стадий и переходов. Сейчас мы бы рассмотрели возможность управления workflow через temporal, но на тот момент выбор procaas был оптимальным.

05.png

Теперь пайплайн создания ресторана выглядит не как набор разрозненных тасков, а как чёткая последовательность шагов:

  • Draft → Candidate: валидация входных данных из CRM.
  • Enrichment: обогащение данными (например, подтягивание геокоординат).
  • Creation/Update: последовательный вызов доменных сервисов.

Новая роль визиторов

Мы перенесли паттерн Visitor в сервис онбординга, но теперь визиторы работают иначе. Они больше не стучатся в базу монолита, а ходят в специализированные микросервисы через API:

  • ZoneVisitor создаёт зоны доставки в сервисе eats-place-zones.
  • PartnerVisitor регистрирует юридические лица в eats-partners.
  • Данные реплицируются в новое хранилище eats-place-storage.

В монолите осталась лишь рудиментарная логика создания базовой «заготовки» Place для поддержки легаси-интерфейсов, но вся бизнес-логика и оркестрация переехали в микросервисы.

Event-Driven развитие

Помимо синхронных вызовов, мы начали активно использовать Logbroker — это такой аналог Kafka в Яндексе. Изменения в состоянии ресторана теперь публикуются в топик, откуда их могут читать любые заинтересованные сервисы. Например, сервис кэширования или маркетинговых триггеров может узнать о появлении нового ресторана мгновенно, не опрашивая базу.

Результаты и влияние на продукт

Главным итогом этой архитектурной эволюции стало не просто удобство разработки, а реальные бизнес-метрики. В 2022 году среднее время запуска ресторана (от появления данных в системе до первого принятого заказа) составляло 31 день. К началу 2025 года мы сократили этот показатель до 11 дней. Ускорение почти в три раза — это результат совместной работы инженеров, продактов и команды контента.

06.png

Новая архитектура развязала руки продуктовой команде. Раньше любая фича упиралась в жёсткость монолита. Теперь мы можем быстро внедрять инструменты самообслуживания для партнёров:

  • Self-Service зоны доставки: рестораны теперь сами рисуют полигоны на карте через интерфейс, а сервис валидации геоданных проверяет их корректность. Раньше это было доступно только для нашей собственной логистики.
  • AI-описания блюд: мы интегрировали генерацию текстов для меню. Ресторатору достаточно загрузить фото и название, а нейросеть сама создаст подходящее описание.
  • Регистрация магазинов выполняется по аналогии с ресторанами. Ребята из команды ритейла независимо от команды ресторанов меняют workflow под свои потребности. Мы не «толкаемся» локтями в коде одного сервиса.

Сейчас мы движемся к тому, чтобы монолит окончательно перестал быть мастером данных о ресторанах. Все данные будут жить в микросервисах, а админка превратится в тонкий клиент. Мы планируем развивать инструменты склейки меню для сетевых брендов и управления контентом в рамках одной физической точки.

В заключение хочется отметить, что архитектура — это не застывшая в камне статуя, а живой организм. Она должна меняться под воздействием внешней среды и адаптироваться. Наш опыт показывает: иногда нужно пожертвовать красивыми транзакциями ради масштабируемости, а потом вернуться к строгости типов, но уже на новом витке спирали.

Главное — не бояться переписывать то, что работало вчера, если сегодня это мешает бизнесу расти.

  • backend
  • DevOps

Будьте в курсе всех событий