Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет! Я Алексей, Java-разработчик. Хочу поделиться опытом внедрения подхода Contract-First в backend.
Что такое контракт
Представим продакшн и три группы участников: мобильное приложение, бэкенд и фронтенд.
Кружки на картинке — множество инстансов одного приложения или множество мобильных устройств. Пунктиром показаны HTTP-вызовы. Предположим, два приложения на iOS и Android (зелёные кружки) обращаются к backend (фиолетовые кружки). Это и есть контракты — правила, по которым общаются два участника.
Я расскажу, как описывать контракты, проводить новые стрелочки, их согласовывать и не потеряться в потоке изменений в большой микросервисной архитектуре.
Наш backend — это 60 разработчиков и 100 микросервисов. Мы поддерживаем внутренние и внешние интеграции и катим 50+ релизов в день, большинство — с изменениями контрактов. Согласование контрактов у нас приобретает повышенный уровень сложности.
Code-First: как мы раньше управляли контрактами
До 2021 года мы использовали Code-First-подход. Описание контракта выглядело так:
@ApiModel(description = "Информация о пользователе")
public class UserInfo {
@ApiModelProperty(value = "Уникальный идентификатор пользователя", required = true)
@NotNull
@JsonProperty("id")
private final String id;
@ApiModelProperty(value = "Имя")
@JsonProperty("firstName")
private final String firstName;
...
Чтобы реализовать HTTP endpoint, мы описывали в Java-коде модели запросов и ответов. Затем помечали их аннотациями Springfox. Классы моделей, запросов и ответов паковали в JAR, а JAR отгружали в Maven Repository. Для каждого микросервиса заводили библиотеку с набором всех контрактов. Для контракта микросервиса profile была библиотека profile-api, для notifer, notifier-api и так далее.
Если одному микросервису нужно было был сходить в другой, он подключал необходимую API-библиотеку к себе в зависимости и использовали её для взаимодействия. Для тех, кто не умеет в Java, у нас был Swagger UI.
Это OpenAPI-спецификация, отрендеренная в HTML и сгенерированная по метаданным классов. Так умеет делать Springfox и Springdoc из коробки. Подход достаточно распространен, я часто встречал его в других компаниях.
Проблемы разработки с Code-First
Если масштабировать Code-First-подход на 100+ сервисов и 60 разработчиков, могут возникнуть проблемы. Давайте их разберём.
Code-First Jar Hell
Представим, что микросервис с именем profile — «коммуникабельный парень». Вот часть его дерева зависимостей на другие API-библиотеки:
+--- yoomoney:profile:2.0.0
| +--- yoomoney:notiifer-api:4.0.0
| | \--- yoomoney:command-api-engine:2.10.1 -> 2.13.1
| +--- yoomoney:cards-api:3.2.0
| | \--- yoomoney:command-api-engine:2.13.1
| +--- yoomoney:content-api:1.0.1
| | \--- yoomoney:command-api-engine:2.10.0 -> 2.13.1
| +--- yoomoney:debt-api:2.0.0
| | \--- yoomoney:command-api-engine:2.13.1
| +--- yoomoney:identifier-api:4.2.0
| | \--- yoomoney:command-api-engine:2.13.1
....
Допустим, мы выпустили новую мажорную версию библиотеки yoomoney:command-api-engine:3.0.0, подключили её в yoomoney:identifier-api:5.0.0 и хотим использовать новое identifier-api в сервисе profile.
Наше новое дерево зависимостей микросервиса profile теперь выглядит так:
+--- yoomoney:profile:2.0.0
| +--- yoomoney:notiifer-api:4.0.0
| | \--- yoomoney:command-api-engine:2.10.1 -> 3.0.0 // конфликт мажорных версий
| +--- yoomoney:cards-api:3.2.0
| | \--- yoomoney:command-api-engine:2.13.1 -> 3.0.0 // конфликт мажорных версий
| +--- yoomoney:content-api:1.0.1
| | \--- yoomoney:command-api-engine:2.10.0 -> 3.0.0 // конфликт мажорных версий
| +--- yoomoney:debt-api:2.0.0
| | \--- yoomoney:command-api-engine:2.13.1 -> 3.0.0 // конфликт мажорных версий
| +--- yoomoney:identifier-api:5.0.0
| | \--- yoomoney:command-api-engine:3.0.0
....
В одну JVM нельзя одновременно загружать несколько разных версий одного класса c совпадающими именами. Если API-библиотеки транзитивно тянут разные мажорные версии зависимостей, будут проблемы в runtime.
Хорошо, если ваш сервис упадёт ещё до запуска. Когда вы захотите подключить новую мажорную версию в API-либу, придётся пройтись по остальным API-библиотекам и сделать то же самое. Зарелизить 100 либ — не самое приятное занятие на вечер пятницы.
Изменения API-библиотек в Code-First
На ревью основное внимание уделяется Java-коду, а не контрактам. В идеале нужно проверить согласованность имён, правильность композиции и т.д. На ревью накидывают комментарии в стиле: забыл сделать builder для сущности, не поставил аннотацию. А то, что endpoint уже есть, но называется по-другому, могут пропустить.
Представьте API-библиотеку, в которой 30 endpoint'ов. Разработчик вносит правку в один endpoint или делает новый. На ревью вы не видите общей картины, 29 endpoint'ов ускользнут из поля зрения, из-за этого качество страдает.
Следующая проблема — внесение изменений мобильными разработчиками и фронтендерами. Помню тёмные времена, когда мне говорили: «У нас есть команда с информацией о пользователе, надо немного расширить её ответ, а мы нарисуем красивый UI». Мне приносили JSON-файлик или даже табличку на корпоративной Wiki с описанием endpoint'а