Если попросить разработчика представить, как отображается ресторан в базе данных, первое, что приходит на ум — простая таблица с полями id, name и address. В целом недалеко от истины. Вот только в реальности агрегатора доставки, особенно такого крупного, как Яндекс Еда, ресторан — это нечто большее, чем просто одна таблица с парой строчек в БД.
Ресторан или магазин представляют из себя некую сущность, которую мы внутри называем Place. Это разветвлённый граф объектов: зоны доставки, сложноструктурированное меню, расписания, юридические лица, налоговые ставки, фотографии и десятки других параметров, разбросанных по разным сервисам.
Чтобы ресторан начал принимать заказы, все кусочки этого непростого пазла должны быть сложены идеально.

С 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.
Логика была следующей:
- На вход поступает контейнер с данными — условный place.json.
- Мы запускаем конвейер из специализированных классов-визиторов. Каждый визитор отвечает строго за свой домен: один парсит и сохраняет адрес, другой — рассчитывает и пишет комиссии, третий — обрабатывает меню.
- Весь процесс оборачивается в одну большую транзакцию в базе данных.
Схематично это выглядело так: открываем транзакцию → применяем AddressVisitor → применяем CommissionVisitor → … → применяем N-Visitor → коммит.

У такого подхода были железобетонные плюсы, критически важные для целостности данных:
- Детерминированность: порядок выполнения визиторов всегда строго определён в коде.
- Идемпотентность: благодаря уникальным индексам и транзакционной природе мы были защищены от создания дублей.
- Атомарность: мы не могли получить недозаполненный Place, у которого есть адрес, но нет комиссии. Либо создаётся полноценная сущность, либо ничего.
Ловушка строгой валидации и походы во внешние сервисы
Ирония в том, что именно главный плюс архитектуры — принцип «всё или ничего» — стал её главным минусом.
Передав ввод данных в CRM, мы потеряли контроль над валидацией на фронтенде. В нашей старой админке мы могли навесить жёсткие регулярки на каждое поле. В Salesforce такой гибкости валидации из коробки не было, или она была настроена недостаточно строго.
В итоге к нам начали поступать невалидные данные. Представьте ситуацию: менеджер заполнил огромную анкету ресторана, потратил на это огромное количество времени, но допустил всего лишь одну маленькую опечатку в формате расписания рабочих часов. На уровне нашего монолита транзакция доходила до проблемного места, падала с ошибкой, и весь созданный прогресс откатывался.
Дополнительную проблему создавали вызовы внешних сервисов внутри транзакции. Мы не могли повлиять на время исполнения внешнего запроса, что в свою очередь периодически приводило к дедлокам, поскольку транзакция продолжительное время оставалась открытой.
Ресторан физически не мог попасть в сервис из-за одной небольшой ошибки в десятках полей. Да, такая система была стабильной и предсказуемой, но получилась слишком хрупкой для работы с внешними, не всегда качественными данными.
Этап 2. Асинхронность против пандемии (2020 год)
Весной 2020 года бизнес столкнулся с экзистенциальным вызовом невиданного ранее масштаба. Локдаун обрушил офлайн-рынок общепита, и рестораны массово кинулись в доставку, чтобы выжить. Для нас это означало не просто рост нагрузки, а необходимость переработать сам процесс подключения партнёров.
Старая схема, где контент-менеджеры вручную проверяли и заводили каждый ресторан, стала тем самым бутылочным горлышком. Людей физически не хватало, чтобы обрабатывать все поступающие заявки. Бизнес поставил жёсткий дедлайн: за два месяца реализовать систему Self-Registration — инструмент, где ресторан мог сам заполнять меню, рисовать зоны доставки и загружать документы.
Очереди вместо транзакций
Мы прекрасно понимали, что качество данных, которые будут вводить сами рестораторы, неизбежно окажется ниже, чем из CRM. И они 100% будут ошибаться гораздо чаще, чем обученные менеджеры. Жёсткая транзакционная модель «всё или ничего» в такой ситуации просто парализует весь процесс: малейшая ошибка в одной форме заблокирует создание всего аккаунта.
Пришлось полностью менять архитектуру. Мы сохранили логику визиторов, но разорвали единую транзакцию.
В новой схеме v2:
- Базовая сущность: сначала создаётся минимальный каркас сущности Place с базовыми полями. Это делается быстро и просто.
- Асинхронное насыщение: дальше каждый визитор (AddressVisitor, MenuVisitor, ZoneVisitor) запускается как отдельная задача в очереди. Для этого мы использовали STQ/RabbitMQ.
- Изоляция контекстов: ошибка при сохранении расписания больше не влияет на создание юридического лица или загрузку меню потому что эти процессы стали независимыми.

Для пользователей (в данном случае — ресторанов) мы внедрили систему «семафоров»: интерфейс показывал статус заполненности каждого блока. Если меню загружено, зоны отрисованы, а договор подписан — загорался зелёный свет, и ресторан мог самостоятельно нажать кнопку «Запустить», отправив себе транзакционное письмо об активации.
Потеря детерминизма
Плюсы перехода на очереди были очевидны: система стала горизонтально масштабируемой. Мы могли просто добавить воркеров, чтобы разгрести пиковую нагрузку, не блокируя базу длинными транзакциями.
Но за всё приходится платить. И нам пришлось пойти на усложнение логики и потерю прозрачности:
- Недетерминированный порядок. В очередях мы не могли гарантировать, что зона доставки будет создана после того, как создан регион, к которому она привязана.
- Зависимости данных. Пришлось писать дополнительный код для валидации целостности. Например, чтобы проверять: есть ли у нас бренд перед созданием меню.
- Вредные ретраи. Если задача падала из-за отсутствия зависимых данных, она уходила в ретрай. Это создавало паразитный трафик и усложняло отладку. Приходилось выяснять, почему визитор зоны упал 5 раз подряд? Потому что база тормозила или потому что визитор региона ещё не отработал?
Система не была идеальной, но она выжила и дала возможность спасти бизнес в пандемию. Впрочем, мы понимали, что это техдолг, который придётся возвращать.
Этап 3. Миграция Delivery Club и новый контракт данных (2022 год)
К лету 2022 года нас ждал новый вызов — интеграция бизнеса Delivery Club. Задача стояла конкретная: мигрировать около 120 000 активных ресторанов в нашу систему, плюс какое-то количество архивных. При этом мы ещё параллельно переезжали с Salesforce на внутреннюю YaCRM.
Анализ данных Delivery Club показал, что их структура сущности ресторана очень похожа на ту JSON-схему, с которой мы уже работали при интеграции Salesforce. Это навело нас на мысль: вместо того чтобы писать кастомные скрипты миграции на один раз, нужно создать универсальный контракт данных.
Рождение сервиса eats-place-onboarding
Реализовывать такой контракт в старом PHP-монолите было бы как минимум неправильно. Поэтому мы выделили логику приёма и адаптации данных в отдельный микросервис — eats-place-onboarding.

Этот сервис стал единой точкой входа для любых внешних систем: будь то YaCRM или дампы из Delivery Club. Он взял на себя две ключевые функции:
- Трансформация: приведение разнородных данных к каноническому виду.
- Валидация: проверка обязательных полей и бизнес-правил до того, как данные попадут в ядро системы.
Внутри сервиса мы ввели чёткое разделение состояний заявки, которого нам так не хватало в монолите:
- 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 был оптимальным.

Теперь пайплайн создания ресторана выглядит не как набор разрозненных тасков, а как чёткая последовательность шагов:
- 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 дней. Ускорение почти в три раза — это результат совместной работы инженеров, продактов и команды контента.

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




