🏗️ Раздел 17 · Вопрос #24

Что такое паттерн Strangler Fig

Мартин Фаулер (Martin Fowler) описал этот паттерн после наблюдения за австралийскими деревьями: семечко удушающего инжира прорастает в кроне взрослого дерева, пускает корни вниз...


🟢 Junior Level

Strangler Fig (Паттерн «Удушающий инжир») — это архитектурная стратегия постепенной, эволюционной миграции монолитного приложения на микросервисную архитектуру путём пошаговой замены отдельных частей системы новыми сервисами до тех пор, пока монолит не отомрет полностью.

Фаза 0 (Монолит):      [═════════════════ Монолит ═════════════════]

Фаза 1 (Первый шаг):   [══════════════ Монолит ══════════════] + [User Service]

Фаза 2 (Развитие):     [═══════════ Монолит ═══════════] + [User Service] + [Order Service]

Фаза 3 (Финал):        [User Service] + [Order Service] + [Payment Service] + [Catalog Service]
                       (Старый монолит полностью остановлен и удален! 🎉)

Происхождение метафоры:

Мартин Фаулер (Martin Fowler) описал этот паттерн после наблюдения за австралийскими деревьями: семечко удушающего инжира прорастает в кроне взрослого дерева, пускает корни вниз к земле, оплетает ствол и со временем полностью заменяет собой старое дерево, которое естественным образом отмирает внутри новой древесины.

Почему отказ от «Big Bang Rewrite» спасает бизнес:

  • Антипаттерн «Большой взрыв» (Big Bang): Попытка остановить развитие монолита на 2 года и переписать всё с нуля. 90% таких проектов заканчиваются банкротством: за 2 года бизнес уходит вперед, новая система не учитывает сотни скрытых краевых случаев монолита, а одновременный релиз приводит к фатальным падениям.
  • Strangler Fig: Каждый спринт (2–4 недели) отделяется один изолированный бизнес-контекст, немедленно выкатывается в Production и приносит ценность, пока остальная система продолжает функционировать без единой секунды простоя (Zero Downtime).

🟡 Middle Level

1. Четыре фазы цикла миграции

                       ┌───────────────────────────────┐
                       │     API Gateway / Ingress     │
                       └───────┬───────────────┬───────┘
            /api/v1/orders     │               │  /api/v1/users (мигрировано!)
                               ▼               ▼
                      ┌────────────────┐ ┌─────────────────┐
                      │    Монолит     │ │  User Service   │
                      └────────────────┘ └─────────────────┘
  1. Intercept (Перехват): Перед монолитом развертывается API Gateway (Spring Cloud Gateway, Envoy, Nginx). Все клиентские вызовы идут строго через шлюз.
  2. Transform (Реализация): Новый сервис (например, User Service) разрабатывается с нуля по современным стандартам и со своей изолированной базой данных (Database per Service).
  3. Coexist (Сосуществование): Gateway перенаправляет маршрут /api/v1/users на новый микросервис. Все остальные вызовы продолжают прозрачно идти в монолит. Данные синхронизируются в реальном времени.
  4. Eliminate (Удаление): Старый неиспользуемый код пользователей вырезается из монолита.

2. Защитный слой: Anti-Corruption Layer (ACL)

При отделении функционала новый микросервис рискует быть «отравлен» устаревшей и запутанной моделью данных монолита:

[Чистая доменная модель (Новый сервис)] ◄──► [Anti-Corruption Layer (ACL)] ◄──► [Монолит (Legacy API/DB)]

Anti-Corruption Layer (Слой предотвращения деградации): Специальный транслятор (фасад / адаптер), который преобразует устаревшие структуры монолита в чистые доменные объекты нового сервиса. Он защищает новую кодовую базу от проникновения легаси-терминов и кривых структур БД.


🔴 Senior Level

1. Двунаправленная синхронизация данных (Bidirectional Sync)

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

Синхронизация через CDC (Change Data Capture):
[Legacy DB] ──(Debezium CDC 1)──► [Kafka Topic] ──► [New Service DB]
    ▲                                                     │
    └──────────(Debezium CDC 2 / Outbox)──────────────────┘

Защита от бесконечного цикла репликации (Loop Avoidance):

Если база $A$ пишет в базу $B$ через CDC, а база $B$ реплицирует изменения обратно в базу $A$, возникает бесконечный цикл обновлений. Решение:

  1. Добавление технической колонки source_system = 'NEW_SERVICE'.
  2. Фильтр Debezium CDC или Kafka Connect игнорирует события, у которых источник совпадает с целевой системой.

2. Теневой запуск: Traffic Shadowing (Dark Launching)

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

                  ┌───────────────────────────────┐
                  │    API Gateway / Envoy Proxy  │
                  └───────┬───────────────┬───────┘
                          │ (Основной)    │ (Теневой дубль 100% трафика в фоне)
                          ▼               ▼
                     [ Монолит ]    [ Новый сервис ] ──► [ Shadow DB (Запись изолирована) ]
                          │               │
                          └───────┬───────┘
                                  ▼
                         [ Diff Engine (Сравнение JSON) ]
  1. Envoy / Gateway Shadowing: Шлюз отправляет реальный запрос в монолит и возвращает ответ клиенту. Одновременно копия запроса асинхронно отправляется в новый микросервис.
  2. Diffing: Ответ нового микросервиса сравнивается побайтово или семантически с ответом монолита (инструменты типа Twitter Diffy).
  3. Безопасность: Ответ нового сервиса клиенту не отдается. Для операций мутации (POST/PUT) вызовы внешних систем (эквайринг, email) заменяются моками, а запись идет в теневую тестовую БД.

4 Tricky Questions

1. Как безопасно протестировать мутирующие операции (POST / PUT / DELETE) через Traffic Shadowing, не вызвав двойных списаний денег или отправки дубликатов SMS?

Ответ: Для безопасного теневого тестирования методов записи:

  1. Изоляция базы данных: Теневой сервис подключается к изолированной Shadow Database, куда копируются актуальные данные. Реальная база монолита не затрагивается.
  2. Mocking побочных эффектов (Side-Effects): На уровне сетевого клиента или Spring профиля (@Profile("shadow")) отключаются вызовы внешних шлюзов (платежи, SMS, фискализация чеков). Вместо них ставятся моки (WireMock / No-Op адаптеры).
  3. Сравнение доменных событий: Сравниваются не побочные эффекты, а сгенерированные бизнес-события в Kafka или результирующие состояния сущностей в Shadow DB с основной базой.

2. С какого модуля правильнее всего начинать распил монолита по паттерну Strangler Fig?

Ответ: Никогда не начинают с ядра системы (Core Domain — например, балансы пользователей или оформление заказов). Правильная стратегия:

  1. Низкая связность (Low Coupling): Модуль с минимальным количеством внешних зависимостей в монолите.
  2. Низкий бизнес-риск: Ошибка не должна привести к отзыву банковской лицензии.
  3. Идеальные кандидаты для старта: Сервис отправки нотификаций (Email/SMS), сервис генерации PDF-чеков, статический каталог статей, сбор телеметрии или сервис скидочных купонов. Команда набивает руку на настройке CI/CD, K8s, сетевой инфраструктуры и только затем переходит к Bounded Contexts средней сложности.

3. Что делать, если новый сервис требует выполнения межсервисной транзакции со старым монолитом во время переходного периода?

Ответ: Синхронный 2PC (двухфазный коммит) между монолитом и новым сервисом неприменим из-за несовместимости и блокировок. Решение: Применение Saga с участием монолита:

  1. Монолит адаптируется для поддержки двухфазных бизнес-команд (например, добавление метода API POST /orders/cancel для отката).
  2. Новый сервис выступает в роли Saga Orchestrator, координируя шаги через асинхронные очереди сообщений (Kafka/RabbitMQ).
  3. Если операция в монолите падает, новый сервис выполняет компенсирующую транзакцию в своей БД.

4. Всегда ли нужно «душить» монолит до последней строчки кода (Правило 80/20)?

Ответ: Нет. Распил монолита — это бизнес-инвестиция, а не религиозный догмат. Часто первые 80% монолита (высоконагруженные, часто меняющиеся продуктовые фичи) успешно выносятся в микросервисы за 1 год. Оставшиеся 20% представляют собой стабильный, редко меняющийся легаси-код (например, закрытие финансового месяца или печать архивных отчетов раз в квартал). Выносить этот стабильный остаток экономически нецелесообразно: его затраты на переписывание превысят любую выгоду. Монолит превращается в один из рядовых сервисов экосистемы (Legacy Core Service) и спокойно живет годами за общим API Gateway.


🎯 Шпаргалка для интервью

Главное:

  • Strangler Fig — стратегия постепенной, эволюционной замены монолита на микросервисы без остановки работы (Zero Downtime).
  • Альтернатива «Большому взрыву» (Big Bang Rewrite), защищающая от банкротства и срыва сроков.
  • Фазы: Intercept (API Gateway) $\to$ Transform (новый сервис) $\to$ Coexist (роутинг + синхронизация) $\to$ Eliminate (удаление легаси-кода).
  • Anti-Corruption Layer (ACL) защищает домен нового сервиса от легаси-структур монолита.
  • Данные мигрируют через CDC (Debezium + Kafka), избегая проблем Dual-Write.
  • Проверка корректности: Traffic Shadowing (теневой дубль запросов) + канареечный релиз.
  • Начинать миграцию нужно с наименее связанных и наименее рискованных модулей (нотификации, генерация отчетов).

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Лучший подход — заморозить проект на год и полностью переписать монолит с нуля» (Классический провал Big Bang).
  • ❌ «Для переноса данных монолит должен писать одновременно в две базы данных через @Transactional» (Dual-Write ломает согласованность при любых сетевых сбоях).
  • ❌ «Первым делом из монолита нужно выпиливать самый сложный и запутанный финансовый модуль» (Сломает бизнес на первом же шаге; начинают с простых слабосвязанных модулей).

Связанные темы: