К микросервисам через reverse engineering и кодогенерацию
Автор: tyutyunkovРазрабатывая информационную систему с нуля, мы можем выбрать практически любой вариант технологии и архитектуры в целом, в том числе — принцип взаимодействия частей системы. Но что делать, если система уже есть и у неё довольно богатая история? Как большую энтерпрайз систему, которая развивалась в режиме монолита, разделить на микросервисы и организовать взаимодействие между ними?
Часто основная сложность заключается в том, что нужно одновременно поддерживать уже существующий код монолита и параллельно внедрять новые принципы и подходы. В статье я расскажу, как мы в Wrike, используя reverse engineering и немного кодогенерации, реализовали первые шаги по выделению отдельных микросервисов и запустили первый «почти настоящий» BFF-сервис в рамках нашего монолита.

Привет! Меня зовут Слава Тютюньков, я Backend Tech Lead в Wrike. В этой статье я хочу поговорить о том, как мы в backend-команде готовились к работе с монолитом, чем в этой задаче нам помог reverse engineering, как мы использовали кодогенерацию, с какими сложностями столкнулись в процессе и что получили в итоге.
Как система выглядит сейчас и к чему мы хотим прийти
Wrike — это SaaS-решение для совместной работы и управления проектами. Архитектура системы представляет собой распределенный монолит — одно большое веб-приложение и около сотни различных дополнительных сервисов рядом. Но несмотря на многообразие сервисов мы не можем назвать текущую архитектуру микросервисной: сервисы работают с общей базой данных, лежат в монорепозитории, большая часть логики сосредоточена в нескольких крупных модулях, разделяемых между всеми сервисами.
При этом у монолита много различных потребителей API: основной web-клиент, мобильные приложения, публичные API и интеграции.

Мы довольно продолжительное время работаем в рамках такой архитектуры, у нас неплохо выстроены процессы вокруг. Например, мы деплоимся ежедневно, обновляя при этом как часть системы, так и всю систему полностью. Но у подобной архитектуры (как и у любой другой) есть недостатки. Мы понимаем, что по мере развития и роста компании нам так или иначе станет «тесно» в рамках монолита. Поэтому мы постепенно движемся в сторону разделения монолита на микросервисы.
Мы хотим, чтобы архитектура в итоге выглядела примерно так:

Но сделать это не так просто по разным причинам:
- Продукт не стоит на месте: мы постоянно развиваем его, изменяем функциональность, добавляем новые фичи и т.д.
- Есть технические аспекты: код и модули тесно связаны.
- Проблемы обратной совместимости по API. Например, у мобильного приложения Wrike отдельный релизный цикл. Нам сложно изменить что-то в монолите и не затронуть при этом мобильное приложение.
Поэтому в первую очередь мы решили изолировать API для мобильного приложения, вынеся его в отдельный сервис — BFF. Таким образом мы сможем отделить монолит от внешнего потребителя API и создать «фасад» для монолита.

BFF позволит нам сосредоточиться на изменениях в самом монолите, его устройстве, внутренних коммуникациях и вынесении отдельных фрагментов. При этом мы сможем не бояться что-то сломать и сделать API неконсистентным.

Постепенно выделяя микросервисы, мы хотим прийти к такой архитектуре:

Готовимся к работе
Когда мы собираемся проектировать новую систему или менять существующую, подготовка — важный этап. Чтобы процесс изменения системы не усложнял жизнь всей команде, нужно договориться об огромном количестве нюансов и решений —технических, инфраструктурных и организационных. В этом тексте мы рассмотрим техническую составляющую — организация инфраструктуры заслуживает отдельной статьи.
О чем мы решили договориться заранее:
Протокол взаимодействия микросервисов. Мы выбрали REST-Like и JSON в качестве транспорта. Мы рассматривали и другие варианты вроде gRPC или RSocket, но нас устроил REST: мы пошли по «консервативному пути» — большинство разработчиков в команде умеют с ним работать, поэтому на первых этапах внедрения ребятам будет проще и удобнее.
Библиотеки. В качестве клиента мы договорились использовать Retrofit2, Jackson — в качестве библиотеки для маппинга JSON-ов.
Описание схемы. Еще одна проблема монолита — у эндпоинтов нет описания, есть только код. При работе в микросервисном мире подобное недопустимо: без описания API невозможно построить нормальное взаимодействие между микросервисами. Опираясь на предыдущий выбор (REST+JSON), логичным способом описания стал OpenAPI.
Организация процесса. Описанного API в виде схемы недостаточно, необходимо контролировать и гарантировать, что реальный интерфейс сервиса соответствует имеющейся схеме. Мы решили использовать подход Schema-First. В рамках этого подхода разработчик напрямую не может менять в коде интерфейс и настройки эндпоинта, все происходит через схему — меняется схема, меняется интерфейс сервиса. А если реализация не соответствует схеме, сервис просто не соберется и не запустится.
В качестве «основы» для микросервисов мы выбрали довольно стандартное решение — Spring Boot и Spring MVC.
Дальше нужно было выбрать первую «жертву».
Параметры, по которым мы выбирали:
- Отсутствие собственного домена.
- Узкоспециализированный API.
- Отдельный релизный цикл.
- Особые требования обратной совместимости.
В итоге мы решили создать BFF для Android-приложения:
- Мобильное приложение покрывает большой объем функциональности системы. У него есть своя специфика, но для него нельзя выделить один домен.
- Мобильное приложение разделяет общие эндпоинты с web-версией, но у него своя специфика построения интерфейса и загрузки данных. Наличие интерфейса, специализированного и оптимизированного под мобильное приложение — важный фактор, и BFF как раз может его решить.
- Монолит деплоится ежедневно, мобильное приложение не может себе такого позволить. Более того, миграция пользователей на новые версии происходит довольно медленно.
- Нам необходимо поддерживать обратную совместимость эндпоинтов продолжительное время. Сейчас с Android-командой действует соглашение о сохранении обратной совместимости как минимум полгода.
Шаг первый: создаем сервис BFF
Для начала рассмотрим часть взаимодействия от мобильного приложения к монолиту через BFF.

Мы создали пустой сервис, настроили его и запустили:

Все заработало — сервис есть, трафика пока что нет, но первый этап мы прошли.

Шаг второй: разбираемся, где брать данные
Мы строим BFF в качестве «фасада» к монолиту. В этом случае логично брать данные из монолита. Мы это делаем, используя его текущий API.
Выбираем способ получения данных. Получить данные можно несколькими способами. Первый — использовать кастомный клиент. Если клиент позволяет получать данные через REST или внутренние коммуникации с монолитом, можно использовать его. В нашей ситуации такой клиент был, но не подходил для наших задач: у нас есть публичный API, но для требований Android-приложения этого было недостаточно. В частности есть различия в модели и представлении данных: Android-приложение ориентируется на внутреннюю специфику, недоступную через публичный API.
Тогда мы решили посмотреть на монолит как на большой микросервис и попытались встроить его в общую архитектуру. Для этого нужно было создать схему и описать монолит в общих терминах.
Если в компании система уже описана какой-либо схемой, то можно использовать готовую. В нашем случае каждая команда по-своему описывает протокол взаимодействия с фронтендом, и общей схемы нет. Поэтому нам нужно было ее создать.
Создаем схему. Если схема нужна для небольшого под-домена или небольшого подмножества эндпоинтов в монолите, то можно описать ее вручную. Скорее всего, возникнет вопрос с актуализацией, но в целом это возможно. Мы не хотели вручную писать схему для 150 эндпоинтов, поэтому этот способ нам не подошел.
Еще один вариант — использовать готовое решение. Например, если эндпоинты описаны через Spring MVC, то библиотека springdoc-openapi позволяет по имеющимся аннотациям получить схему. Но мы используем кастомный web-фреймворк, поэтому такой способ нам тоже не подошел.
Тогда мы решили написать подобную библиотеку самостоятельно.
Здесь нам и пригодился реверс инжиниринг. Мы проанализировали эндпоинты: оказалось, что большая часть из них (почти все интересующие нас) выглядят похожими друг на друга.
@HandlerMetaInfo(
tags = "navigation",
path = "api/navigation_settings",
method = HttpMethod.PUT,
securitySchemas = {}
)
public class PutNavigationSettings implements SchemaHandler<Input, Output> {
protected Input parseRequest(final HttpServletRequest request) {
return new Input(
Integer.parseInt(request.getParameter("mode")),
Stream.of(request.getParameterValues("items"))
.map(NavigationItem::fromString)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
}
protected Output processRequest(final Input input) {
/// save to db
return new Output(input.getItems());
}
static class Input {
private final int mode;
private final List<NavigationItem> items;
Input(final int mode, final List<NavigationItem> items) {
this.mode = mode;
this.items = items;
}
public int getMode() {
return mode;
}
public List<NavigationItem> getItems() {
return items;
}
}
static class Output {
private final List<NavigationItem> items;
Output(final List<NavigationItem> items) {
this.items = items;
}
public List<NavigationItem> getItems() {
return items;
}
}
}
Input (в нашем случае — отдельный класс, которые описывают модель), Output (то, что мы отдаем клиенту) и некоторая мета-информация в аннотация и/или конфигах.
Эта структура легко ложится на схему OpenAPI:

Схема выглядит стандартной и несложной. Мы написали библиотеку, которая генерирует схему по структуре эндпоинтов и собрали полный список — получилось порядка 150. Затем запустили обход всех эндпоинтов и получили схему. После опубликовали схему в artifactory, чтобы переиспользовать в BFF. Также это нужно, чтобы фронтенд мог взять схему и на своей стороне получить декларативный клиент.
Получается, что мы описали схему монолита, и для BFF он теперь выглядит практически как микросервис. Большой и не очень удобный, но с ним можно работать.

Шаг третий: из схемы в код
Дальше в дело вступила кодогенерация. С помощью схемы мы получили декларативный клиент для BFF. Чтобы получать данные из монолита, мы запроцессили схему через OpenAPI Generator и добавили особенности, которые были нужны в нашем случае.
Получили по схеме все нужные модели:
@JsonPropertyOrder({
JSON_PROPERTY_MODE,
JSON_PROPERTY_ITEMS
})
public class PutNavigationSettingsDto {
static final String JSON_PROPERTY_MODE = "mode";
static final String JSON_PROPERTY_ITEMS = "items";
private final int mode;
private final List<NavigationItem> items;
@JsonCreator
private PutNavigationSettingsDto(@JsonProperty(JSON_PROPERTY_MODE) final int mode,
@JsonProperty(JSON_PROPERTY_ITEMS) final List<NavigationItem> items) {
this.mode = mode;
this.items = items;
}
@JsonGetter(JSON_PROPERTY_MODE)
public int getMode() {
return mode;
}
@JsonGetter(JSON_PROPERTY_ITEMS)
public List<NavigationItem> getItems() {
return items;
}
public static Builder builder(final int mode) {
return new Builder(mode);
}
public static final class Builder {
private final int mode;
private List<NavigationItem> items;
public Builder(final int mode) {
this.mode = mode;
}
public Builder withItems(final List<NavigationItem> items) {
this.items = items;
return this;
}
public PutNavigationSettingsDto build() {
return new PutNavigationSettingsDto(mode, items);
}
}
}
@JsonPropertyOrder({
JSON_PROPERTY_ITEMS
})
public class PutNavigationSettingsResponseDto {
static final String JSON_PROPERTY_ITEMS = "items";
private final List<NavigationItem> items;
@JsonCreator
private PutNavigationSettingsResponseDto(@JsonProperty(JSON_PROPERTY_ITEMS) final List<NavigationItem> items) {
this.items = items;
}
@JsonGetter(JSON_PROPERTY_ITEMS)
public List<NavigationItem> getItems() {
return items;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private List<NavigationItem> items;
public Builder() {
}
public Builder withItems(final List<NavigationItem> items) {
this.items = items;
return this;
}
public PutNavigationSettingsResponseDto build() {
return new PutNavigationSettingsResponseDto(items);
}
}
}
Получили декларативный Retrofit2-клиент:
public interface NavigationSettingsApi {
@Headers({
"Content-Type:application/json"
})
@PUT("navigation_settings")
Call<PutNavigationSettingsResponseDto> updateNavigationSettings(@Header("Authorization") String authToken,
@Header("account") Account accountId,
@Body PutNavigationSettingsDto input);
}
Это позволило «объявить» клиент в сервисе, использовать его для работы и не думать о том, работаем мы с монолитом или микросервисом.

Шаг четвертый: проксируем
Чтобы минимизировать трудозатраты команды мобильных разработчиков, на первом этапе мы решили максимально сохранить текущий протокол и поменять только эндпоинт — адрес, куда «ходит» мобильное приложение. BFF в нашем случае получился проксирующим с небольшим добавлением специфики монолита.
Мы описали схему BFF, переиспользуя те компоненты, которые получили на предыдущем шаге:
/navigation_settings:
put:
tags:
- navigation
requestBody:
required: true
content:
"application/json":
schema:
$ref: '#/components/schemas/NavigationSettingsPutRequest'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/NavigationSettingsPutResponse"
Дальше мы сгенерировали интерфейсы для Spring MVC. Чтобы минимизировать количество ошибок в коммуникации между микросервисами, мы следуем правилу — разработчики самостоятельно не описывают интерфейсы эндпоинтов в микросервисах. Подход Schema-First постулирует, что описывается всегда только схема, а интерфейсы генерируются автоматически. Это уменьшает риск ошибки разработчика в имплементации и гарантирует консистентное взаимодействие между микросервисами.
@Validated
public interface NavigationControllerApi {
@RequestMapping(value = "/navigation_settings",
produces = {"application/json"},
consumes = {"application/json"},
method = RequestMethod.PUT)
@PreAuthorize("#principal.accountId != null")
default ResponseEntity<WrikeResponseDto> navigationSettingsPut(@AuthenticationPrincipal final AuthInfo principal,
@Valid @RequestBody final NavigationSettingsPutRequestDto navigationSettingsPutRequestDto) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Аналогичным образом генерируем модели.
Проксируем запросы, используя Retrofit2-клиент:
@Override
public ResponseEntity<WrikeResponseDto> navigationSettingsPut(final AuthInfo principal, @Valid NavigationSettingsPutRequestDto input) {
final Response<PutNavigationSettingsResponseDto> response;
try {
final WrikeToken wrikeToken = principal.getWrikeToken();
final String authToken = wrikeToken.getBearerToken();
response = navigationSettingsApi.updateNavigationSettings(
authToken,
wrikeToken.getRequestAccountId().get(),
PutNavigationSettingsDto.builder(0)
.withItems(input.getItems())
.build()
)
.execute();
} catch (final IOException e) {
log.error("", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
if (response == null) {
log.warn("[PUT] Response is null");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
if (!response.isSuccessful()) {
return ResponseEntity.status(response.code()).build();
}
final PutNavigationSettingsResponseDto data = response.body();
return ResponseEntity.ok(
WrikeResponseDto.builder(true)
.withData(data)
.build()
);
}
Логика обработчика описывается следующим образом:
- Берем входные данные из запроса.
- Добавляем необходимые заголовки.
- Используя полученный декларативный клиент, делаем запрос к монолиту.
- Полученные данные оборачиваем и отдаем в ответ клиенту.
Мы закрыли последний этап на схеме, имплементируя взаимодействие между мобильным клиентом и монолитом через BFF.

В первой версии мы задеплоили именно такой вариант, и он заработал. Правда были небольшие проблемы с проксированием и сбором метрик, но пилотный проект как раз и нужен, чтобы собрать проблемы и исправить их.
Анализируем, что получилось
Кажется, что все хорошо, можем продолжать пилить микросервисы. Но давайте чуть внимательнее посмотрим на проксирующий код эндпоинов в BFF — тот самый код, который позволяет получать данные.
По сути это обращение к монолиту через использование общей архитектуры. При работе с микросервисом у нас будет все то же самое.
Давайте взглянем на реализацию проксирования запросов еще раз:
Pеализация navigationSettingsPut
Если посмотреть на код внимательнее, то можно увидеть, что в нем очень много специфики эндпоинтов монолита. Нам нужно передавать токен авторизации: когда запрос идет между микросервисами или от BFF в основную систему, нужно явно передавать авторизационные заголовки. Также необходимо передавать дополнительные данные (в нашем примере — ID аккаунта пользователя, чтобы корректно отработал роутинг между различными сегментами системы).
В коде довольно много бойлерплейта обработки — следствие того, что мы выбрали REST. Мы должны убедиться, что запрос ушел, вернулся с правильным статусом и только после этого можем доставать данные и с ними работать.
Если для каждого вызова внутри микросервисов использовать подобный подход, то это не сильно облегчит нам жизнь. Поэтому мы решили посмотреть, что можно оптимизировать с точки зрения кода.
Что мы хотели изменить:
- Избавиться от бойлерплейта.
- Не передавать и не заполнять общие параметры.
- Абстрагироваться от протокола и маппинга данных. Мы хотели описать микросервис как обычный бин, не завязывать интерфейсы на конкретные имплементации REST (Retrofit 2) и убрать маппинг (Jackson) из описания моделей.
- Оставить возможность работать на «низком» уровне (стриминг, более тонкая обработка статусов и т.д.).
Для решения этих проблем мы снова обратились к кодогенерации.
«Тюним» кодогенерацию
Мы разбили генерацию клиента на два слоя-этапа.
Первый слой — интерфейс микросервиса. На этом уровне генерируется только API сервиса. Никаких аннотаций, упоминаний про Retrofit или что-либо еще.
Сервисы:
public interface NavigationSettingsService {
PutNavigationSettingsOutputDto updateNavigationSettings(PutNavigationSettingsInputDto input);
}
Также поступили с моделью данных: генерируем DTO из схемы с билдерами. Таким способом мы полностью изолируем систему от имплементации.
Модели:
public class PutNavigationSettingsInputDto {
private final int mode;
private final List<NavigationItem> items;
protected PutNavigationSettingsInputDto(
final int mode,
final List<NavigationItem> items
) {
this.mode = mode;
this.items = items;
}
public static Builder builder(final int mode) {
return new Builder(mode);
}
public static final class Builder {
private final int mode;
private List<NavigationItem> items;
public Builder(final int mode) {
this.mode = mode;
}
public Builder withItems(final List<NavigationItem> items) {
this.items = items;
return this;
}
public PutNavigationSettingsInputDto build() {
return new PutNavigationSettingsInputDto(mode, items);
}
}
}
Модели не поменялась — мы только убрали аннотации.
Второй слой — имплементация «траспорта». На этом этапе генерируется декларативный Retrofit2-клиент и Jackson mixin-ы для моделей (они нужны, чтобы связать реальную модель с тем, как данные передаются по сети). В качестве дефолтной имплементации интерфейса микросервиса мы добавили вызовы Retrofit2-клиента и перенесли весь бойлерплейт обработки на этот уровень.
Retrofit2-клиент выглядит похожим на предыдущий вариант: мы только добавили дополнительную обертку для ответов методов и вынесли таким способом часть логики из сервиса.
public interface NavigationSettingsServiceGateway {
@Headers({
"Content-Type:application/json"
})
@PUT("navigation_settings")
RetrofitCall<PutNavigationSettingsOutputDto> updateNavigationSettingsMobile(@Header("Authorization") String authToken,
@Header("account") IdOfAccount accountId,
@Body PutNavigationSettingsInputDto input);
}
Retrofit2 — дополнительная «обертка», которая реализует часть логики (в частности, обработки статуса ответа). Эта обертка может быть вынесена в отдельную библиотеку (что мы и планируем сделать в будущем). Сейчас обертка генерируется по шаблону и располагается рядом с остальными классами.
RetrofitCall
Используя полученный клиент, подготавливаем стандартную реализацию сервиса. С одной стороны, мы даем разработчику возможность использовать готовый шаблон: здесь есть необходимые вызовы и обработки, чтобы оперировать только верхнеуровневыми моделями. С другой — в этом сервисе присутствуют все необходимые зависимости, чтобы реализовать соответствующий метод вручную. Это, например, может быть полезно, если необходимо специальным образом обработать ответ и/или организовать иной способ вызова удаленного сервиса.
public class NavigationSettingsServiceImpl implements NavigationSettingsService {
protected static final Logger log = LoggerFactory.getLogger(NavigationSettingsServiceImpl.class);
protected final AuthDataProvider authDataProvider;
protected final NavigationSettingsServiceGateway gateway;
public NavigationSettingsServiceImpl(final AuthDataProvider authDataProvider, final NavigationSettingsServiceGateway gateway) {
this.authDataProvider = authDataProvider;
this.gateway = gateway;
}
@Override
public PutNavigationSettingsOutputDto updateNavigationSettings(PutNavigationSettingsInputDto input) {
final AuthDataProvider.AuthData authData = authDataProvider.getAuthData();
if (log.isDebugEnabled()) {
log.debug("request 'updateNavigationSettings': userId={}, accountId={}", authData.getUserId(), authData.getAccountId());
}
return gateway.updateNavigationSettingsMobile(authData.getAuthToken(), authData.getAccountId(), input).getBody();
}
}
Mixin — специальный способ Jackson добавить описание моделей, не меняя их код.
@JsonPropertyOrder({
JSON_PROPERTY_MODE,
JSON_PROPERTY_ITEMS,
})
public abstract class PutNavigationSettingsInputDtoMixin {
static final String JSON_PROPERTY_MODE = "mode";
static final String JSON_PROPERTY_ITEMS = "items";
@JsonCreator
private PutNavigationSettingsInputDtoMixin(@JsonProperty(JSON_PROPERTY_MODE) final int mode,
@JsonProperty(JSON_PROPERTY_ITEMS) final List<NavigationItem> items) {
}
@JsonGetter(JSON_PROPERTY_MODE)
public abstract int getMode();
@JsonGetter(JSON_PROPERTY_ITEMS)
public abstract List<NavigationItem> getItems();
}
Чтобы вся схема заработала с минимальными усилиями, мы подготовили дефолтную конфигурацию Jackson, Retrofit2 и т.д. Разработчику останется только подключить конфигурацию к проекту.
MixinRegistration + конфигурация:
public class MixinRegistrationModule extends SimpleModule {
@Override
public void setupModule(final SetupContext context) {
super.setupModule(context);
/// ...
context.setMixInAnnotations(PutNavigationSettingsInputDto.class, PutNavigationSettingsInputDtoMixin.class);
context.setMixInAnnotations(PutNavigationSettingsOutputDto.class, PutNavigationSettingsOutputDtoMixin.class);
/// ...
}
}
@Configuration
public class JacksonModelMixinsConfiguration {
@Bean(name = "jacksonObjectMapperMixinCustomizer")
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder
.modulesToInstall(new MixinRegistrationModule());
}
}
Регистрация бина сервиса с использованием дефолтной реализации с Retrofit2-клиентом:
public class ApiGatewayConfiguration {
private final Retrofit retrofit;
public ApiGatewayConfiguration(final Retrofit.Builder retrofitBuilder) {
this.retrofit = retrofitBuilder
.addCallAdapterFactory(retrofitCallAdapterFactory())
.build();
}
protected CallAdapter.Factory retrofitCallAdapterFactory() {
return new RetrofitCallAdapterFactory();
}
protected <T> T createRegionApiGateway(final Class<T> gatewayClass) {
return retrofit.create(gatewayClass);
}
@Bean
public NavigationSettingsService beanNavigationSettingsService(final AuthDataProvider authDataProvider) {
return new NavigationSettingsServiceImpl(authDataProvider, createRegionApiGateway(NavigationSettingsServiceGateway.class));
}
/// other services
}
На текущий момент мы используем Retrofit2, но такой подход позволяет заменить его на другой инструмент — кастомный http-фреймворк, Spring OpenFeign или что-то другое.
Возможности Spring Framework 6
Что получилось в итоге
@Override
public ResponseEntity<WrikeResponseDto> navigationSettingsPut(final AuthInfo principal, @Valid NavigationSettingsPutRequestDto input) {
final PutNavigationSettingsOutputDto response = navigationSettingsService.updateNavigationSettingsMobile(
PutNavigationSettingsInputDto.builder(0)
.withItems(input.getItems())
.build()
);
return ResponseEntity.ok(
WrikeResponseDto.builder(true)
.withData(response)
.build()
);
}
- Убрали бойлерплейт обработки http/rest.
- Вынесли отдельно модель и интерфейс сервиса.
- Код обработчика в BFF выглядит чистым и приятно читаемым.
- Используем подход для других микросервисов.
Сейчас в команде мобильной разработки мы работаем над выделением одного из микросервисов из монолита. Для этого мы описываем схему сервиса, генерируем интерфейс по этой схеме, а в качестве имплементации подставляем локальные бины, которые у нас уже есть.
Сейчас мы учим систему работать через конкретный интерфейс. Когда будем готовы вынести базу данных и код сервиса отдельно, нам нужно будет заменить транспорт на http, и все должно заработать.
BFF на проде, второй сервис на подходе, и мы почти завершили миграцию.
Выводы
Выбор подхода — важный шаг при перестроении системы. Какие технологии будут использоваться, как все элементы системы связываются воедино — об этом стоит договориться заранее. Мы потратили на этот этап достаточно много времени и пересмотрели разные варианты решения проблемы, но оно того стоило.
Реверс инжениринг — хороший способ описать текущую систему. Изучение текущего кода, его обработка и получение схемы позволяет описать систему и решает проблему автоматизации.
Кодогенерация упрощает жизнь и избавляет от бойлерплейта. Кодогенерация помогает решить много задач и позволяет унифицировать подход, чтобы разработчики подходили к реализации одинаково.