🏗️ Розділ 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: Кожні кілька спринтів відокремлюється один ізольований бізнес-контекст, що негайно постачається в 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 ігнорує події, джерело яких збігається з поточною цільовою системою.

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

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

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

4 Tricky Questions

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

Відповідь: Для безпечного тіньового тестування запитів запису:

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

2. З якого модуля найдоцільніше розпочинати розпил моноліту за патерном Strangler Fig?

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

  1. Низька зв’язність (Low Coupling): Модуль із мінімальною кількістю перехресних викликів у коді моноліту.
  2. Низький бізнес-ризик: Помилка в першій ітерації не повинна призвести до критичних фінансових втрат.
  3. Ідеальні кандидати для старту: Сервіс сповіщень (Email/Push/SMS), сервіс формування PDF-звітів, статичний каталог статей чи довідників, сервіс промокодів. Команда відпрацьовує CI/CD, Kubernetes, мережеву взаємодію та Observability на безпечному компоненті перед переходом до ядра.

3. Що робити, якщо новий сервіс вимагає виконання розподіленої транзакції зі старим монолітом під час перехідного періоду?

Відповідь: Синхронний двофазний коміт (2PC) між новим сервісом та монолітом непридатний через ризики дедлоків та несумісність драйверів. Рішення: Використання патерну Saga з участю моноліту:

  1. Моноліт доопрацьовується для надання компенсувальних API (наприклад, метод POST /orders/{id}/cancel для повернення статусу).
  2. Новий сервіс бере на себе роль Saga Orchestrator, керуючи кроками через черги подій.
  3. Якщо операція в моноліті падає, оркестратор викликає компенсувальну дію у власній базі даних.

4. Чи обов’язково розпилювати моноліт до останнього рядка коду (Правило 80/20)?

Відповідь: Ні. Розпил моноліту — це інженерно-економічне рішення, а не догма. Зазвичай перші 80% функціоналу (високонавантажені модулі, що активно розвиваються командами) переносяться в мікросервіси за рік-два. Залишок у 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) із запобіганням циклічній реплікації.
  • Валідація: Traffic Shadowing (тіньове дублювання запитів) + Diff Engine.
  • Старт міграції — зі слабкозв’язаних і низькоризикових модулів (сповіщення, звіти).

Червоні прапорці (Чого говорити НЕ МОЖНА):

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

Пов’язані теми: