1. Небезпека плутанини між цими режимами
На слух різниця між цими трьома режимами здається майже академічною. Справді: старий проєкт, старий код, щось змінюємо — яка різниця, як це назвати? Але в реальній роботі назва задачі визначає майже все інше: яким буде diff, що ви зобов’язані довести, які ризики перелічити, чи можна відкотитися без локальної катастрофи.
Коротко нагадаю, що таке diff. Різниця між старою і новою версією файлів — її ви читаєте перед комітом і її ж переглядає рев’юер. Змішали в одному diff три типи роботи — ускладнили життя і собі, і перевіряльнику. А legacy-проєкти мстять не одразу, а трохи пізніше — коли цифри у звіті MRR раптом починають жити власним духовним життям. CashFlow Dashboard рахує підпискову виручку, повернення та prorating: змінилася поведінка обчислення після «невеликого чищення коду» — гарна назва задачі нікого не втішить.
Корисно тримати в голові зовсім просту схему:
Змінюється лише структура коду, а поведінка має залишитися тією самою → refactoring
Система поступово оздоровлюється, але живе на тому самому стеку → modernization
Змінюється стек, середовище виконання, framework або build → migration
Проблема новачків тут дуже людська: усе погане хочеться назвати «modernization», а все про версії — «ну це просто оновимо залежності». На жаль, Gradle від добрих слів молодшим не стає, а javax.* сам у jakarta.* не переїде.
2. Refactoring: структура змінюється, поведінка — ні
З refactoring зазвичай найпростіше: його мета чесна і скромна — зробити код зрозумілішим, коротшим, чистішим або зручнішим для тестування, не змінюючи спостережувану поведінку. Жодного нового результату для користувача, нового стеку, лікування всіх хронічних хвороб за один підхід.
У контексті CashFlow Dashboard refactoring міг би виглядати так: у MrrEngine довгий метод calculateMrr(), де змішані валідація, розрахунок proration, обробка знижок і складання результату. Винесли частину логіки в приватні методи, перейменували змінні, прибрали дублювання — і на тих самих вхідних даних отримали той самий результат. Це refactoring.
Коротка постановка такої задачі:
# TASK_SPEC
Mode: refactoring
Goal: спростити calculateMrr() без зміни результату
Scope: MrrEngine.java
Non-goals: нові залежності, зміна SQL, перехід на новий стек
Check: characterization tests проходять без змін # поведінка збережена
Тут важливе слово Mode. Воно одразу задає дисципліну. Почали змінювати JSON-відповідь endpointʼа, конфігурацію Spring, версію framework «раз уже відкрили файл» — ви вийшли з refactoring.
Refactoring майже завжди локальний: один файл, клас, група тестів. Його критерій — не «код став сучаснішим», а «зовнішня поведінка залишилася незмінною». Саме тому characterization tests із попередніх лекцій такі важливі: вони дають конкретну перевірку — ви переставили меблі, але не зламали будинок.
3. Modernization: оздоровлення без зміни стеку
Modernization — це оздоровлення застарілої ділянки без зміни платформи. Тут ви працюєте вже не з однією незручною функцією, а з проблемним шматком системи. Але сервіс залишається на тому самому стеку, у тому самому середовищі, з тими самими зовнішніми контрактами. Ви просто зменшуєте legacy-ризик крок за кроком.
Наприклад, у CashFlow Dashboard ви могли виявити, що billing-модуль викликає старий SDK платіжного провайдера з кількох місць, а місцями ходить до бази сирими SQL-запитами. Фіксуєте поточну поведінку тестами, виділяєте адаптер для провайдера, переносите частину логіки в зрозумілі межі — це modernization. Усередині майже напевно є кроки refactoring, але мета ширша: зробити систему безпечнішою для майбутніх змін.
Ось так міг би виглядати короткий артефакт на старті modernization:
# MODERNIZATION_NOTE
Ціль: зменшити ризик змін у billing-модулі
Крок 1: зафіксувати поточну поведінку тестами
Крок 2: виділити PaymentGatewayAdapter
Крок 3: прибрати прямі SQL-виклики із сервісу
Стек не змінюємо: Boot 2.7, Java 8 # це ще не migration
Ключова думка тут дуже корисна: modernization може містити багато кроків refactoring, але від цього не стає migration. Поки ви не змінюєте версію Java, major-версію фреймворку, build tool до нової лінії і не переносите застосунок у нове середовище — ви на території modernization.
І тут теж дуже легко зірватися в хаос. Частая помилка звучить красиво: «Давайте оздоровимо модуль і одразу переведемо на новий Spring Boot». На папері бадьоро. У diff — як комбайн, що їде полем без гальм.
4. Migration: зміна середовища і класу ризику
Migration починається там, де ви перестаєте змінювати лише код і починаєте змінювати середовище, у якому він живе. Нова версія фреймворку, новий runtime, нова major-версія залежності, інший build tool, новий формат конфігурації, інколи нова схема даних — перехід в інший технологічний стан.
Для нашого CashFlow Dashboard у межах цього блоку migration — річ конкретна: перший стрибок із Spring Boot 2.7 на Spring Boot 3.x і з Java 8 на Java 21. Не весь шлях до стеку Commerce OS, а перший великий перехід. Навіть один такий крок приносить нову базову лінію Java, зміну пакетів javax.* на jakarta.*, оновлення частин security-конфігурації і багато дрібних несумісностей, які люблять ховатися до першого запуску.
Іноді побачити migration найпростіше прямо в diff. Зʼявилися такі рядки — ви вже не в refactoring:
- springBootVersion = '2.7.18' // поточна major-лінія
+ springBootVersion = '3.x' // нова major-лінія, це вже migration
- javaVersion = '8' // старий baseline runtime
+ javaVersion = '21' // новий baseline runtime
У migration нас цікавить не «код став красивішим», а чи збереглася ключова поведінка після переходу. Це й є просте інтуїтивне значення feature parity: важливі користувацькі та системні сценарії працюють так, як мають, попри зміну платформи.
Гарна постановка migration-задачі звучить зовсім інакше, ніж постановка refactoring-задачі:
# TASK_SPEC
Mode: migration
Goal: підготувати пілотний перехід на Boot 3.x і Java 21
Scope: build-конфіг, security-конфіг, один read-only endpoint
Non-goals: рефакторинг BillingService, нова схема БД, нові фічі
Success: pilot slice проходить поточні перевірки та поводиться як раніше
Зверніть увагу, наскільки важливі тут Scope і Non-goals. У migration легко впасти в режим «раз уже все одно чіпаємо» — а це найкоротший шлях до неперевірюваного результату. Оновили Boot і «заодно» виділили новий інтерфейс, прибрали legacy SQL, нормалізували timestamps в UTC — зібрали в одну коробку три різні класи ризику. Така коробка зазвичай відкривається з хлопком.
Щоб не плутатися, корисно пам’ятати: migration — не лише upgrade фреймворку. Сюди потрапляють інші типи переходів:
| Тип міграції | Приклад для CashFlow Dashboard |
|---|---|
| Міграція framework | Spring Boot 2.7 → Spring Boot 3.x |
| Міграція runtime | Java 8 → Java 21 |
| Міграція build tool | Gradle 7.6.4 → новіша лінія |
| Міграція залежностей | нова major-версія SDK платіжного провайдера |
| Міграція конфігурації | зміна формату або ключів налаштувань |
| Міграція даних | окремий перехід схеми або формату зберігання |
5. Три режими на одному CashFlow Dashboard
Коли теорія починає розпливатися, найкраще повернути її до одного й того самого проєктного контексту. Одні й ті самі файли CashFlow Dashboard беруть участь у всіх трьох режимах — у різні моменти і з різними цілями. Саме тому порівняння через таблицю тут корисніше за будь-яке пафосне визначення.
| Режим | Що змінюємо | Що має залишитися | Головний артефакт | Типовий ризик |
|---|---|---|---|---|
| Refactoring | структуру коду | спостережувану поведінку | TASK_SPEC + локальні перевірки | випадково змінити бізнес-логіку |
| Modernization | проблемну legacy-ділянку крок за кроком | зовнішні контракти й бізнес-сенс | risk map, baseline, поетапний план | розповзання scope і надто широкий diff |
| Migration | стек, runtime, framework, build | feature parity на ключових сценаріях | інвентаризація, докази сумісності, migration plan | breaking changes і дорогий rollback |
Подивіться, як один і той самий BillingService живе у трьох світах. Винесли повторюваний шматок логіки в метод — refactoring. Побудували навколо нього чисту межу, виділили адаптери, прибрали крихкі залежності — modernization. Перевели на новий Boot, нову Java і нову лінію залежностей — migration.
Тобто питання не в тому, який файл ви відкрили, а в тому, що саме ви обіцяєте змінити і що зобов’язуєтеся зберегти. Це дуже корисна думка для початківців: назви режимів описують не місце в проєкті, а характер змін. Той самий build.gradle відкривають і для маленького cleanup-коментаря, і для важкої migration. Зовні файл один, інженерно це різні світи.
6. Ціна змішування всіх режимів в одному diff
Ось тут і починається практичний біль, заради якого взагалі потрібна вся лекція. Змішаний diff майже завжди виглядає «продуктивно»: багато руху, файлів, покращень, здається, що команда одним махом закриє всі борги. Біда в тому, що він погано читається, погано перевіряється і ще гірше відкочується.
Подивіться на анти-приклад постановки:
# TASK_SPEC
Goal: переписати BillingService під Boot 3.x, заодно винести
Stripe SDK у новий інтерфейс і нормалізувати timestamps в UTC
Acceptance: tests pass, код став чистішим
Тут в одному абзаці сидять три різні роботи. Перехід на Boot 3.x — migration. Новий інтерфейс навколо Stripe SDK — modernization з елементами refactoring. Нормалізація timestamps у UTC — зміна даних і поведінки, можливо, окрема міграція даних. Падуть тести — не зрозуміти, яка частина винна. Зміниться фінансовий звіт — не довести, що це неминучий наслідок migration, а не побічний ефект від «заодно покращили». Потрібен rollback — ви не знаєте, що відкотити.
Набагато здоровіше розкласти це на незалежні задачі — хоча б на три чесні набори змін: migration pilot із чіткими non-goals; modernization навколо SDK на старому або стабілізованому новому стеку; зміна роботи з часом окремо.
Змішування режимів особливо небезпечне тим, що ламає verification. У refactoring доказ один: поведінка незмінна. У modernization інший: ризик зменшився, межі чистіші. У migration третій: feature parity після зміни середовища. Запхнули все в одну коробку — який набір доказів збирати? Відповідь зазвичай чесна: «ну… tests pass». Цього недостатньо. Ось чому «маленький зрозумілий diff» — не бюрократія, а спосіб відокремити один тип відповідальності від іншого і не сперечатися потім у review до ночі, хто саме запустив цю чудову снігову кулю.
7. Здорова migration-постановка
Гарна migration-задача майже завжди звучить трохи нудніше, ніж хочеться: менше героїзму, більше інженерної дисципліни. Але саме тому вона стає виконуваною. Вам потрібно не надихнути систему на зміни, а обмежити її так, щоб перехід був доказуваним.
Корисний чорновик може виглядати так:
# MIGRATION_DRAFT
Ціль: перейти на Boot 3.x і Java 21 для pilot slice
Scope: build-конфіг і один read-only потік
Non-goals: рефакторинг MrrEngine, зміна БД, нові API
Feature parity: JSON відповіді та коди помилок не змінюються
Rollback: окрема гілка міграції і tag baseline
Evidence before start: current-state inventory + risk map
Такий текст робить одразу кілька корисних речей. Обмежений scope не дасть утягнути півсистеми за одним рядком версії в build.gradle. Non-goals рятують від «ми ще трохи покращили». Rollback і evidence до старту переводять задачу з емоції «треба оновитися» в інженерний процес.
Ще один важливий момент: migration у нашому навчальному контексті — робота з високим ризиком. Мислення «зараз Claude сам усе акуратно підправить» тут не підходить. Потрібен review-first: спершу артефакти, потім перевірювані кроки, і лише потім зміни. Не можете назвати цільову межу, pilot slice, non-goals і критерій збереження поведінки — задача ще не готова до реалізації. Вона поки що лише гарно нервує.
8. Межа міграції в цьому модулі
Це важливо проговорити окремо, тому що у слова migration неприємна звичка розростатися до розмірів всесвіту. У цьому блоці ми не доводимо CashFlow Dashboard за один стрибок до всього стеку Commerce OS. Беремо перший великий перехід: Boot 2.7 → Boot 3.x і Java 8 → Java 21. Це не «навчальне понарошку» — навпаки, інженерно чесно: вже тут достатньо breaking changes, нової сумісності, оновленої конфігурації та залежності від build-інструментів. Потягніть одночасно ще й наступний великий перехід — знову змішаєте кілька незалежних migration-хвиль в один стресовий проєкт.
Щойно режим названо, робота розкладається природно: спершу фіксуємо Migration Current State, потім звіряємо його з official docs і release notes, далі збираємо COMPATIBILITY_MATRIX.md, а на цій базі пишемо MIGRATION_PLAN.md для пілотного зрізу. Поки цього набору немає, відкривати build.gradle заради самого upgrade рано.
Тому найважливіше тут — назвати режим до того, як ви відкриєте build.gradle. Refactoring — очікуєте збереження поведінки при зміні структури. Modernization — зменшуєте legacy-ризик крок за кроком. Migration — змінюєте середовище і доводите feature parity на вибраному зрізі. І в цей момент старий CashFlow Dashboard перестає бути містичним болотом, де «щось треба оновити», і знову стає звичайною інженерною задачею: з ясним scope, маленьким diff, зрозумілим доказом і без надії на удачу як на основний інструмент розробки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ