1. Дані та конфігурація — окремий клас ризику
На цій темі багато хто вперше розуміє, чому міграції даних і конфігурації лякають навіть сильних розробників. Код відкочується комітом, а проблему видно очима компілятора. Дані та налаштування ламаються мовчки, без спецефектів, але з дорогими наслідками. Це окремий клас ризику.
І тут важливо не змішувати цю розмову з першим migration/boot3-pilot на reports/*. Там був маленький pilot лише для читання Boot 3 без змін схеми та конфігураційних стрибків. Тут приклад інший — зі своєю гілкою, backup і дисципліною відкату. Завдання не розширити той pilot до небезпечного масштабу, а показати інший клас ризику.
На прикладі CashFlow Dashboard це видно особливо добре. Під час переходу зі Spring Boot 2.7 та Java 8 на Boot 3.x і Java 21 гучні проблеми майже чесні: javax.* не компілюється, тести падають. Підступніше — дані. У модулі звітів частина подій у локальній зоні, частина — в UTC, а розрахунок MRR по-різному трактує одне поле часу. Проєкт збереться ідеально, а звіти за місяць розійдуться з реальністю на кілька відсотків. Компілятор мовчить.
Те саме з конфігурацією. Старий код читав REPORT_TZ, новий чекає cashflow.reports.default-zone, staging живе на старих змінних — локально все зелене, а в staging поведінка несподівана. Не аварія, а «щось дивно рахує».
Корисно тримати перед очима просте порівняння:
| Що мігруємо | Що зазвичай допомагає впіймати помилку | Чим небезпечно |
|---|---|---|
| Код | компілятор, тести, diff | проблема зазвичай помітна одразу |
| Дані | backup, звірка з baseline, validation report | можна тихо спотворити бізнес-результат |
| Конфігурацію | інвентаризація налаштувань, dry-run, owner review | проєкт може «працювати», але в неправильному режимі |
Саме тому для більшості студентів це насамперед тема про розуміння ризику. Немає явного доступу, відповідальності та узгодження — завдання не «героїчно виконати міграцію», а розпізнати, чому її не можна робити наскоком. Іноді професіоналізм — це просто не натискати Enter.
2. Межі безпеки до першої зміни
Якщо кодову міграцію іноді ще намагаються почати з духом «розберемося дорогою», то для даних і конфігурації такий підхід майже завжди закінчується нічним повідомленням у чат і знайомством з черговим інженером. Межі встановлюють заздалегідь.
| Межа | Що це означає на практиці |
|---|---|
| Явне узгодження | хтось призначений знає, що міграція починається і навіщо |
| Backup | у вас є знімок бази та поточної конфігурації до змін |
|
відкат описано до старту, а не після першої аварії |
| Owner review | відомий власник модуля або середовища, який підтверджує крок |
| Test data first | спочатку перевіряємо на тестових даних або staging, не на production |
| Production boundary | жодного «швиденько перевіримо на бойовому» без окремого рішення |
Тут корисно думати не категоріями бюрократії, а категоріями ціни помилки. Додали поле — терпимо. Перезаписали кривим скриптом дати білінгу в підписок — і вже неважливо, наскільки гарний ваш pull request.
Саме тому навіть маленький pilot живе в окремій гілці та з окремими backup-артефактами:
mkdir -p backups
pg_dump -Fc cashflow_dashboard > backups/cashflow-pre-utc.dump # знімок бази до міграції
cp config/application.yml backups/application-pre-boot3.yml # знімок поточних налаштувань
git switch -c migration/reports-utc-pilot # окрема гілка під pilot
./gradlew test --tests 'reports.*' # базова перевірка до змін
Цей набір команд не робить нічого магічного — він просто залишає вам шлях назад. І в міграціях це вже дуже багато.
Важливе й ще одне правило: якщо rollback або backup «не потрібен» — не пропускайте його мовчки, а заповніть як none, because ...: «data rollback: none, бо дані не змінюються». Порожнеча надто часто означає не «не потрібно», а «забули подумати».
3. Схема expand → backfill → switch → contract
Коли розмова доходить до міграції даних, у новачків часто виникає бажання або «одразу все переписати», або безкінечно відкладати. Практика любить середину — схему expand → backfill → switch → contract, де система якийсь час живе і зі старим, і з новим уявленням даних.
Візьмімо реалістичний сценарій CashFlow Dashboard. У legacy частина подій лежить у legacy_ts, а зона — окремо в legacy_timezone. Мета — єдине поле normalized_ts_utc, щоб звіти та MRR не залежали від локальних поясів і трактувань старого формату.
Логіка фаз виглядає так:
| Фаза | Що відбувається | Наскільки реальним є відкат |
|---|---|---|
| Expand | додаємо нове поле, нічого не ламаючи в старому читанні | відкат простий |
| Backfill | переносимо старі дані в новий формат пакетами | відкат ще простий |
| Switch reads | читання переводимо на новий стовпець через прапор | відкат дуже дешевий |
| Switch writes | нові записи пишемо і в старе, і в нове уявлення | відкат усе ще можливий |
| Contract | видаляємо старе поле після стабілізації | відкат дорогий або неможливий без backup |
Перший крок — не «перемкнути все на нове», а розширити схему:
ALTER TABLE subscription_payments
ADD COLUMN normalized_ts_utc TIMESTAMPTZ; -- новий стовпець під UTC
UPDATE subscription_payments
SET normalized_ts_utc = legacy_ts AT TIME ZONE COALESCE(legacy_timezone, 'UTC')
WHERE normalized_ts_utc IS NULL; -- первинний backfill старих записів
CREATE INDEX idx_sub_payments_utc
ON subscription_payments(normalized_ts_utc); -- щоб читання не стало повільнішим
Далі починається найцікавіша частина. Старе поле не вирізаємо одразу: нове заповнюється та перевіряється, застосунок живе за старими правилами. Потім читання переводять на новий стовпець через прапор — помітили розбіжність, вимкнули прапор, повернулися до старої логіки.
Наприклад, у конфігурації pilot це може виглядати так:
cashflow:
reports:
use-utc-column: false # спочатку читаємо старе поле
migration:
dual-write-utc: false # поки не пишемо одночасно у два формати
Пізніше перемикаєте use-utc-column: true, потім вмикаєте подвійну запис. І лише після стабілізації, коли validation report підтверджує feature parity, а моніторинг мовчить, думають про contract — видалення стовпця.
І ось тут у команди зазвичай виникає спокуса: «раз усе працює, давайте одразу видалимо legacy». Не треба. Видалення — не нагорода за поведінку, а окремий ризикований крок. Хочеться негайно — встаньте, налийте чаю і дайте бажанню пройти. Старе поле нікого не вб’є за одну-дві ітерації. А ранній DROP COLUMN вбиває вихідні.
4. Міграція конфігурації
Конфігурація підступна тим, що її помилки рідко виглядають драматично: застосунок стартує, відповідає на health-check, проходить частину smoke-перевірок — і поводиться не так, як ви думаєте. Розбирайте її як окремий мініпроєкт, а не додаток до коміту.
У CashFlow Dashboard legacy-налаштування легко розповзаються по проєкту: application.yml, змінні середовища, старий прапор у CI, staging-конфіг, секретний шлях до зовнішнього сервісу. Після переходу на нову версію Boot частина ключів змінюється, частина застаріває, частина читається інакше.
Наприклад, замість розрізнених REPORT_TZ і USE_LEGACY_TZ розумніше зібрати одну точку правди:
cashflow:
reports:
timezone-source: utc-column # тепер звіти читають новий стовпець
default-zone: UTC # єдина часова зона за замовчуванням
management:
endpoints:
web:
exposure:
include: health,info,prometheus # перевіряємо після оновлення Boot
Головний принцип тут той самий, що й у даних: не стрибати різко. На один реліз тримають і старий ключ, і новий: застосунок шукає cashflow.reports.default-zone, немає його — тимчасово підхоплює legacy-ключ. Не красиво, зате практичний страховий варіант.
Є ще один нюанс: конфігурація — це не лише прикладні параметри, а й операційні: шляхи до логів, увімкнені endpoint’и, прапори dual-write, параметри планувальника, налаштування експортів, назви секретів. Не плутайте «задокументувати конфігурацію» і «розкрити секрети»: у POST_MIGRATION_NOTES.md вказують імена змінних, джерела та поведінку — не реальні токени й паролі.
Простіше всього думати так: компілятор бачить код, тести бачать поведінку, а конфігурацію в повному обсязі не бачить ніхто, крім вас і середовища. Тому inventory налаштувань, owner review і dry-run на staging тут означають не менше за тести в кодовій міграції.
5. Документація після міграції
Міграція закінчується не тоді, коли гілка злилася і CI зелений, а коли наступний інженер сам у всьому розбереться. Саме тому post-migration documentation — не бонус, а остання обов’язкова частина роботи.
Корисно бачити весь пакет артефактів як один ланцюжок:
RISK_MAP.md
↓
CHARACTERIZATION_TESTS.md
↓
COMPATIBILITY_MATRIX.md
↓
MIGRATION_PLAN.md → ROLLBACK.md → MIGRATION_VALIDATION_REPORT.md
↓
POST_MIGRATION_NOTES.md + оновлений runbook/операційна інструкція
POST_MIGRATION_NOTES.md не має повторювати MIGRATION_VALIDATION_REPORT.md рядок за рядком — його завдання інше: дати короткий, зрозумілий людині контекст. Зазвичай вистачає шести секцій:
| Секція | Навіщо потрібна |
|---|---|
| Що змінилося | щоб через місяць не гадати, що саме мігрували |
| Як запускати | щоб локальний і staging-запуск не залежали від пам’яті автора |
| Як перевіряли | щоб validation evidence було пов’язано з зрозумілим сценарієм |
| Known issues | щоб команда не сприймала обмеження за нові баги |
| Rollback path | щоб відкат шукали в одному місці, а не по чату |
| Owners і follow-up cleanup | щоб було ясно, хто спостерігає і що ще залишилося доробити |
Шаблон початку такого документа може бути дуже простим:
# POST_MIGRATION_NOTES.md
Власник: @dashboard-tech-lead
Дата: 2026-05-24
Посилання:
- MIGRATION_PLAN.md
- ROLLBACK.md
- MIGRATION_VALIDATION_REPORT.md
## Що змінилося
## Як запускати та перевіряти
## Known issues і наступний cleanup
Зверніть увагу: тут немає спроби знову переказати весь validation report. Документ радше працює як навігаційна карта: ось що змінили, ось як жити далі, ось де докази, ось хто відповідає.
Окремо корисно оновити операційну інструкцію сервісу. Не обов’язково створювати новий файл, якщо в репозиторії вже є свій runbook або розділ у README для експлуатації. Але після міграції там мають зʼявитися щонайменше чотири речі: які метрики дивитися, який алерт вважати критичним, який прапор або налаштування використовується для швидкого відкату і хто чергує на цій ділянці. Це особливо важливо для конфігураційних міграцій: вони люблять ламатися не в момент релізу, а через кілька годин, коли вмикається реальне середовище і справжнє навантаження.
Гарний міграційний пакет залишає після себе дуже спокійне відчуття. Через пів року нова людина відкриває POST_MIGRATION_NOTES.md, бачить, що у звітах нормалізували час в UTC, читання перевели на normalized_ts_utc, dual-write вже вимкнено, перевірка описана в MIGRATION_VALIDATION_REPORT.md, відкат лежить у ROLLBACK.md, а за ділянку відповідає конкретний owner. Якщо такого відчуття немає і документ нагадує археологічну знахідку з написом «ну ми тут щось мігрували, здається, стало краще», значить робота ще не завершена.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ