Симптом выглядит знакомо: в backlog лежит задача «переписать старый расчёт», но у неё нет границы. Внутри смешаны HTTP-обработчик, правила скидок, запись статуса, письмо и два неочевидных вызова соседних модулей. Команда начинает с нового сервиса, а через месяц не может честно ответить, какое поведение уже перенесено и что будет потеряно при переключении. Цена не только в задержке релиза. Пока работа идёт в стороне, legacy получает новые правила, а большой merge собирает разные риски в одну точку cut-over.
Полное переписывание кажется безопаснее потому, что его проще нарисовать: старый блок слева, новый справа, дата переключения внизу. Но объект для поставки не равен архитектурной картинке. Для начала нужен один измеримый шов: вход, один наблюдаемый результат, известный владелец и отдельный план возврата. Такой шов не обещает сохранить всю систему. Он уменьшает blast radius первого изменения и даёт команде проверяемый вопрос: этот конкретный запрос продолжает выполнять объявленный контракт или нет.
Не кусок кода, а граница, на которой можно спорить предметно
Шов не равен папке, классу или новому слою. Он существует, когда можно назвать четыре вещи: кто инициирует действие, какой вход допускается, какой ответ или эффект имеет значение и где остаётся старый владелец состояния. Для API это может быть один route и одна операция. Для фоновой задачи — один тип сообщения с ключом идемпотентности. Для интерфейса — одно действие пользователя, за которым стоит конкретный command. Если шов описан фразой «вся корзина» или «весь расчёт», его ещё нельзя отдавать в новую реализацию: это тема исследования, а не единица rollout.
У первого шва есть стоимость. Понадобится адаптер, журнал решения, отдельная проверка и временное сосуществование двух путей. Это дороже, чем удалить старый вызов в первой ветке. Но цена видима: она относится к одной операции и одному owner. Цена большого переписывания скрыта до поздней интеграции: неявные правила, редкие ошибки и побочные эффекты находят тогда, когда откат уже затрагивает весь новый контур. Fowler в исходном тексте 2004 года описывает риск критического cut-over и ценность постепенного вытеснения, а не универсальный рецепт маршрутизации.
| Наблюдение | Почему это ещё не шов | Проверка до реализации | Действие |
|---|---|---|---|
| «Перенесём расчёт заказа» | Не названы вход, ответ и боковые эффекты | Выписать одну операцию и её consumer | Сузить до preview или confirm, но не брать оба сразу |
| Новый сервис получил копию схемы | Схема не говорит о статусе, порядке и повторе | Назвать valid, invalid и repeat outcomes | Сделать compatibility record для этих случаев |
| У маршрута нет owner | Никто не принимает конфликт старого и нового пути | Указать owner решения и owner состояния | Не открывать rollout без обоих имён |
| Rollback означает «вернёмся назад» | Неясно, что менять и что уже необратимо | Отделить route return от data recovery | Начать с обратимого route change или остановиться |
| Тест зелёный локально | Он не подтверждает traffic и скрытые зависимости | Записать, какие данные и среда нужны | Оставить локальный тест в его границе |
Карта до кода: потребитель, состояние, эффект
Перед созданием адаптера стоит завести короткую карту из пяти строк. Первая — consumer: браузер, job или другой сервис. Вторая — вход: параметры, обязательные значения, повтор. Третья — наблюдаемый ответ: категория статуса, поля и порядок, если он значим. Четвёртая — effect: запись, сообщение, cache invalidation либо честная отметка unknown. Пятая — владелец: кто отвечает за решение при расхождении. Такая карта не заменяет чтение кода. Она делает чтение направленным: искать надо доказательство пяти строк, а не пытаться понять весь монолит за один проход.
Не прячьте неизвестное под словом parity. Если команда ещё не знает, повторяет ли запрос effect, это отдельный риск контракта. Сначала ставится состояние unknown, затем выбирается способ получить evidence в разрешённой среде: трасса, лог, тестовый стенд, история инцидента или ручной сценарий. До этого новый путь может существовать только как предложение. Переезд без знания effect опасен тем, что обычный успешный ответ выглядит одинаково в двух вариантах, а дубликат письма, списания или задачи появляется после того, как клиент получил 200.
OpenAPI 3.1.0, выпущенный 15 февраля 2021 года, полезен как язык HTTP-границы: он фиксирует operations, paths и форму Schema Object. Но спецификация не знает, что означает поле для бизнеса и какой порядок эффектов ожидает legacy. Поэтому OpenAPI-документ — один артефакт шва, не сертификат поведенческой совместимости. Рядом остаётся таблица инвариантов: что сравниваем, кто её утверждает и где она перестаёт быть достаточной.
Сначала один маршрут, потом новый внутренний мир
Маршрутизатор полезен не потому, что он современный, а потому что в нём можно удержать решение о пути. У маршрута должны быть явные варианты: legacy-only, новый путь как предложение после review и возвращение к legacy-only. Здесь важнее свойство: переключатель живёт на границе операции, а не размазан по десяти вызовам внутри старого кода. Тогда owner может показать, какой запрос затронут, а не объяснять, почему новый сервис иногда получил половину работы.
Первый новый adapter не обязан содержать всю доменную логику. Его задача — принять контракт шва, преобразовать известный вход и вернуть объявленную форму. Когда adapter вынужден читать глобальную переменную, напрямую обновлять общую базу и отправлять письмо, это не глубина интеграции. Это сигнал, что выбранный шов слишком широк или скрытые эффекты требуют отдельного этапа. В такой ситуации честнее уменьшить scope, чем написать ещё один универсальный gateway и потерять видимость границы.
Учебный fixture: проверить форму решения, не legacy
Ниже запускается минимальный fixture этой статьи. Он создаёт fixed synthetic plan с одним условным route, тремя exact правилами контракта и тремя непроведёнными cases. Положительная ветка проверяет, что plan не расширился до whole-system rewrite, owner назван, а автоматическое полномочие на release отсутствует. Отрицательные ветки отвергают claims о реальном parity, coverage, выполненном rollout и snapshot, похожий только по длине массивов. Это полезно для ревью формы decision record, но не для оценки вашего модуля.
node web/scripts/upgrade-2024-01.mjs --verify-fixture
Запуск не читает legacy code или history, не запускает test runner, CI, сеть либо HTTP и не измеряет coverage или performance. Он не видит настоящие ответы, базу, сообщения, пользователей и прошлые инциденты. Поэтому PASS не означает «поведение legacy сохранено». Он означает более скромную вещь: учебная запись не потеряла один шов, три правила, owner, ручной gate и неразрушительный rollback proposal. Реальное evidence собирается отдельно и хранится рядом с решением, а не подменяется строкой в terminal.
Маршрут: симптом → причина → проверка → действие
- Симптом. Задача описывает замену модуля целиком, но никто не показывает один вход и один ожидаемый результат первого релиза.
- Причина. Архитектурную границу смешали с планом разработки: новый сервис стал единицей работы раньше, чем была выделена операция и её владелец.
- Проверка scope. Для candidate route зафиксируйте consumer, input, response category, declared effect, legacy boundary и new boundary. Если одна строка не названа, шов не готов.
- Проверка обратимости. Отделите возврат route к legacy от восстановления данных. Если данных уже нельзя вернуть, не называйте шаг обратимым.
- Действие. Оставьте legacy-only путём по умолчанию, подготовьте adapter и decision record для одного шва. Новый путь не включается без owner evidence.
- Повтор. После первого evidence сравните только объявленные инварианты. Расхождение расширяет contract или уменьшает scope, а не оправдывает rewrite всего модуля.
Rollback начинается до первой доли rollout
В схеме rollback не равен удалению новой реализации. Сначала нужно решить, что возвращают: rule маршрута, версию конфигурации или входной adapter. Затем — кто выполнит действие, как подтвердить возврат и какие данные не покрываются этим действием. Если новый путь уже оставляет необратимые записи, возврат route не восстанавливает мир автоматически. Тогда plan обязан назвать migration или reconciliation отдельно и не пользоваться словом rollback как утешением для review.
Google SRE Workbook 2018 определяет canary как частичное и ограниченное по времени развёртывание с оценкой перед продолжением. Из этого не следует, что любой процент безопасен или что synthetic traffic моделирует state. Та же глава предупреждает: artificial load может увеличить code coverage, но плохо представляет state coverage в изменяемых системах. Практический вывод: сначала согласуйте, какие сигналы можно сравнить и какой route действительно можно вернуть, затем обсуждайте размер первой волны.
Если первый шов не имеет обратимого route, это не обязательно стоп всей программы. Можно выбрать read-only operation, вынести side effect или подготовить data plan с отдельным owner. Неправильное действие — назвать непроверенную операцию обратимой ради даты. Надёжная модернизация растёт из таких малых отказов: команда видит конкретное ограничение, меняет scope и сохраняет возможность вернуться к работе без широкой компенсации.
Ограничения и следующий шаг
Этот материал не выбирает ваш domain boundary, не читает историю модуля, не строит dependency graph и не доказывает, что adapter сохранит данные, latency, ошибки или правила. Synthetic names, cases и маршруты не являются шаблоном URL, схемы или policy. Fowler даёт историческое объяснение риска cut-over; OpenAPI — язык описания HTTP-интерфейса; Google — рамку частичного rollout. Ни один источник не сообщает, какой шов безопасен в неизвестной системе.
Следующий шаг короткий: выберите одну операцию, для которой команда может заполнить карту из consumer, input, response, effect, owner и rollback boundary за один рабочий сеанс. Приложите ссылку на доступное evidence и отдельно отметьте unknown. Если карта выросла до десятка операций, это не повод ускорять rewrite. Это доказательство, что первую поставку надо сузить. M7 здесь не магия: техлид делает цену и границы видимыми, чтобы команда изменила их до production-риска.
Историческая граница января 2024
К концу января 2024 были доступны исходная статья Fowler 2004 года, OAS 3.1.0 от 15 февраля 2021 года и Google SRE Workbook 2018. Здесь не используются поздние платформенные практики и не сочиняются результаты миграции. Уровень M7 проявляется в выборе scope, владельца, стоимости сосуществования и способа повторить решение; он не выдаёт учебный fixture за parity-проверку настоящей legacy-системы.
Проверяемые источники
- Martin Fowler: Original Strangler Fig Application, 29.06.2004 — Первичный текст автора метафоры: критическое cut-over переписывание оказывается сложнее ожидаемого и рискованно; постепенное вытеснение может раньше дать ценность. Это не готовая инструкция для чужого домена, базы данных или маршрутизатора.
- OpenAPI Specification v3.1.0, 15.02.2021 — Официальная спецификация описывает language-agnostic интерфейс HTTP API, paths, operations и Schema Object. Она помогает фиксировать форму интерфейса, но не доказывает runtime parity, порядок эффектов или поведение неизвестного legacy-модуля.
- Google SRE Workbook: Canarying Releases, copyright 2018 — Официальная глава определяет canary как частичное и ограниченное по времени развёртывание с оценкой перед продолжением, требует сравнивать canary и control и называет границы synthetic load. Она не задаёт процент, метрики, полномочия или rollback для этого пакета.