Как мы вынесли биллинг из монолита без остановки платежей (паттерн душитель)

Шаг 1: Подготовка схемы данных. В старой базе (PostgreSQL) таблицы платежей были жестко связаны внешними ключами (Foreign Keys) с таблицей пользователей. Мы убрали FK на уровне базы и перенесли проверку целостности в код монолита.
Шаг 2: Дублирующая запись (Dual Write). Мы написали новый микросервис биллинга на Go. В монолите настроили брокер сообщений (RabbitMQ). При каждой покупке монолит писал данные в свою старую базу и параллельно кидал событие в очередь. Новый сервис читал очередь и писал в свою новую базу.
Шаг 3: Теневое чтение (Shadow Read). На этом этапе запросы от клиентов шли в монолит. Но монолит асинхронно запрашивал данные из нового сервиса и сравнивал их со своими. Разницу (diff) мы писали в логи. За неделю мы отловили 5 багов округления копеек.
Шаг 4: Переключение. Когда логи сравнения стали чистыми на 100%, мы изменили флаг в конфигурации (Feature Toggle). Монолит перестал обращаться к своей базе и начал ходить за балансом напрямую в новый микросервис через gRPC. Старый код удалили через месяц.

Отличная реализация. Dual Write через брокер - это надежно, но есть подводный камень: порядок сообщений. Если клиент дважды кликнул на оплату, брокер может доставить события не по порядку. Мы в похожем сценарии добавляли поле version (целочисленный счетчик) к каждой записи в монолите. Микросервис проверял: если входящая версия меньше текущей в базе, событие просто игнорируется. Это решает проблему состояния гонки (race condition).

Как владелец продукта, задам главный вопрос: сколько человеко-часов это заняло и как быстро окупилось? Обычно такие рефакторинги продаются бизнесу очень тяжело, потому что “для клиента визуально ничего не изменилось”.

Заняло 3 месяца силами двух разработчиков и одного тестировщика. Окупаемость считали через Time-to-Market (время вывода новой фичи). Раньше, чтобы добавить новый тариф, мы пересобирали весь монолит и тестировали его неделю (риск уронить другие модули). Сейчас команда биллинга выкатывает новые тарифы за день. Бизнес смог запустить новогодние промо-акции без задержек, что принесло дополнительные 15% выручки за квартал.

Хочу дополнить про Шаг 1 с удалением внешних ключей. Это больно, но необходимо. Чтобы не оставить в базе “сиротские” записи (например, платеж есть, а юзера удалили), мы настраивали фоновый джоб (cron), который раз в сутки сверял ID пользователей в биллинге с основной базой и помечал невалидные записи специальным статусом для ручного разбора.

Риск теневого чтения в том, что вы удваиваете нагрузку на базу данных. Монолит делает SELECT для себя, а потом еще и микросервис делает SELECT. Если у вас 10 000 RPS (запросов в секунду), сеть может не выдержать. Для высоконагруженных систем теневое чтение лучше запускать не на 100% трафика, а на 1-5%, используя балансировщик или хеширование ID пользователя.

Для маленьких проектов этот паттерн избыточен. Если у вас 50 оплат в день, проще повесить заглушку “технические работы на 2 часа” ночью и перенести скриптом всё за один раз.