Попытка единовременной полной замены крупного монолита («Big Bang Rewrite») в 80% случаев завершается бюджетной катастрофой, затягиванием сроков на годы и критическими сбоями в продакшене. Монолитная система непрерывно развивается вместе с бизнесом, создавая эффект «движущейся мишени», пока команда пытается воспроизвести устаревший функционал с нуля.
Архитектурным стандартом безопасной модернизации является паттерн Strangler Fig (Душитель фикуса). Суть подхода заключается в постепенном, контролируемом вытеснении монолитных модулей новыми микросервисами или Headless-приложениями, расположенными за единым фасадным слоем маршрутизации (API Gateway / Edge Proxy). Для внешних потребителей система выглядит как единый монолитный интерфейс, однако внутренний трафик инкрементально перенаправляется на новые сервисы по мере их готовности.
Ключевым барьером реализации Strangler Fig является трудоемкость создания промежуточных слоев изоляции — Anti-Corruption Layer (ACL, Антикоррозийный слой). Агентные AI-пайплайны 2026 года автоматизируют этот процесс: они генерируют двунаправленные адаптеры данных между денормализованными структурами монолита (таблицы wp_posts / wp_postmeta, спагетти-массивы) и чистыми доменными сущностями (Domain Entities) новой архитектуры, гарантируя строгую контрактную совместимость.
Архитектура системы в процессе декомпозиции включает фасад маршрутизации, антикоррозийный адаптер и механизм синхронизации данных:
[HTTP Клиенты / Пользователи]
│
▼
[Edge Routing Facade (Next.js Middleware / Envoy)]
│ │
(Canary / Path) │ │ (Legacy Fallback)
▼ ▼
[New Headless App] [Legacy Monolith (PHP)]
(Next.js 15 / RSC) │
│ │
▼ │
[Anti-Corruption Layer (ACL)] ◄────┘
(AI-Generated Data Adapters)
│
▼
[Unified Data Store]
(Outbox / CDC Synchronizer)wp_postmeta) в типизированные структуры документов MongoDB / PostgreSQL JSONB.Реализация состоит из двух ключевых компонентов: пограничного шлюза маршрутизации на базе Next.js Middleware и типобезопасного Anti-Corruption Layer адаптера, сгенерированного ИИ.
Next.js Middleware проверяет путь, сессионные маркеры и флаги канареечного развертывания, прозрачно проксируя немигрированные запросы на legacy PHP-сервер:
import { NextRequest, NextResponse } from 'next/server';
// Конфигурация маршрутизации Strangler Fig
const LEGACY_BACKEND_URL = process.env.LEGACY_BACKEND_URL || 'https://legacy-monolith.internal';
const MIGRATED_PREFIXES = ['/products', '/catalog', '/api/v2/products'];
export async function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl;
// Проверка: мигрирован ли маршрут на новый Headless стек
const isMigratedRoute = MIGRATED_PREFIXES.some((prefix) =>
pathname.startsWith(prefix)
);
// Канареечный релиз: процент пользователей, видящих новый сервис
const canaryCookie = request.cookies.get('canary_opt_in')?.value;
const isCanaryUser = canaryCookie === 'true' || Math.random() < 0.1; // 10% канарейка
if (isMigratedRoute && isCanaryUser) {
// Обслуживание через новый Next.js App Router (локальная обработка)
const response = NextResponse.next();
response.headers.set('x-served-by', 'nextjs-headless-strangler');
return response;
}
// Проксирование на legacy PHP-монолит
const targetUrl = new URL(`${pathname}${search}`, LEGACY_BACKEND_URL);
// Копирование заголовков для сохранения сессий монолита
const forwardHeaders = new Headers(request.headers);
forwardHeaders.set('x-forwarded-host', request.headers.get('host') || '');
forwardHeaders.set('x-strangler-proxy', 'true');
return NextResponse.rewrite(targetUrl, {
request: {
headers: forwardHeaders,
},
});
}
export const config = {
matcher: [
/*
* Исключение статических файлов сборки Next.js из проксирования
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};Монолит хранит товары в разрозненных таблицах базы данных (WordPress EAV модель). Адаптер изолирует новый домен от легаси-структур:
import { z } from 'zod';
// 1. Схема «грязных» данных монолита (Legacy WordPress Post + Meta)
export const LegacyPostPayloadSchema = z.object({
ID: z.union([z.string(), z.number()]).transform((val) => String(val)),
post_title: z.string(),
post_name: z.string(),
post_status: z.string(),
meta: z.record(z.string(), z.unknown()).default({}),
});
export type LegacyPostPayload = z.infer<typeof LegacyPostPayloadSchema>;
// 2. Чистая доменная модель товара (Clean Architecture Domain Entity)
export const DomainProductSchema = z.object({
id: z.string().uuid().or(z.string().min(1)),
sku: z.string(),
title: z.string().min(1),
slug: z.string(),
priceCents: z.number().int().nonnegative(),
isAvailable: z.boolean(),
tags: z.array(z.string()),
});
export type DomainProduct = z.infer<typeof DomainProductSchema>;
/**
* Anti-Corruption Layer: безопасная конвертация устаревших метаданных
* в чистую доменную сущность с защитой от поврежденных данных.
*/
export function adaptLegacyProductToDomain(rawLegacyData: unknown): DomainProduct {
const legacy = LegacyPostPayloadSchema.parse(rawLegacyData);
// Извлечение EAV метаполей с проверкой типов
const rawSku = legacy.meta['_sku'];
const rawPrice = legacy.meta['_regular_price'];
const stockStatus = legacy.meta['_stock_status'];
const sku = typeof rawSku === 'string' && rawSku.trim().length > 0
? rawSku.trim()
: `LEGACY-${legacy.ID}`;
// Преобразование цены: float-строка "199.90" переводится в целые центы (19990)
const parsedPrice = typeof rawPrice === 'string' ? parseFloat(rawPrice) : 0.0;
const priceCents = Number.isFinite(parsedPrice) ? Math.round(parsedPrice * 100) : 0;
const isAvailable = legacy.post_status === 'publish' && stockStatus === 'instock';
// Формирование чистой сущности
return DomainProductSchema.parse({
id: legacy.ID,
sku,
title: legacy.post_title,
slug: legacy.post_name,
priceCents,
isAvailable,
tags: Array.isArray(legacy.meta['_custom_tags']) ? legacy.meta['_custom_tags'] : [],
});
}Для сотен таблиц монолита написание адаптеров автоматизируется с помощью строгого контракта генерации:
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const AdapterGeneratorSchema = z.object({
adapterName: z.string(),
legacySchemaCode: z.string().describe('Zod-схема для парсинга входных legacy-данных'),
domainSchemaCode: z.string().describe('Zod-схема для новой доменной сущности'),
transformFunctionCode: z.string().describe('Функция трансляции с санитизацией EAV-полей'),
unitTestCode: z.string().describe('Полный набор Vitest тестов с краевыми случаями'),
});
export async function generateAclAdapter(
legacySqlSchema: string,
targetDomainRequirements: string
) {
const prompt = `
Ты — Principal Software Architect. Спроектируй Anti-Corruption Layer (ACL) адаптер на TypeScript.
Входные структуры монолита (SQL / JSON):
${legacySqlSchema}
Требования к новому доменному интерфейсу:
${targetDomainRequirements}
ПРАВИЛА:
1. Не допускай просачивания legacy-полей (например, post_author, guid, post_mime_type) в домен.
2. Цены всегда нормализуй в целые числа (копейки/центы).
3. Обрабатывай отсутствующие или некорректные типы через fallback-значения.
4. Генерируй строгий код TypeScript без использования 'any'.
`;
const { object } = await generateObject({
model: openai('gpt-4o', { structuredOutputs: true }),
schema: AdapterGeneratorSchema,
prompt,
});
return object;
}Попытка одновременно писать данные в монолитную базу и базу нового сервиса через два сетевых вызова приводит к рассинхронизации при падении любого из вызовов. Решение: использование паттерна Transactional Outbox или Change Data Capture (Debezium/Kafka), где изменения сначала коммитятся в локальную транзакцию, а затем асинхронно доставляются в реплику.
Если пользователь авторизован в монолите через PHP-сессию (PHPSESSID), а новый Next.js бэкенд ожидает JWT-токен в заголовке Authorization, запросы между системами будут терять контекст пользователя. Маршрутизатор обязан содержать модуль конвертации сессионных cookie в JWT или обращаться к общему Redis Session Store.
Если доменная модель нового сервиса сохраняет имена полей post_status, wp_nonce, term_taxonomy_id, декомпозиция провалена — монолит заразил новую архитектуру. Anti-Corruption Layer обязан транслировать устаревшие понятия в общепринятую модель предметной области (Ubiquitous Language).
Если новый микросервис зависает из-за всплеска трафика, фасад маршрутизации обязан мгновенно активировать Circuit Breaker и временно перенаправить входящий поток обратно на монолит, предотвращая каскадный отказ всей инфраструктуры (Cascading Failure).
Развертывание Edge Proxy / Next.js Middleware перед монолитом с прозрачной пересылкой 100% трафика.
Определение изолированного домена (например, каталог товаров) с минимальным числом связей.
ИИ-синтез схем Zod и трансформаторов между базами данных монолита и новым сервисом.
Дублирование реального трафика на новый сервис без отдачи результатов клиенту для проверки стабильности.
Постепенное перенаправление 5% -> 25% -> 100% пользовательского трафика через фасад.
Удаление устаревшего кода и таблиц БД после полного замещения всех функций системы.