1. Вы мигрируете не строку версии, а сеть зависимостей
Когда разработчик впервые слышит слова dependency graph, в голове иногда рисуется монструозная схема из сотни коробочек и стрелок, от которой хочется закрыть ноутбук и уйти выращивать помидоры. На практике всё гораздо прозаичнее. Граф нужен для одного: перестать гадать, что вы обновляете — одну библиотеку, цепочку библиотек или сборочный инструмент, от которого зависит всё остальное.
На примере CashFlow Dashboard это особенно заметно. Кажется, вы хотите «просто обновить Spring Boot». Но Boot не живёт в вакууме: он тянет Spring Framework, security-слой, Jackson, валидацию, часть тестовой инфраструктуры, конфигурационные ключи, иногда — поведение Gradle-плагина. Обновите одну строку версии в build.gradle, остальное сочтёте мелочью — и это уже не миграция, а техно-лотерея.
Здесь полезно различать два артефакта:
| Артефакт | На какой вопрос отвечает |
|---|---|
| Dependency graph | Кто от кого зависит и через какую цепочку это приходит в проект |
| Compatibility matrix | Что эта зависимость означает для миграции: безопасно ли обновлять, что сломается, нужен ли промежуточный шаг |
Direct dependencies — это то, что вы явно написали в build.gradle. Transitive dependencies — это то, что пришло «в нагрузку». Если прямые зависимости — это гости, которых вы звали на вечеринку, то транзитивные — друзья гостей, которые пришли вместе с ними, а потом ещё привели своих знакомых. И обычно именно они создают самые интересные разговоры на кухне.
Например, прямые зависимости CashFlow Dashboard могут выглядеть вполне невинно:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.postgresql:postgresql'
}
Но если посмотреть чуть глубже, вы увидите уже цепочку:
+--- org.springframework.boot:spring-boot-starter-web -> 2.7.18
| \--- com.fasterxml.jackson.core:jackson-databind -> 2.13.x
+--- org.springframework.boot:spring-boot-starter-security -> 2.7.18
| \--- org.springframework.security:spring-security-config -> 5.7.x
\--- org.postgresql:postgresql -> 42.5.x
Вот здесь и начинается взрослая инженерия. Миграция ломается не на слове web starter как таковом, а на цепочке совместимости между Java, Gradle, Boot, Security, Jackson, JPA и вашим собственным кодом. Поэтому первый полезный вывод этой лекции звучит скучно, но очень практично: вы мигрируете не строку версии, а сеть зависимостей.
2. Сначала evidence, потом интерпретация
Когда граф уже получен, возникает второй вопрос: как его вообще читать, если вы не хотите провести вечер, глядя на тысячу строк вывода? Здесь помогает простое правило: не всё нужно анализировать одинаково глубоко. Сначала отделите важные узлы от фонового шума.
Для миграции CashFlow Dashboard я бы мысленно делил зависимости на три слоя. Платформенный: Java, Spring Boot, Gradle, Spring Security, JPA, драйвер БД. Инфраструктурный: JSON, логирование, миграции схемы, сериализация, интеграционные клиенты. Тестовый и служебный: JUnit, Mockito, test starters, плагины проверки. Новички часто смотрят только на runtime, забыв, что тесты и сборка тоже должны пережить миграцию, — а потом приложение «почти соберётся», а CI ляжет лицом вниз.
Чтобы не читать граф в сыром виде, полезно сохранять evidence отдельно: заведите каталог evidence/ и складывайте туда вывод зависимостей. Тогда Claude Code, вы и любой reviewer смотрят в один файл, а не пересказывают друг другу ощущения.
./gradlew dependencies > evidence/dependencies.txt
./gradlew dependencyInsight --dependency spring-security-config > evidence/security.txt
Ценна тут не команда, а привычка: сначала зафиксировать evidence, потом интерпретировать. С Claude Code это окупается сразу — вы даёте ему конкретный evidence/dependencies.txt, а не «ну там вроде много зависимостей, посмотри».
Ещё один важный момент для начинающих: не каждая строка графа обязана стать строкой матрицы — в матрицу попадает только та, что реально влияет на миграционное решение. Условный commons-lang3 живёт в проекте без приключений. А spring-security-config, jakarta.persistence, hibernate-core, postgresql driver, Gradle wrapper и Java toolchain почти наверняка заслуживают отдельной строки.
3. Sequence constraints — часть совместимости
Очень соблазнительно думать, что compatibility matrix — это просто список: компонент, версия, статус, риск. Но без зафиксированного порядка вы получите красивую таблицу, по которой всё равно нельзя безопасно двигаться. В миграции порядок важен почти как сами версии, иногда важнее: технически совместимые компоненты могут требовать строгой последовательности шагов.
На CashFlow Dashboard это видно особенно хорошо. Spring Boot 3.x требует более новую Java, новый Gradle wrapper — другой toolchain, а переход с javax.* на jakarta.* бессмысленно вносить, пока сборка живёт в старом окружении и не может это проверить. «Что обновлять» и «в каком порядке» — одна задача на двух уровнях.
Небольшая схема помогает это увидеть:
flowchart LR
A[Migration Current State] --> B[Dependency graph]
C[Official docs / release notes] --> D[Status and constraints]
B --> D
D --> E[COMPATIBILITY_MATRIX.md]
Внутри Status and constraints как раз живут sequence constraints. Например, у вас может быть такая логика:
| Компонент | Current | Target | Важное ограничение порядка |
|---|---|---|---|
| Java | |
|
Должна быть обновлена до перехода на Boot 3.x |
| Gradle | |
|
Нужен промежуточный шаг до нового Boot plugin |
| Spring Boot | |
|
Идёт после Java и обновления части build-окружения |
Это не значит, что для каждого проекта порядок будет одинаковым, но для каждого проекта он должен быть явным. Если sequence constraint не записан, он всё равно существует — просто в голове автора. А всё, что живёт только в голове автора, в миграции обычно потом становится багом, который «почему-то никто не ожидал».
В матрице sequence constraint можно хранить прямо в строке компонента. Коротко и практично:
## Компонент
Gradle
## Текущая версия
7.6.4
## Целевая версия
8.x
## Статус совместимости
requires intermediate version
## Ограничение последовательности
Обновить до перехода на Boot 3.x и проверить toolchain под Java 21
Такая запись кажется избыточной ровно до того момента, пока кто-то не решит «сразу попробовать Boot 3, а Gradle потом». После этого избыточность резко превращается в спасательный круг.
4. Словарь статусов совместимости
Самая частая проблема плохих migration-артефактов — расплывчатый язык. «Надо проверить», «вроде совместимо», «может потребоваться адаптация», «посмотрим по ходу». Это всё звучит вежливо, но не помогает принимать инженерные решения. Compatibility matrix хороша только тогда, когда в ней есть фиксированный словарь статусов. Иначе у каждого автора будет свой диалект, а у reviewer'а — головная боль.
И здесь полезно помнить разницу между двумя слоями. confirmed / assumption / unknown живут в research notes и показывают, насколько мы уверены в выводе. Матрица отвечает на другой вопрос: что с этим делать в плане миграции. Поэтому незакрытый unknown не становится магически решением — обычно он приезжает сюда как needs manual validation, а иногда доживает до Open risks в MIGRATION_PLAN.md.
Для этого уровня полезно держаться одного набора статусов:
| Статус | Что означает на практике |
|---|---|
|
Обновление выглядит прямым и не требует заметных кодовых изменений |
|
Нельзя прыгнуть сразу; нужен промежуточный шаг |
|
Сейчас переход заблокирован несовместимостью или отсутствием условий |
|
Компонент проще заменить, чем тянуть дальше |
|
Код проекта должен быть адаптирован под новую версию |
|
Потребуются изменения конфигурации, пропертей, build-настроек |
|
По коду и docs нельзя честно подтвердить совместимость без ручной проверки |
Для начинающих особенно полезен последний статус. Он очень дисциплинирует. Вместо того чтобы притворяться уверенным, когда evidence не хватает, вы честно пишете needs manual validation. Это не слабость, а признак нормальной инженерной работы. Слабость — написать safe update, потому что так спокойнее смотрится таблица.
На практике один компонент иногда требует двух отметок сразу. Например, Spring Boot 2.7 → 3.x для CashFlow Dashboard почти наверняка будет означать и needs code changes, и needs config changes. В таком случае лучше выбрать один основной статус, а второй явно описать в Required action. Но если вашей команде удобнее писать двойной статус, вроде needs code changes + needs config changes, это тоже допустимо — главное, чтобы правило было единым по всей матрице.
Вот хороший пример строки, которая не прячется за туманом формулировок:
## Компонент
Spring Boot
## Текущая версия
2.7.18
## Целевая версия
3.x
## Статус совместимости
needs code changes + needs config changes
По такой строке уже понятно, что это не «обновили и забыли». А дальше reviewer идёт в Required action и Evidence, где видит уже конкретику.
5. Читабельный COMPATIBILITY_MATRIX.md
Хорошая матрица совместимости — это не огромная Excel-таблица из ада и не роман в двенадцати томах. Это повторяемый, читаемый артефакт, где у каждой строки один и тот же каркас. Важна не форма ради формы, а то, что по ней легко пройти глазами и быстро увидеть: где безопасно, где риск, где blocker, где недостаточно evidence.
Для CashFlow Dashboard я бы рекомендовал такой шаблон строки:
## Компонент
...
## Текущая версия
...
## Целевая версия
...
## Статус совместимости
...
## Риск
...
## Требуемое действие
...
## Ограничение последовательности
...
## Доказательства
...
Обратите внимание на поле Evidence. Именно оно отличает матрицу от набора умных мыслей. В Evidence должны лежать ссылки на реальные источники: build.gradle, gradle-wrapper.properties, вывод dependencies, конкретный класс в коде, раздел migration guide, release notes. Если evidence нет, у вас не строка матрицы, а заготовка для обсуждения.
Ниже — три коротких примера, которые хорошо показывают разницу между типами строк.
## Компонент
Spring Boot
## Текущая версия
2.7.18
## Целевая версия
3.x
## Статус совместимости
needs code changes
## Доказательства
build.gradle; migration guide: Jakarta EE 9
Здесь строка сразу говорит: обновление упрётся не только в версию, но и в код.
## Компонент
Gradle
## Текущая версия
7.6.4
## Целевая версия
8.x
## Статус совместимости
requires intermediate version
## Доказательства
gradle-wrapper.properties; Gradle release notes
А здесь уже видно, что нельзя перепрыгнуть шаг «потому что хочется быстрее».
## Компонент
Custom Hibernate UserType
## Текущая версия
legacy implementation
## Целевая версия
jakarta-compatible implementation
## Статус совместимости
needs manual validation
## Доказательства
src/main/java/.../MoneyUserType.java; docs insufficient
Это отличный пример честной строки. Код есть, проблема видна, но официальный источник не даёт гарантии, что всё переживёт миграцию без ручной проверки. Значит, так и пишем.
Ещё одна полезная привычка: включать в матрицу не только библиотеки, но и системные компоненты среды. Java — это компонент. Gradle wrapper — компонент. Spring Boot plugin — компонент. PostgreSQL driver — компонент. Иногда даже тестовый стек — отдельный компонент. Потому что миграция ломается не только в runtime-коде, а в любой точке, где сходятся версия, toolchain и ваша реальная кодовая база.
6. Claude Code как аналитик, а не гадалка
Когда у вас уже есть current-state inventory, evidence-файлы и заметки из official docs, Claude Code становится очень полезным помощником. Но именно помощником-аналитиком, а не автоматическим мигратором. Его задача здесь — читать фиксированные входы, помогать группировать зависимости, предлагать строки матрицы и показывать, где evidence не хватает. Не больше. Как только он начинает в духе «ну обычно тут всё совместимо», пора возвращать разговор на землю.
Хороший запрос к Claude Code в этой теме звучит примерно так:
Построй строки compatibility matrix для CashFlow Dashboard.
Используй только:
- build.gradle
- gradle-wrapper.properties
- evidence/dependencies.txt
- migration research notes
Для каждой строки верни:
component, current, target, status, risk, sequence constraint, evidence.
Если подтверждения не хватает, ставь needs manual validation.
Ничего не обновляй и не запускай.
Заметьте, здесь есть три важных ограничения. Во-первых, вы задаёте фиксированные входы. Во-вторых, вы задаёте фиксированный словарь полей. В-третьих, вы прямо запрещаете execution. Это сильно снижает шанс, что Claude уйдёт в жанр «сейчас я вам ещё и полпроекта обновлю, раз уж открыл терминал».
При этом финальное решение по каждой строке всё равно остаётся за вами. Claude может помочь быстро заметить, что spring-boot-starter-security тянет старую ветку security-конфигурации, или что кастомный MoneyUserType не имеет явной совместимости с jakarta-миром. Но статусы blocked, needs replacement и даже safe update должен утверждать человек, который прочитал evidence и понимает риск для системы.
Полезная практическая проверка очень проста. Откройте любую строку своей матрицы и попробуйте вслух ответить на три вопроса: что это за компонент, почему у него такой статус и на какой источник вы опираетесь. Если на одном из вопросов вы начинаете говорить что-то вроде «ну это Claude предположил», строка ещё не готова. А если каждая строка читается так же уверенно, как комментарий к обычному PR, значит COMPATIBILITY_MATRIX.md действительно выполняет свою работу: показывает не только список зависимостей, а реальную карту риска и совместимости для миграции CashFlow Dashboard.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ