JavaRush /Курси /Claude code /Контракт поведінки та пофазний план

Контракт поведінки та пофазний план

Claude code
Рівень 28 , Лекція 4
Відкрита

1. План починається з поведінки, а не з build.gradle

На цьому етапі дуже хочеться відкрити файл збирання, змінити кілька версій і подивитися, що засвітиться першим. Спокуса зрозуміла: ви знаєте blocker'и, читали changelog, бачили, що javax.* у Boot 3 з вами не поїде. Але якщо почати з конфігів, не відповівши «що має залишитися працездатним», — отримаєте технічну метушню без інженерного сенсу.

Зелений збір — це ще не міграція, а лише умова, щоб система запустилася. Мета не в тому, щоб «підняти версію Spring Boot», а в тому, щоб перевести систему в нове середовище так, щоб обов’язкова поведінка збереглася. Контракт збереження поведінки — це ті самі критерії приймання, тільки для цілого переходу між технологічними станами, а не для однієї функції.

Тут добре працює побутова аналогія. Уявіть, що магазин переїжджає в нове приміщення. Стелажі інші, касу треба замінити, термінали оновити — це можна. Але покупець усе одно має зайти, вибрати товар, оплатити й отримати чек. Каса новіша, а повернення перестало працювати — переїзд формально відбувся, а по суті провалився.

Саме тому формулювання на кшталт «BillingService має працювати» або «застосунок має збиратися на новій версії» для міграційного плану занадто слабкі: вони прив’язані до реалізації або до факту запуску, але не до спостережуваної поведінки.

Слабке формулювання Чому не годиться Робоче формулювання
BillingService має працювати це імʼя класу, а не поведінка POST /api/subscriptions створює підписку і повертає очікувану відповідь
Помилки не мають змінитися занадто розпливчасто коди PAYMENT_FAILED і PRORATION_INVALID зберігаються
Система має бути швидкою немає вимірюваного критерію p95 для читання підписки не гірше ніж 1.2x від baseline

Якщо коротко, compatibility matrix відповідає на питання що заважає перейти. Контракт збереження поведінки — навіщо цей перехід робиться і що не можна втратити дорогою.

2. Контракт поведінки для CashFlow Dashboard

Контракт збереження поведінки звучить серйозно, але по суті це просто список обіцянок системи зовнішньому світу мовою спостережуваного результату. Користувачеві, API-клієнту й тестовому стенду байдуже, який клас викликався всередині. Їм важливо, що endpoint відповів, webhook обробився, timestamp не з’їхав на три години ліворуч, а код помилки не перетворився на загадкове «щось пішло не так».

Для CashFlow Dashboard такий контракт зазвичай будується навколо кількох зон: критичні потоки, API-контракти, поведінка даних, помилки, інтеграції — і лише потім продуктивність, у негероїчному сенсі: не «стало швидше на 73%», а «не стало настільки гірше, що користувачі це відчують».

Зона поведінки Що зазвичай фіксують для CashFlow Dashboard
Критичні потоки create subscription, cancel, refund, plan switch
API-контракти status code, JSON schema, обов’язкові поля
Поведінка даних UTC, статуси підписки, суми й округлення
Помилки збереження важливих бізнес-кодів помилок
Інтеграції webhook-підпис, обробка подій платіжного сервісу
Перевірка продуктивності latency не деградує за розумний поріг
Зворотна сумісність pilot не змінює схему БД і зовнішні контракти

Нижче — вдалий фрагмент такого контракту. Він короткий, але вже придатний для review, бо говорить про поведінку, а не про внутрішності проєкту:

## Контракт збереження поведінки
- create, cancel, refund і plan switch продовжують працювати
- `/api/subscriptions/*` повертає ту саму JSON schema
- timestamps залишаються в UTC, без нормалізації в pilot
- коди `PAYMENT_FAILED` і `PRORATION_INVALID` зберігаються
- webhook від платіжного провайдера валідовується тим самим secret
- latency читання підписки не гірша за `1.2x` від baseline

Зверніть увагу, тут немає жодного слова про BillingService, SubscriptionFacade або MigrationHelper. І це добре: реалізація змінюється, а контракт має її переживати й залишатися читабельним для того, хто поточний код не писав.

Наймарніше формулювання тут — «все має працювати як раніше». Воно звучить переконливо, але перевірити його неможливо. Щойно ви розкладаєте це «все» на конкретні потоки, endpoint'и, коди помилок і поведінку даних — міграція перестає бути релігійним актом і стає інженерним завданням.

3. Phased migration plan — маршрут із gate

Коли compatibility matrix уже готова, виникає спокуса перетворити її на плаский чек-лист: спочатку Java, потім Boot, потім Gradle, потім тести. На серветці це зручно, у живому проєкті — погано: немає фази зупинки, немає gate, немає точки, де можна чесно сказати «далі йти рано».

Тому пофазний план майже завжди виграє у списку завдань: він розкладає перехід на чотири стани — Baseline, Pilot, Broader update і Cleanup. Не магічна схема, але вона добре підходить для першого стрибка з Boot 2.7 і Java 8 на гілку Boot 3.x і Java 21.

Фаза Сенс Переходимо далі, якщо Точка відкату
Baseline фіксуємо початковий стан characterization baseline зелений, matrix узгоджена tag або commit початкового стану
Pilot мігруємо вузький безпечний slice parity subset проходить для pilot-сценарію повернення до baseline tag і старого deployment
Broader update розширюємо міграцію на основний контур критичні потоки проходять planned validation відкат до стану після pilot
Cleanup прибираємо тимчасові shim і старі залежності немає legacy-хвостів, поведінка збережена відкат до останньої робочої фази

Саме Pilot найчастіше рятує команду від великих неприємностей. Він спеціально має бути вузьким. Для CashFlow Dashboard розумніше брати не найнебезпечніший сценарій запису, а, наприклад, один read-only endpoint на кшталт GET /api/subscriptions/{id}. Чому не refund? Тому що refund — це вже бізнес-операція з побічними ефектами, а мета pilot-фази не в тому, щоб одразу виграти війну, а в тому, щоб перевірити, чи життєздатний сам migration-підхід на невеликій ділянці.

flowchart TD
    A[Baseline] -->|matrix + baseline готові| B[Pilot]
    B -->|pilot slice проходить parity subset| C[Broader update]
    C -->|critical flows проходять planned validation| D[Cleanup]

Baseline-фаза при цьому не «порожня». Вона дуже важлива, просто в ній мало романтики. У ній ви фіксуєте поточний working state, перевіряєте characterization suite, зберігаєте baseline tag, збираєте matrix і перечитуєте contract. Ця фаза потрібна, щоб потім не згадувати: «А на якому саме коміті в нас усе ще працювало?»

Broader update — це вже розширення pilot-підходу на основний контур. Але лише після того, як pilot довів, що ланцюжок «new runtime → new Boot → current code with changes → planned validation» взагалі складається в робочий сценарій. Cleanup наприкінці потрібен, щоб не залишити проєкт у стані «ніби мігрували, але половина тимчасових сумісних костилів так і живе поруч зі старим кодом».

4. Rollback point — артефакт, а не надія

Щойно мова заходить про rollback, багато хто автоматично думає про git revert. Git, звісно, чудовий, але сам по собі він не вирішує всю проблему. Якщо ви встигли змінити dependency set, переключили deployment, підняли новий runtime або, не дай Боже, зачепили схему даних, однієї команди відкату історії вже недостатньо. Тому rollback point у міграційному плані має бути зафіксований так само явно, як target version або gate фази.

У простому випадку rollback point — це tag або commit, що відповідає останньому гарантовано робочому стану. Але для міграції цього мало. Вам потрібно ще розуміти, який deployment-артефакт відповідає цьому стану, який набір залежностей вважається попереднім стабільним комплектом, чи є feature flag, який дозволяє вимкнути новий зріз без екстреної хірургії, і чи не змінили ви щось таке, що вже не повернеться простим відкатом коду.

З цієї причини pilot-фаза зазвичай спеціально уникає schema changes. Щойно ви додаєте справжню міграцію даних у найперший стрибок, rollback перестає бути дешевим. Він перетворюється на окремий мініпроєкт, а нам поки що потрібно утримати міграцію в режимі контрольованого переходу, а не випробування нервової системи.

Вдалий блок rollback у плані виглядає дуже приземлено:

## Очікуваний відкат
- baseline tag: `v2.7-baseline`
- deployment rollback: повернути артефакт `cashflow:2.7-baseline`
- dependency rollback: відновити wrapper і dependency block з baseline
- pilot toggle: вимкнути `pilot_v3_endpoint`
- db schema в pilot не змінюється

Тут немає жодної поезії, зате все зрозуміло. Якщо pilot не проходить перевірку, ви не сперечаєтеся із собою і не вигадуєте план порятунку на ходу. У вас уже записано, до якого стану і яким способом повертатися.

Хороша точка відкату відповідає на питання «куди саме ми повертаємося?», а не «чи сподіваємося ми, що повернутися вдасться». Різниця між цими двома формулюваннями приблизно така сама, як між запасним колесом у багажнику та надихаючою фразою «ну, якщо що, щось придумаємо».

5. Planned validation та open risks до старту

На етапі планування дуже легко переплутати дві речі: заплановану перевірку і доказ того, що перевірку вже пройдено. Сьогодні у нас може бути лише перше. Це важливо, бо міграційний план не має вдавати звіт про виконання. Його завдання — чесно сказати, що саме ми будемо перевіряти на кожній фазі, чим підтверджуватимемо збереження поведінки і які ризики досі висять над проєктом.

Planned validation зазвичай прив’язується до фаз. Для Baseline це може бути зелений characterization suite і зафіксований baseline tag. Для Pilot — subset контрактних перевірок на одному endpoint, smoke на staging і порівняння очікуваної відповіді з baseline. Для Broader update — integration tests за критичними потоками, webhook smoke, sanity check за latency, можливо, порівняння важливих JSON schema. Для Cleanup — фінальний прогін тих самих перевірок уже без legacy-костилів.

Тут корисно розвести три поняття, які часто злипаються:

Елемент На яке питання відповідає Приклад
Gate чи можна переходити до наступної фази pilot slice пройшов parity subset
Planned validation чим саме це підтверджуватиметься characterization, integration, smoke
Open risk що ще може зламати фазу невідома сумісність custom UserType

Сюди якраз і доїжджають незакриті результати дослідження. Якщо в research notes лишилися assumption і unknown, а для них так і не знайшлося достатнього evidence в матриці, вони отримують статус needs manual validation і переходять в Open risks плану. План не має вдавати, що цих дірок більше немає.

З open risks історія не менш важлива. Якщо ви не записали ризик, він не зник. Він просто готує неприємний сюрприз. У плані зручно розділяти два типи ризиків: прийняті й невирішені. Прийнятий ризик — це те, з чим ви свідомо готові жити на цій фазі, наприклад коротке повторне розгортання з невеликим технічним вікном. Невирішений ризик — це те, що реально може заблокувати старт фази, наприклад неперевірена сумісність кастомного Hibernate-типу з новою гілкою.

## Відкриті ризики
Прийняті:
- коротке повторне розгортання з простоєм до 5 хвилин

Невирішені:
- сумісність custom Hibernate UserType
- поведінка старого JsonAdapter на новій гілці jackson

Такий блок здається простим, але саме він не дає плану вдавати, що він завершений і ідеальний. Хороший MIGRATION_PLAN майже завжди трохи дратує, тому що в ньому чесно видно, де ще болить.

6. Збирання MIGRATION_PLAN.md з артефактів

До цього моменту у вас уже є майже всі входи для нормального плану. Є current-state inventory, є compatibility matrix, є risk map із попереднього блоку, є characterization baseline, є контракт збереження поведінки. Сам план не має народжуватися з повітря. Він збирає ці шматки в маршрут, який можна читати, обговорювати, рев’ювати і лише потім виконувати.

Практично MIGRATION_PLAN.md зручно сприймати як один головний файл, у якому сходяться всі попередні артефакти. Це вже не нотатка для себе, а робочий документ для вас, для рев’юера і для Claude Code, якщо ви використовуєте його як помічника під час підготовки чернетки.

Каркас файла може виглядати так:

# MIGRATION_PLAN

## Мета / Межі / Не-цілі
## Цільові версії
## Контракт паритету фіч
## Фаза 0: baseline
## Фаза 1: pilot
## Фаза 2: широке оновлення
## Фаза 3: чистка
## Запланована валідація / відкат / відкриті ризики

Якщо ви підключаєте Claude Code до цієї роботи, йому краще давати не абстрактне прохання «склади план міграції», а вже зібраний пакет входів. Наприклад так:

На основі `Migration Current State`, `COMPATIBILITY_MATRIX.md`, `RISK_MAP.md` і
контракту збереження поведінки склади чернетку `MIGRATION_PLAN.md`.
Потрібні фази Baseline, Pilot, Broader update і Cleanup.
Для кожної фази вкажи gate, planned validation, rollback point і open risks.
Не пропонуй виконувати команди й не змінюй код.

Тут важлива остання строка. На цьому етапі Claude має допомагати як редактор і аналітик, а не як нетерплячий розробник, який уже тягнеться редагувати build.gradle. Чернетку він збере швидко, але перевіряти, чи справді gate вимірюваний, чи справді rollback point конкретний, чи справді non-goals утримують scope, доведеться вже вам.

У результаті хороший MIGRATION_PLAN.md пов’язує все, що ви зробили раніше. Current-state inventory відповідає за чесну точку старту, compatibility matrix — за технічні обмеження й порядок, контракт збереження поведінки — за сенс переходу, а phased plan — за маршрут. І коли всі ці частини починають працювати разом, міграція нарешті перестає бути фразою «ну ми там стек оновлюємо» і стає документом, яким можна йти без героїзму й без лотереї.

7. Хмарний планувальник

Окремо варто сказати про хмарний планувальник Claude Code. Міграційний план — це, мабуть, найканонічніший кейс, де хмарне планування, умовно /ultraplan або аналог — точна назва й доступність можуть змінюватися від версії до версії, — дає максимальну користь. Причин три, і всі вони випливають із природи самої задачі.

По-перше, у вас на руках великий комплект доказів. Discovery із попередніх тем модуля зібрав current-state inventory, official migration guide, changelog, compatibility matrix і dependency graph — локальний контекст легко переповнюється таким обсягом, а хмарна поверхня спокійно тримає весь evidence в одному просторі.

По-друге, фазовий план вимагає узгодженості між фазами: baseline, pilot, broader update і cleanup мають бути взаємно несуперечливими, а хмарний планувальник уміє сам ловити такі нестикування — наприклад, що у фазі 1 видаляється застарілий API, на який ще спирається фаза 0.

По-третє, міграція рідко буває терміновою, це робота на тижні й місяці, тож невелика затримка хмарного агента не є блокером — паралельно ви спокійно готуєте baseline і tests.

Висновок хмарного планувальника лягає в той самий шаблон, що описаний вище: Goal, Scope, Non-goals, Target versions, Feature-parity contract, Phase 0..3, Planned validation, Rollback expectation, Open risks. Межа схвалення при цьому не зсувається: фазовий план затверджує власник команди або технічний лід, а хмарний планувальник — це інструмент для чернетки та звірки узгодженості, не особа, що приймає рішення.

І якщо міграція зачіпає чутливі дані або секрети — наприклад, міграцію конфігурації з доступами до продакшну або міграцію схеми з персональними даними клієнтів, — то, як ми обговорювали раніше в курсі про витік даних через хмарні поверхні, хмарний планувальник у регульованому середовищі зазвичай не є варіантом, і план залишається в локальному робочому циклі.

Після цього залишається вже не гадати, а виконувати pilot на тих самих опорах: маленьким зрізом, з validation evidence і із заздалегідь підготовленим rollback.

1
Опитування
Refactoring, modernization і migration, рівень 28, лекція 4
Недоступний
Refactoring, modernization і migration
Три режими змін, discovery, changelog-driven research і поетапний план міграції
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ