К микросервисам через 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 на проде, второй сервис на подходе, и мы почти завершили миграцию.
Выводы
Выбор подхода — важный шаг при перестроении системы. Какие технологии будут использоваться, как все элементы системы связываются воедино — об этом стоит договориться заранее. Мы потратили на этот этап достаточно много времени и пересмотрели разные варианты решения проблемы, но оно того стоило.
Реверс инжениринг — хороший способ описать текущую систему. Изучение текущего кода, его обработка и получение схемы позволяет описать систему и решает проблему автоматизации.
Кодогенерация упрощает жизнь и избавляет от бойлерплейта. Кодогенерация помогает решить много задач и позволяет унифицировать подход, чтобы разработчики подходили к реализации одинаково.