Заметки бэкенд-разработчика: как мы создавали новую программу лояльности
«Много коинов не бывает!»
Привет, Хабр! Мы в Dodo Engineering любим ставить амбициозные цели и проводить эксперименты. В этой статье я хочу рассказать, как небольшая фича-команда за три месяца сделала и запустила новую программу лояльности в Додо Пицце.
Хотите взглянуть на проект целиком глазами бэкенд-разработчика? Тогда заваривайте чай, мы начинаем путешествие в страну спринтов, хотфиксов, миграций и блестящих додокоинов!
Что было не так со старой программой лояльности
Прежде чем рассказывать о разработке новой программы лояльности, нужно немного окунуться в предысторию и объяснить, почему у бизнеса появилась такая потребность.
В Додо Пицце уже давно существовала программа лояльности в виде додо-рублей. Они начислялись за каждый заказ клиенту как 5% кешбэк от суммы заказа. Ими можно было оплатить часть следующих заказов как обычными рублями. Можно было даже оплатить весь заказ целиком.
Но у старой программы лояльности был ряд недостатков:
Не хватало гибкости в настройке, что приводило к большим скидкам в чеках. По факту у нас была программа, которая начисляла внутреннюю валюту за каждый заказ, и так же в каждый заказ накопления могли быть списаны. При этом любое доначисление от маркетинга или колл-центра в виде компенсаций лишь растило скидку, так как додо-рубли можно было применить и с промокодами, и с комбо. Это не позволяло контролировать её размер и очень не нравилось партнерам-франчайзи, которые были вынуждены выполнять заказы с большой скидкой и с нулевой маржинальностью.
Не мотивировала делать заказы и не вовлекала. Клиенту не требовалось дополнительных усилий, кроме как совершить заказ. Программа лояльности просто существовала и давала фиксированную скидку. Из-за этого она была не столь привлекательна, её часто не замечали, а на счетах клиентов оставались неиспользованные додо-рубли.
Её нельзя было масштабировать. Додо-рубли использовались только в России и не подходили для работы в других странах. What is dodo ruble? :) Там пришлось бы запускать другую валюту, а это значит другое название, позиционирование и другой курс. Параллельно запускались стартапы Дринкит и Донер 42 — программа должна была уметь гибко перестраиваться под их запросы.
В мае 2020 года, понимая всю сложность ситуации с карантином, компания приняла решение остановить начисление додо-рублей и оставить только списание. Разумеется, мы предупредили клиентов и принесли извинения за такие меры, но они помогли нам снизить процент скидки и увеличить маржинальность в непростое время. Мы благодарны нашим клиентам за понимание и поддержку — никто не был против такого решения.
Разрабатываем новые концепции
Нам потребовался год, чтобы прийти к финальной концепции новой программы лояльности. Было 3 промежуточных варианта:
Разный уровень кешбэка в зависимости от активности клиента. В этот момент в компании ввели RFM-сегментацию клиентов и самым лояльным мы были готовы дать 7% кешбэка, а новичкам — 3%.
Почему не приняли: было много вопросов по привлекательности программы, так как концепция по факту не поменялась, при этом клиенты стартовали уже не с 5%, а с 3%. Партнёры-франчайзи опасались, что 7% кешбэка может сильно ударить по общему проценту скидок на клиентов, что понизило бы маржинальность.
Разный уровень кешбэка с ограничениями. Развитие предыдущей концепции, где мы не начисляли кешбэк за комбо и за акционные товары.
Почему не приняли: у программы появилось слишком много условий, которые сложно было бы транслировать и доносить клиентам. Из-за этих ограничений программа ещё сильнее потеряла бы в гибкости, а все дополнительные маркетинговые механики были бы с огромным списком «За что не начисляем».
Кусочки. Пожалуй, самая привлекательная концепция, так как напрямую была привязана к физическому миру. Кусочки — это внутренняя валюта, которая даётся за каждые потраченные 500 рублей в чеке. Чтобы забрать любую маленькую пиццу, требовалось 6 кусочков, среднюю — 8, большую — 10. Мы даже успели приступить к разработке, но её пришлось остановить.
Почему не приняли: несмотря на привлекательность упаковки и уникальности, у этой концепции был ряд проблем, которые всплыли по ходу. При детальном расчёте стало понятно, что программа даёт скидку ещё больше, чем предыдущие концепции. Чтобы удовлетворить ограничениям скидки, 1 кусочек нужно было давать за каждые 1000 рублей в чеке, и это сразу убивало привлекательность программы из-за высокого порога входа.
Но последняя концепция имела ряд фундаментальных вещей, которые мы взяли за основу в финальную версию:
внутрення валюта. Начисляется за каждые N рублей, потраченных в заказе, и копится у клиента на счету. Больше никаких условий и ограничений;
маркетплейс. Место, где можно потратить накопленную валюту. Есть разные категории подарков. У категории есть стоимость во внутренней валюте и можно забрать любой продукт из категории за эту стоимость.
Новая программа лояльности в мобильных приложениях
Построенная по этим пунктам программа получается гибкой: можно управлять как стоимостью начисления, так и новым маркетплейсом. При этом есть возможность внедрить категории и продукты, недоступные в основном меню. А также сделать повышенное или дополнительное начисление внутренней валюты, не ставя под угрозу процент скидки (не все клиенты сразу в следующем заказе смогут применить валюту). Додокоины — наша новая внутренняя валюта. Они начисляются за каждые 20 рублей в чеке и их можно уже сейчас потратить в мобильном приложении.
Таким образом, к весне 2021-го у нас был амбициозный проект, который многие с нетерпением ожидали. До этого момента, в течение полутора лет, он находился в подвисшем состоянии: концепцию несколько раз полностью переосмысляли, разработку то начинали, то отменяли. Нужна была команда, которая сделает крутое решение, но не закопается в разработку еще на один год.
Вызов принят!
А у нас кибер-лапки!
У всех команд в Dodo Engineering есть интересные названия. Команда разработки, которая взялась за новую программу лояльности, носит гордое имя Cyber Paws (кибер-лапки). Лапки — это фича-команда, в которой есть бэкенд, фронтенд, iOS и Android-разработчики, а также QA-инженер, дизайнер, продакт-оунер и people-process lead.
Cyber Paws на момент запуска проекта в сентябреДекомпозиция, отрицание, принятие
Проект разделили на несколько крупных блоков: учёт бонусных баллов, процесс их начисления, админка для управления меню лояльности, создание новых элементов UI и т.д. Когда стали обсуждать каждый блок более детально, то увидели, что с большой вероятностью не успеем реализовать всё, что хотим. Поэтому договорились о минимально необходимом наборе механик, с которыми можно будет запуститься. Их мы обозначили как задачи нулевого приоритета. Все остальные задачи попали в группу первого, второго и последующих приоритетов. Они должны были делаться в оставшееся время или уже после запуска в сентябре.
Будем откровенны, на этом этапе мы допустили ошибку — не достаточно хорошо проговорили риски и не заложили временной буфер в roadmap проекта. Например, мы не обсудили, что понадобится дополнительное время на согласование технических решений с другими командами и архитектором. Также можно было предсказать, в каких компонентах системы могут быть долгие релизы и подводные камни, которые замедлят разработку.
Никогда не пренебрегайте обсуждением потенциальных рисков на старте проекта. Этот элемент должен быть обязательной частью вашего планирования.
Разбиение проекта на этапы (доска в Miro)
Душевные терзания: писать в монолите или пилить новый сервис
Вариантов технической реализации оказалось несколько.
Программа лояльности затрагивает почти все компоненты системы и поднимает древние холиварные вопросы. Делать всё внутри большого монолита или создать новый сервис? Какую часть унести в новый сервис? Как монолит и новый сервис должны взаимодействовать?
Кроме того, нужно помнить, что мы пишем код не в вакууме, а работаем с бизнесом, у которого есть ожидания по срокам. Вынести новые механики в отдельный проект и запуститься в сентябре мы никак не успевали. Кроме того, логичным вариантом казалось вынести сразу весь расчёт заказа из монолита и уже поверх него добавить наши механики. Это очень непростая задача, на которую может понадобиться дополнительно несколько месяцев.
В итоге, взвесив все за и против, решили писать код в монолите. При этом закоммитились перед остальными командами, что после запуска сделаем новый сервис. После ряда бурных обсуждений стал вырисовываться образ будущего проекта.
Один из черновых набросков базовых механик лояльности
Дальше я расскажу про некоторые наши решения.
Архитектор, нам нужно серьёзно поговорить: пишем RFC
Когда завершили планирование, нужно было приступить к написанию RFC. RFC (Request for comments) — это статья, в которой мы, разработчики, описываем своё новое техническое решение и выносим его на общее обсуждение. Ссылку на этот документ присылаем в специальный канал в Slack. Архитектор, разработчики из других команд и SRE могут прочитать его, оставить комментарии по установленному формату. В течение нескольких дней мы реагируем на комментарии и в итоге утверждаем концепцию. Более подробно о том, как работает этот процесс, можно почитать тут.
В течение целой недели мы отвечали на вопросы и, наконец, пришли к общему взаимопониманию. Настало время писать код.
Часть документа с RFC по программе лояльности в Notion
Творим историю
Разработку начали с реализации учёта новой валюты.
Нужно было как-то хранить баланс додокоинов и историю всего, что с ними происходило. Для этого придумали таблицу с историей операций.
Операция состояла из множества полей, включая ID клиента, ID заказа, количества начисляемых додокоинов и т.д. Важно, что каждая операция содержала в себе поле с новым актуальным балансом, который получился после её применения. Кроме того, мы добавили поле с версией этого баланса.
Версия была простым числом, которое становилось больше в каждой последующей операции клиента. Благодаря этому, чтобы узнать баланс клиента, нужно просто найти его самую последнюю операцию (с самой большой версией) и посмотреть на значение баланса. Если же у клиента нет операций в истории, то его баланс считали равным нулю. Всю историю можно было посмотреть в табличке в Redash. Там под капотом был запрос к реплике для чтения. Позже это всё уехало в отдельную админку для управления бонусными аккаунтами.
История операций с додокоинами (результат запроса в Redash)
Когда пытаешься всё рассчитать…
Теперь настало время сделать расчёт заказа с додокоинами. Нужно было вписаться в существующий процесс расчёта, в котором уже и так есть немало шагов: расчёт налогов, применение автоматических акций, проверка продуктов на доступность и так далее.
Мы придумали решение, которое сократило значимую часть работы. Продукты, купленные за додокоины, не становились новой сущностью в системе, а должны рассчитываться как обычные продукты, но по особым условиям. К ним применялась специальная скрытая акция, которая занижала стоимость до 1 рубля.
Также здесь появилась интересная задача с проверкой актуальности баланса. Дело в том, что после расчёта заказа должен происходить следующий шаг — сохранение заказа в базу. На этапе сохранения мы хотели фактически списывать додокоины. Между этими процессами есть задержка, в которую баланс клиента может измениться. Поэтому решили, что расчёт заказа проставит в расчитанную корзину специальное значение версии баланса, которая была на момент расчёта. Затем, на этапе сохранения заказа, внутри большой транзакции, можно сравнить версию баланса из результатов расчёта и актуальную. Хорошо, что ранее мы уже заложили такое поле (версию) в историю операций и смогли им воспользоваться.
Сейчас, после запуска проекта, мы думаем над усовершенствованием всех этих механик. Наброски нового сервиса программы лояльности, который находится на этапе проектирования, начинают походить на настоящий эквайринг. От версий баланса мы переходим к процессу, похожему на холдирование.
Но вернёмся обратно в те дремучие времена.
«Кто последний?» Продумываем механику очереди
Для начисления баллов пришлось организовать механику очереди. К сожалению, на момент проектирования не было универсального способа узнать, когда заказ завершён. В идеальном варианте мы должны были бы начать реагировать на отметку в курьерском приложении о том, что заказ доставлен. Также нужно было обработать и другие источники. Самым простым и быстрым вариантом на тот момент оказалось положить операцию начисления в таблицу с очередью на пару часов. Если за это время заказ не отменили, то его можно считать завершённым и начислить додокоины. За этим должен был следить специальный фоновый процесс (джоба на Quartz). Мы договорились, что сделаем этот вариант, но если останется время — перейдём на начисление по событиям.
Фича-тоглы спасут мир
Во время планирования проекта решили, что будем запускать его постепенно. Включать такую сложную фичу сразу во всей стране опасно как с технической стороны, так и со стороны бизнес-процессов.
В августе наметили провести тестовую обкатку в 5 городах в России, чтобы собрать данные и поймать ошибки, которые могли бы всплыть на федеральном запуске. Для этого закрыли все основные механики за фича-тоглами. Также фича-тоглы помогли распараллелить работу в мобильных приложениях и на бэкенде. Мы договорились о контрактах и сделали заглушки на API, чтобы мобильные разработчики могли сразу приступить к своим задачам. В итоге они сделали и зарелизили свою часть уже к середине июля. Все новые элементы UI были на продакшене, скрытые за мобильными фича-тоглами.
Техническая админка для управления фича-тоглами мобильных приложений
Конфликт с додо-рублями
В середине работы над проектом обнаружился интересный кейс с додо-рублями. Их начисление давно выключили и казалось, что про них можно забыть. Но внезапно мы узнали, что некоторые бизнес-процессы не готовы к полному отказу от старой программы лояльности. Некоторые клиенты, включая тайных покупателей, всё ещё получали компенсации додо-рублями, и альтернативный вариант был не до конца проработан.
Мы были готовы к такому повороту и ничего не нужно было переделывать. Мудрые мира сего завещали всегда делать non-breaking changes. Наше решение изначально было спроектировано так, что позволяло применить в заказе додокоины и не исключало возможности использовать старые додо-рубли. Более того, отображение обеих валют не ломало UI. Это дало возможность некоторое время поддерживать сразу две бонусные валюты до момента полного отказа от одной из них.
Админка нам поможет
Кроме всего прочего, мы сделали для себя админку, с помощью которой можно было вручную начислять додокоины и отслеживать изменения баланса. Это оказалось хорошим решением. Прямого запроса от бизнеса на нее не поступало, но было очевидно, что рано или поздно он появится — так и произошло. В первые дни запуска эту админку использовали сотрудники колл-центра, чтобы отвечать на вопросы клиентов про начисление додокоинов, а также в ручную корректировать баланс. Затем админка переехала под крыло специального менеджера бонусной системы. Ещё она помогла нашему QA тестировать задачи: с её помощью он начислял себе додокоины для различных экспериментов.
Одна из новых админок по программе лояльности
Вызываем подкрепление
Пока шла работа над лояльностью, другая команда занималась запуском проекта DMS (data materialization service). Этот сервис предлагал более быстрый и надёжный способ получения меню клиентскими приложениями.
Мы скооперировались с ней и встроили новое меню лояльности (маркетплейс) в их проект. Так мы делали свою фичу с использованием свежего подхода, на который всё равно потом пришлось бы переехать.
Этим наше взаимодействие с соседней командой не ограничилось. Ближе к августу появилось понимание, что не успеваем сделать всё, что задумали. Ребята пришли к нами на помощь и на время присоединились к проекту — за это им большой респект.
Здесь хочется написать небольшой совет, родившийся из опыта кросс-командного взаимодействия: если вы привлекаете к проекту другую команду, то несёте ответственность за то, чтобы погрузить в свой, незнакомый для неё домен. Чаще работайте в парах, чаще синхронизируйтесь, формируйте общее понимание продукта. Иногда проговорить контракты между сервисами недостаточно.
Проверка в бою
Наступил август. В первых числах мы собирались провести пробный запуск в пяти городах. Основная механика с начислением и списанием додокоинов работала, но не хватало нескольких важных вещей, например:
нельзя было временно поставить в стоп продукты из меню лояльности (как это можно делать с продуктами из обычного меню);
не успели зарелизить лендинг с правилами работы новой бонусной системы.
Запустив проект без этих фич, мы рисковали создать проблемы сотрудникам пиццерий и вызвать негатив клиентов. Пробный запуск отодвинули на 16 августа, и две следующие недели велась активная доработка прототипа.
Для тестового запуска выбрали пять городов: Якутск, Мурманск, Чехов, Сыктывкар, Тамбов, Волгоград. Внезапно нам написали ребята из Эстонии и предложили провести тест у них тоже. Утром 16 августа команда собралась в Google Meet и, затаив дыхание, смотрела, как один из разработчиков последовательно включает фича-тоглы. Ничего страшного не случилось, хотя пара проблем всё-таки всплыла. Весь конец августа проект продолжал работу в тестовых городах. Мы находили небольшие ошибки и делали фиксы.
Одна из ошибок, которую поймали на продакшене
Я выше рассказывал, что из последней операции клиента с додокоинами можно было узнать актуальный баланс (каким он стал после её применения). Где-то глубоко в коде был написан поиск последней операции клиента по ID и самому большому значению времени ExecutionTime. Изначально договаривались искать последнюю операцию по самой большой версии, но, видимо, в процессе это забылось. На продакшене возникали ситуации, когда в очереди для начисления у клиента скопилось несколько операций. Затем они применялись одновременно в момент обработки очереди. Тогда в базе появлялось две и более записей с одинаковым значением ExecutionTime.
Дело в том, что значение ExecutionTime создавалось во время запуска джобы, а не в момент выполнения каждой операции внутри неё. Соотвественно, поиск последней операции по большему ExecutionTime отрабатывал не верно. В проблемном месте просто перешли на использование поля Version и проблема ушла.
В целом проблем оказалось не так уж много и проект показал хорошую техническую стабильность. Корневые механики работали как задумано.
Федеральный запуск и конвертация додо-рублей
Утром 13 сентября мы включили начисление додокоинов во всей России, 14 сентября включили отображение баланса, бонусного меню и других UI-элементов. Клиенты начали делать свои первые заказы с додокоинами. История транзакций и очередь для начисления заполнялись на глазах.
Сообщения в Slack в день федерального запуска
В то же время запустили большую и сложную миграцию в базе данных, которая должна была конвертировать остатки додо-рублей в додокоины. Тут тоже случился эксперимент: команды SRE и дата-инженеров подготовили инструмент для выполнения сложных миграций и мы сделали конвертацию додо-рублей через него. Сложный запрос с конвертацией был разбит на несколько сотен файлов и выполнен постепенно. За этим процессом, который длился около часа, пристально следили дежурный SRE и бизнес-аналитики.
Всё прошло отлично за исключением только одной смешной проблемы, которую обнаружили спустя пару дней после запуска: очередь для начисления баллов начала накапливаться быстрее, чем фоновый процесс её обрабатывал. На тестовом запуске в пяти городах этот момент не всплыл, так как поток заказов там был значительно меньше. Где-то в пылу разработки мы выставили значение 3000 для количества операций из очереди начисления додокоинов, которое джоба обрабатывает за один запуск. Потом просто забыли про него, хотя собирались опытным путём найти оптимальное значение вместе с командой нагрузочного тестирования. Разработчики — люди увлечённые, c нами такое случается. :) В итоге мы увеличили частоту запуска джобы, её пропускную способность и всё пришло в норму.
День Х настал
Наконец-то цель была достигнута. Мы всей командой съехались в Московском офисе и отметили это событие. Но на этом судьба проекта не закончилась. У нас остался технический долг. Также планируем значительно увеличивать команду и продолжать развивать механики лояльности. В этом и следующем году будет несколько интересных апдейтов, о которых пока ещё рано рассказывать.
Надеюсь, наш опыт разработки программы лояльности был интересен, буду рад ответить на вопросы в комментариях.