1. Слово «міграція» надто загальне
Коли ви вперше чуєте слово migration, дуже легко уявити щось однотипне: підняли версії, виправили кілька імпортів, прогнали тести, готово. Проблема в тому, що під одним словом ховаються зміни різної природи: одні ламають компіляцію, інші — поведінку в рантаймі, треті — лише збирання в CI. Тому перша доросла дія в migration execution — назвати тип міграції, а не оголосити: «ми оновлюємо проєкт».
До цього місця у нас був один pilot: migration/boot3-pilot на reports/* з вибраним scope, rollback і validation. Сьогодні я не розширюю його до всього сервісу, а розкладаю за типами змін: де ризик і які датчики на кожному рівні. Дані та конфігурацію сюди свідомо не включаю — там інший ризик і інший патерн безпеки.
На практиці корисно тримати перед очима ось таку карту:
| Сімейство міграції | Що насправді змінюється | Де найчастіше ламається | Перша корисна перевірка |
|---|---|---|---|
| single-library | одна конкретна залежність, яку ви оновлюєте явно | локальне збирання, інтеграційні тести | точковий diff версії + прогін цільового набору тестів |
| dependency | дерево залежностей цілком, включно з транзитивними | конфлікти версій, несумісні плагіни, неочікувані класи в classpath | порівняння lockfile або графа залежностей |
| framework | правила платформи, API, автоконфігурація, домовленості | компіляція, конфігурація, безпека, поведінка в рантаймі | читання release notes + characterization tests |
| runtime | версія мови та середовища виконання | CI, контейнер, рефлексія, TLS, поведінка JVM | matrix-прогін на старій і новій версії |
| build | wrapper, build plugins, pipeline, образ збирання | «локально працює, в CI падає» | clean build у CI на цільовому середовищі |
Ця таблиця здається майже нудною — а це добрий знак: інженерна карта й має виглядати трохи нудніше, ніж катастрофа. Нудна таблиця дешевша за бадьорий дзвінок о 23:40 із питанням «а чому після оновлення Java у нас відвалився звіт, хоча код ми не чіпали?».
Для CashFlow Dashboard це особливо корисно, тому що перший великий стрибок у курсі — не одна міграція, а гібрид: ми зачіпаємо framework, runtime і build одночасно, а поруч ворушаться залежності. Не розкладіть цю суміш на частини — і будь-який MIGRATION_PLAN.md перетвориться на літературний твір без шансів на виконання.
2. Один цикл для всіх типів міграцій
Добра новина в тому, що типів міграцій багато, а базовий цикл у них один і той самий: він застосовується до різних класів змін, кожного разу з уточненням, де ризик і яким evidence доводити коректність. Вигадувати п’ять процесів не потрібно.
flowchart LR
A[Журнал змін / release notes] --> B[Pilot-зріз]
B --> C[Тести й перевірки]
C --> D[Малий diff]
D --> E[Review і рішення]
Ключове тут — спочатку читати release notes, а не відразу редагувати build.gradle з фразою «ну зараз усе само підкаже». Claude Code у цій фазі корисний, але в дуже конкретній ролі: читати документацію, зіставляти breaking changes із файлами pilot-зрізу, підказувати, де допоможе компілятор, а де врятують тести або smoke checks. Хороший запит виглядає так:
Вивчи release notes Spring Boot 3.x і наш pilot-зріз `reports/*`.
Код не змінюй.
Поверни:
1) які breaking changes стосуються цього зрізу;
2) що зловить компілятор;
3) що зловлять лише тести або runtime;
4) з якого файла безпечніше почати pilot.
Зверніть увагу на тон запиту: не «мігруй модуль», а «прочитай, зістав, поясни, де ризик». Це verification-first: модель допомагає побачити карту, а не кидається перебудовувати місто.
3. Міграції single-library і dependency
Найобманливіший тип міграції — той, що виглядає маленьким. Здається, ви просто змінюєте версію однієї бібліотеки, але насправді вона рідко приїжджає одна: тягне транзитивні залежності, обмеження сумісності, новий набір класів. Пакет оновили один, а розмовляти почали п’ятеро.
Для початківця зручно розділити два близькі поняття. Single-library migration — ви явно змінюєте одну залежність: версію SDK, драйвера, клієнта, starter-а (не плутайте зі зміною package або namespace). Dependency migration — коли через це змінюється все дерево. У Java це помітно через BOM, плагіни та бібліотеки, пов’язані через classpath; у JavaScript аналог — peer dependencies.
Ось чому lockfile — не бюрократія, а доказ: показує, що насправді змінилося, а не що ви думали, що змінили.
./gradlew dependencies > build/deps-before.txt
./gradlew --write-locks
./gradlew clean test --tests 'reports.*'
git diff gradle.lockfile # дивимося, хто насправді змінився
./gradlew dependencyInsight --dependency jackson-databind
Цей фрагмент здається сухим, але він дуже практичний, і це значно краще, ніж оновити п’ять пакетів разом, а потім обговорювати, чия версія вирішила зіпсувати вам ранок.
У CashFlow Dashboard такий підхід особливо важливий поруч із фінансовою логікою. Торкнетеся бібліотеки в payments або billing — ціна помилки висока, тому перший pilot-зріз не починається там. Беріть reports або іншу ділянку лише для читання: від складних зон ви не відмовляєтеся назавжди, просто не робите з них полігон для перших падінь.
І ще один важливий момент — у single-library/dependency migration дуже поширений антипатерн: «раз уже залізли, оновімо заодно все застаріле». Звучить господарно, але руйнує довідність. Оновили вісімнадцять залежностей і два дні шукаєте, чий сюрприз прийшов у збирання, — це не міграція, а археологія після вибуху.
4. Framework migration: змінюються правила платформи
Із framework migration усе хитріше: тут ви змінюєте не версію залежності, а правила гри всієї платформи — домовленості, автоконфігурацію, життєвий цикл компонентів, інтеграцію з безпекою та валідацією. Тому major-оновлення майже завжди болючіше, ніж виглядає в diff.
У нашому проєкті це видно наочно: Spring Boot 2.7 → Boot 3.x — класична framework migration. У ній є dependency- і runtime-шар, але головна проблема — змінилися базові контракти платформи. Найпомітніший і мемний приклад — перехід javax.* → jakarta.*.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.validation.Valid;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.Valid;
Компілятор таке ловить охоче, і це навіть приємно — частина роботи робиться голосно й чесно. Але framework migration не закінчується на імпортах. Куди неприємніше те, що не падає під час компіляції: правила Spring Security, actuator, спостережуваність, автоконфігурація. Корисно дивитися на framework migration так — хто що ловить:
| Що змінюється | Хто зазвичай ловить проблему |
|---|---|
| javax → jakarta в коді | компілятор |
| зміни в security-конфігурації | інтеграційні тести |
| actuator / observability / автоконфігурація | runtime і CI |
| поведінка web-шару та серіалізації | API tests і smoke checks |
Саме тому release notes для framework migration не факультативні: їх не читають лише ті, хто любить дізнаватися про breaking changes із production-логів. Якщо у вас є characterization tests із попередніх тем, драматизму менше: ви не вірите, що «Boot 3 теж має працювати», а порівнюєте поведінку pilot-зрізу до і після. Це перетворює міграцію з ритуалу на інженерну процедуру.
У CashFlow Dashboard перший framework-pilot логічно починати не з billing, payments або auth, а зі спокійного звітного контролера, де ціна помилки нижча.
5. Runtime migration: той самий код, нова платформа
Runtime migration часто недооцінюють, тому що здається: якщо код компілюється на новій версії мови — начебто все добре. На жаль, середовище виконання значно підступніше. Ви змінюєте поведінку віртуальної машини, базові бібліотеки, TLS, рефлексію, контейнерний образ, іноді навіть те, як запускаються тести.
У курсовому проєкті Java 8 → Java 21 — не додаток до framework migration, а повноцінна runtime migration. Boot 3 вимагає новішу Java, тому лінії йдуть разом, але розділяти їх у голові корисно. Інакше будь-яка проблема виглядає як «Boot 3 щось зламав», хоча насправді CI просто збирає проєкт на старому JDK.
Найдешевший спосіб не сперечатися про середовище виконання — ганяти старий і новий runtime на одному наборі перевірок. Якщо код підтримує обидві версії, зручна matrix у CI; але для pilot-а Boot 2.7 / Java 8 → Boot 3.x / Java 21 коректніше порівнювати baseline branch на JDK 8 і migration branch на JDK 21 — Boot 3 не зобов’язаний запускатися на Java 8.
# Такий matrix-підхід підходить, коли один і той самий код підтримує обидві JDK.
strategy:
matrix:
java: [ '8', '21' ]
steps:
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
- run: ./gradlew test --tests 'reports.*'
Сенс цього фрагмента не в красі YAML, а в дисципліні: відтворювані перевірки на двох рантаймах покажуть, де поведінка розходиться. Якщо baseline стабільний, а migration branch сиплеться — копати в runtime-шарі, а не звинувачувати навмання весь framework upgrade.
У runtime migration є ще одна неприємна особливість: частина проблем не видна локально, коли середовище розробника відрізняється від CI. Звідси правило: не «на ноутбуці зелено», а «в пайплайні зелено». Лише це evidence — решта оптимізм.
6. Build migration: ламається спосіб збирання
Build migration — той тип змін, який найчастіше дратує сильніше за всі інші, тому що код може бути абсолютно нормальним, а проєкт усе одно не збирається. І найприкріше, що розробник у таких випадках любить говорити: «Але ж я бізнес-логіку не змінював». Саме в цьому й проблема: ламається не предметна область, а механіка доставки — wrapper, плагіни, pipeline, образ збирання, іноді кроки генерації коду.
Коли ви змінюєте build-шар, ви фактично відповідаєте на питання: «Чи зберемо ми тим самим способом проєкт завтра, у CI, на іншій машині?». Якщо відповідь залежить від фаз місяця — build migration ще не завершена.
Найпростіший build-pilot не має бути героїчним — його завдання довести відтворюваність:
- name: Перевірка середовища
run: |
java -version
./gradlew --version
- name: Повне збирання pilot-зрізу
run: ./gradlew clean build
Цей шматок хороший тим, що відразу показує, яка Java і який Gradle wrapper реально в пайплайні. Якщо локально ви на Java 21, а в CI старий рантайм — краще побачити це в перших рядках логу, ніж після двадцяти хвилин падінь на незрозумілих плагінах.
Тут же ховається ще одна тонкість: build-пайплайн і згенерований конфіг — теж частина migration-картини. Оновили Boot і Java, а Docker-образ у CI тягне старе середовище; або wrapper новий, а plugin для генерації коду несумісний. «Працює локально, падає в CI» — build migration, яку просто не назвали своїм ім’ям.
Якщо сказати зовсім просто, build migration перевіряє не «чи вміє код жити», а «чи вміє команда знову і знову отримувати один артефакт із чистого стану». Для legacy це важливіше за рефакторинг: без відтворюваного збирання ви й відкат не зробите.
7. Карта типів міграцій для CashFlow Dashboard
Тепер зберемо все в одну картинку на нашому проєкті — це корисно не лише для розуміння, а й як майже готовий фрагмент MIGRATION_PLAN.md. Коли ви називаєте тип міграції за шарами, план одразу стає зрозумілішим: у кожного шару з’являється своя перевірка, свій pilot і свої винятки.
| Шар у першому pilot CashFlow Dashboard | Що змінюємо в reports-зрізі | Чим доводимо коректність | Що свідомо не чіпаємо |
|---|---|---|---|
| framework | Spring Boot 2.7 → 3.x, javax → jakarta, частину web/autoconfig | компіляція + інтеграційні тести звітів + smoke на endpoint | billing, payments, auth |
| runtime | Java 8 → 21 для того самого pilot-зрізу | CI matrix, один і той самий набір тестів на двох JDK | глобальні JVM-особливості всієї системи |
| build | wrapper, версія Java у CI, кроки збирання | clean build у CI та повторюваний лог середовища | release pipeline цілком |
| dependency | супутні бібліотеки, підтягнуті переходом | lockfile diff + dependencyInsight + targeted tests | великі оновлення зовнішніх SDK |
Даних і конфігурації тут свідомо немає: щойно ви додаєте schema changes, backup і owner approval починають жити за іншими правилами, а pilot перестає бути тим самим дешевим Boot 3 зрізом.
Невеликий фрагмент плану в такому стилі може виглядати так:
## Карта pilot-зрізу
- framework: Spring Boot 2.7 → 3.x в `reports/*`
- runtime: Java 8 → 21 для того самого зрізу
- build: оновити wrapper і CI JDK
- виключено: `billing/*`, `payments/*`, schema changes
Це вже звучить як інженерний документ, а не бойовий клич. І в цьому, власне, весь сенс сьогоднішньої лекції. Перестаєте говорити «оновимо проєкт» і починаєте «робимо framework + runtime + build pilot на reports/* з окремим dependency review» — і міграція стає керованою. Не легкою, не безпомилковою — керованою. У legacy-коді це майже завжди найкраща новина дня.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ