Создать Spring-сервис просто: существует масса статей и отличная документация. Однако среди всего этого многообразия материалов зачастую сложно разобраться, какой именно набор технологий лучше выбрать и каким образом эти технологии должным образом интегрировать друг с другом. После перехода на новые версии библиотек многое начинает функционировать иначе, появляются совершенно другие подходы. В данной статье я хочу продемонстрировать один из возможных способов разработки микросервиса в 2026 году, а также рассмотреть несколько инструментов автоматической генерации кода: OpenApiGenerator, JooqCodegen, GigaChat, Liquibase — и объяснить, как они работают вместе в рамках единого проекта.
Точкой отсчета для построения микросервиса сделаем описание REST API. Допустим, в нашем фантастическом мире аналитик заранее подготовил спецификацию API с помощью формата OpenAPI, хотя подобное встречается и в реальной практике.
Схема процесса создания микросервиса

Кроме API сервиса, мы еще имеем нефункциональные требования, которые принимаются в команде. Одной из целей данной статьи показать определенный набор, формирующий стек разработки: JDK 21, Spring, WebFlux, Postgres, Liquibase, JOOQ. В данной статье не рассматривается преимущество данного стека по сравнению с hibernate, rust, vert.x, micronaut и др. компоненты, которые могут быть использованы для реализации rest API.
Таким образом, статья постулирует, что данный набор технологий может быть успешно совместно использован, если нет других требований.
Создание описания rest‑api
Для разработки openAPI REST-интерфейса аналитику рекомендуется использовать специализированный инструмент swagger-editor. Существует онлайн-ресурс для работы с данным редактором:
Также возможно локальное развертывание сервиса swagger-editor через Docker-контейнер для обеспечения конфиденциальности разрабатываемых файлов:
https://hub.docker.com/r/swaggerapi/swagger-editor
На выходе мы получим файл, содержащий описание планируемых REST API-сервисов.
openapi: 3.0.0
info:
description: |
This is simple client API
version: "1.0.0"
title: User Service
contact:
email: vasiliev.maxim@gmail.com
servers:
- description: VM User Service
url: http://vm-user-service.maximserver/
tags:
- name: user
description: Operations about user
paths:
/user/{userId}:
parameters:
- name: userId
in: path
description: ID of user
required: true
schema:
type: integer
format: int64
post:
tags:
- user
summary: Create user
description: If user is not found, create new user
operationId: createUser
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateUser'
examples:
sample-user:
summary: Example
value:
username: johndoe589
firstName: John
lastName: Doe
email: bestjohn@doe.com
phone: '+71002003040'
description: Created user object
required: true
responses:
'204':
description: User Created
'409':
description: User already exists
get:
tags:
- user
description: Returns a user based on a single ID, if the user does not have access to the user
operationId: find user by id
responses:
'200':
description: user response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: user not found
delete:
tags:
- user
description: deletes a single user based on the ID supplied
operationId: deleteUser
responses:
'204':
description: user deleted
'404':
description: user not found
put:
tags:
- user
description: Update user with User ID supplied
operationId: updateUser
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateUser'
examples:
sample-user:
summary: Example
value:
firstName: Julie
lastName: Doe
email: bestjohn@doe.com
phone: '+71004242424'
responses:
'200':
description: User updated
'404':
description: User not found
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
User:
type: object
properties:
id:
type: integer
format: int64
example: 123
username:
type: string
maxLength: 256
example: johndoe589
firstName:
type: string
example: John
lastName:
type: string
example: Doe
avatar:
type: string
example: https://example.com/avatar.jpg
sex:
type: number
example: 1
verified:
type: boolean
example: false
birthday:
type: string
pattern: '^(0[1-9]|[1-2][0-9]|3[0-1])\\.(0[1-9]|1[0-2])\\.\\d{4}$'
example: 19.03.1982
email:
type: string
format: email
example: bestjohn@doe.com
phone:
type: string
format: phone
example: '+71002003040'
UpdateUser:
type: object
properties:
username:
type: string
maxLength: 256
example: johndoe589
firstName:
type: string
example: John
lastName:
type: string
example: Doe
avatar:
type: string
example: https://example.com/avatar.jpg
sex:
type: number
example: 1
verified:
type: boolean
example: false
birthday:
type: string
pattern: '^(0[1-9]|[1-2][0-9]|3[0-1])\\.(0[1-9]|1[0-2])\\.\\d{4}$'
example: 19.03.1982
email:
type: string
format: email
example: bestjohn@doe.com
phone:
type: string
format: phone
example: '+71002003040'
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
В файле представлено API post, get, update, delete для данных пользователя. Причем для post, update используется структура UpdateUser, а для get возвращается User. Данная бизнес логика и использование идентификаторов, а также наличие определенных полей безотносительно задачи нашего рассмотрения и служит примером использования генераторов и их взаимодействия.
Создание java проекта
Создание java проекта или gradle-проекта в нашем случае и несколько файлов кода.
Для создания проекта воспользуемся генератором проекта spring initializer.
Мы решили, что хотим использовать JDK 21 и реактивный стек, кроме того для доступа к БД будем использовать JOOQ, как будто у нас сложные запросы с фильтрами и соединениями таблиц.
Для упрощения генерации POJO добавим Lombok.
Spring Reactive Web, реактивный контроллер на основе Netty сервера.
JOOQ Access Layer - для доступа к БД. Хорошо, что JOOQ умеет использовать реактивный драйвер. (Альтернативно могли бы использовать Spring Data R2DBC, это описано в документации Spring, для Reactor, но для нас важен JOOQ, как более удобный инструмент для составления сложных запросов)
PostgresSQL Driver – для подключения к БД Postgres. Подмечаем, что он умеет делать R2DBC.
Liquibase Migration – для создания структуры БД, тут нам не нужна реактивность.
Spring Boot Actuator – для поддержки работы в кластере
Prometheus – для мониторинга сервиса
Testcontainers - для создания автотестов в сервисе с использованием БД Postgres, расположенной в контейнере
Еще мы хотим
swagger-uiдля тестировщиков, но его нет в initalizer, поэтому добавим его сами позднее.

Мы нажимаем кнопку Generate и скачиваем zip с проектом, который размещаем в локальной папке, для репозиториев.
В созданный проект добавляем ранее созданный openApi файл:
src/main/resources/specification/openapi-users.yaml
Создание git репозитория
Создаем git репозиторий и сохраняем в нем новый проект vm-user-service
Проект можно посмотреть здесь:
https://github.com/maxmiracle/vm-user-service
Генерируем rest api из описания openAPI.yaml
Добавляем плагин id 'org.openapi.generator' version '7.14.0' Настраиваем генерацию файлов с помощью gradle task, которая будет запускаться перед compileJava.
compileJava.dependsOn "openApiGenerate"
openApiGenerate {
Directory outputGenerated = layout.buildDirectory.dir("generated").get()
generatorName.set("spring")
inputSpec.set("$rootDir/src/main/resources/specification/openapi-users.yaml")
outputDir.set("$outputGenerated/openapi")
apiPackage.set("ru.maximserver.vmuserservice.api")
modelPackage.set("ru.maximserver.vmuserservice.model")
configOptions.set([library : "spring-boot",
useOptional : "true",
openApiNullable : "false",
interfaceOnly : "true",
generatedConstructorWithRequiredArgs: "false",
useTags : "true",
basePackage : "ru.maximserver.vmuserservice",
useJakartaEe : "true",
reactive : "true"])
}
Теперь при запуске сборки build или отдельного запуска task openApiGenerate будут созадваться исходные коды в папке build/generated/openapi. Добавим эту папку в список исходников:
sourceSets {
main {
java {
srcDirs += ["$project.buildDir/generated/openapi/src/main/java"]
}
}
}
Запуск openApiGenerate выполняется перед этапом компиляции. Результат будет формировать временные файлы. Таким образом, классы DTO, такие как UpdateUser, User будут формироваться на лету из openAPI файла. В рассматриваемом проекте эти файлы не хранятся в git. Поэтому, если вы загрузите git-репозиторий, то запустите gradle task 'compileJava', чтобы увидеть сгенерированный код.
Сделаем заготовки файлов с помощью ИИ, чтобы меньше набирать кода.
Воспользуемся сервисом GIGA CODE.
Скормим AI следующий promt + openapi-users.yaml
Сгенерируй, используя описание REST API из файла @openapi-users.yaml:
- Интерфейс rest контроллера
- Классы dto объектов rest
- Реализация rest контроллера
- Скрипт Liquibase для Data Layer этого контроллера
- Data Layer с использованием JOOQ
Особенности реализации:
- Классы разместить в пакете ru.maximserver.vmuserservice.
- Для dto объектов использовать Lombok annotations.
- Интерфейс аннатационного rest контроллера необходимо сделать с использованием WebFlux с добавлением аннотаций для swagger io.swagger.v3.oas.annotations.*
- Использованы **Swagger 3 (OpenAPI 3)** аннотации: `@Operation`, `@ApiResponse`, `@Parameter`, `@Tag`, `@Schema`, `@Content`, `@ExampleObject`.
- Все примеры из OpenAPI (например, `sample-user`) перенесены в аннотации.
- Поддержка **WebFlux** через `Mono<ResponseEntity<T>>`.
- DTO-классы содержат `@Schema` с описаниями и примерами — это улучшает документацию в Swagger UI.
- Поля с примерами и ограничениями (например, `maxLength`, `pattern`) аннотированы соответствующим образом.
- Добавить Конфигурацию OpenAPI (в `application.yml`)
- Добавить валидацию (`@Valid`, `@NotBlank`, `@Email`, `@Pattern`)
Однако, большинство полученных файлов будет использовано с редакцией. Важно понимать и помнить, что ИИ может пропустить важный аспект реализации.
На основе сгенерированного gigacode создаем liquibase скрипт.
Скрипт размещается в ресурсах проекта в папке src/main/resources/db/changelog Скрипт состоит из основного файла, который указывает, какие файлы входят в состав главного. Библиотека liquibase позволяет создавать структуру БД и управлять изменениями этой структуры. По сути это DDL, который может быть представлен xml, yaml или непосредственно sql скриптами. Также liquibase используется в тестовых сценариях вместе с библиотекой testcontainers.
Теперь тест сгенерированный spring initializer, который запускает приложение, отрабатывает без ошибок, так как liquibase теперь генерирует БД и сервис может к ней подключиться.
На основе сгенерированного скрипта Liquibase генерируем JOOQ metadata classes.
Подключаем плагин gradle plugin
plugins {
...
id 'org.jooq.jooq-codegen-gradle' version '3.19.17'
Добавляем dependencies
jooqCodegen "org.jooq:jooq-meta-extensions-liquibase:3.19.17"
Добавляем настройки gradle task, ставим зависимость для компиляции (compileJava) от этого нового task.
compileJava.dependsOn "jooqCodegen"
jooq {
configuration {
generator {
database {
name = "org.jooq.meta.extensions.liquibase.LiquibaseDatabase"
properties {
property {
key = "rootPath"
value = "$rootDir/src/main/resources"
}
property {
key = "scripts"
value = "/db/changelog/db.changelog-master.yaml"
}
property {
key = "includeLiquibaseTables"
value = false
}
property {
key = "liquibaseSchemaName"
value = "public"
}
property {
key = "changeLogParameters.contexts"
value = "!test"
}
}
}
target {
packageName = "ru.maximserver.vmuserservice.jooq.gen"
}
}
}
}
При запуске task jooqCodegen сгенерируется код с метаданными базы данных для библиотеки JOOQ. JOOQ также способен сгенерировать метаданные из БД, однако потребуется поднять инстанс базы данных, например, в Docker. Подобные проекты и сценарии существуют и поддерживаются некоторыми компаниями / командами / проектами. Генерация JOOQ из скриптов Liquibase выглядит изящно, но накладывает определённые ограничения. Один из возможных подходов — зафиксировать метафайлы JOOQ в репозитории проекта (Git), а не генерировать их динамически через Gradle-задачу, как демонстрировалось ранее. Сейчас перед этапом compileJava запускаются обе задачи: openApiGenerate и jooqCodegen.
Реализация rest-контроллера
Интерфейс контроллера уже сгенерирован, нужно лишь определить реализацию, в которой основным действием будет вызов сервисного слоя. Интерфейс генерируется в файле build/generated/openapi/src/main/java/ru/maximserver/vmuserservice/api/UserApi.java.
Реализуем сгенерированный OpenApiGenerator-ом интерфейс UserApi в новом классе:
package ru.maximserver.vmuserservice.controller;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import ru.maximserver.vmuserservice.api.UserApi;
import ru.maximserver.vmuserservice.model.UpdateUser;
import ru.maximserver.vmuserservice.model.User;
import ru.maximserver.vmuserservice.service.UserService;
@RestController
@RequestMapping("${openapi.user-service.base-path:/}")
@RequiredArgsConstructor
public class UserApiController implements UserApi {
private final UserService userService;
@Override
public Mono<@NonNull ResponseEntity<Void>> createUser(
final Long userId,
final Mono<@NonNull UpdateUser> updateUser,
final ServerWebExchange exchange
){
return userService.createUser(updateUser, userId)
.then(Mono.just(ResponseEntity.status(HttpStatus.CREATED).build()));
}
@Override
public Mono<@NonNull ResponseEntity<Void>> deleteUser(
final Long userId,
final ServerWebExchange exchange){
return userService.deleteUser(userId)
.thenReturn(ResponseEntity.noContent().build());
}
@Override
public Mono<@NonNull ResponseEntity<@NonNull User>> findUserById(
final Long userId,
final ServerWebExchange exchange) {
return userService.findUserById(userId).map(ResponseEntity::ok);
}
@Override
public Mono<@NonNull ResponseEntity<Void>> updateUser(
final Long userId,
final Mono<@NonNull UpdateUser> user,
final ServerWebExchange exchange
){
return userService.updateUser(userId, user)
.thenReturn(ResponseEntity.noContent().build());
}
}
По сути реализация сводится к вызову сервисного слоя.
Реализация сервисного слоя
Сервис работы с сущностью Пользователь реализуем в новом классе UserService.
package ru.maximserver.vmuserservice.service;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.DSLContext;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import ru.maximserver.vmuserservice.exception.ResourceNotFoundException;
import ru.maximserver.vmuserservice.mapper.UserMapper;
import ru.maximserver.vmuserservice.model.UpdateUser;
import ru.maximserver.vmuserservice.model.User;
import static ru.maximserver.vmuserservice.jooq.gen.tables.UserAccount.USER_ACCOUNT;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final DSLContext dslContext;
public Mono<Void> createUser(Mono<@NonNull UpdateUser> user, @NonNull final Long userId) {
return user.map(userObject -> userMapper.toUserAccountRecord(userObject, userId))
.flatMap(userAccountRecord ->
Mono.from(dslContext.insertInto(USER_ACCOUNT).set(userAccountRecord).returning()))
.doOnNext(savedResult -> log.info("Inserted Record:\n{}", savedResult))
.then();
}
public Mono<Void> deleteUser(Long userId) {
return Mono.from(dslContext.delete(USER_ACCOUNT)
.where(USER_ACCOUNT.ID.eq(userId)).returning())
.switchIfEmpty(Mono.error(userNotFound(userId)))
.doOnNext(savedResult -> log.info("Deleted Record:\n{}", savedResult))
.then();
}
public Mono<@NonNull User> findUserById(Long userId) {
return Mono.from(dslContext.selectFrom(USER_ACCOUNT)
.where(USER_ACCOUNT.ID.eq(userId)))
.switchIfEmpty(Mono.error(userNotFound(userId)))
.doOnNext(result -> log.info("Found Record:\n{}", result))
.map(userMapper::toUser);
}
public Mono<Void> updateUser(Long userId, Mono<@NonNull UpdateUser> user) {
return user.map(userObject -> userMapper.toUserAccountRecord(userObject, userId))
.flatMap(userAccountRecord -> Mono.from(dslContext.update(USER_ACCOUNT).set(userAccountRecord)
.where(USER_ACCOUNT.ID.eq(userId)).returning()))
.switchIfEmpty(Mono.error(userNotFound(userId)))
.doOnNext(savedResult -> log.info("Updated Record:\n{}", savedResult))
.then();
}
private ResourceNotFoundException userNotFound(Long userId) {
log.info("User {} not found", userId);
return new ResourceNotFoundException("User not found");
}
}
Создание маппера
При реализации сервисного слоя нам понадобился маппер, который связывает сущности базы данных JOOQ с API-сущностями, сгенерированными на предыдущих этапах.
Объявление маппера через интерфейс довольно простое. Вся работа ложится на плечи MapStruct. Препроцессор обработает аннотации и сгенерирует реализацию.
package ru.maximserver.vmuserservice.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import ru.maximserver.vmuserservice.jooq.gen.tables.records.UserAccountRecord;
import ru.maximserver.vmuserservice.model.UpdateUser;
import ru.maximserver.vmuserservice.model.User;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target="id", source="userId")
@Mapping(target=".", source="user")
UserAccountRecord toUserAccountRecord(UpdateUser user, Long userId);
User toUser(UserAccountRecord userAccountRecord);
}
Теперь наше приложение готово. За рамками описания остались конфигурации JOOQ
В случае вопросов обратитесь к репозиторию:
https://github.com/maxmiracle/vm-user-service/blob/master/src/main/java/ru/maximserver/vmuserservice/config/JooqConfig.java
Для эксплуатации сервиса необходимо доработать security сервиса.
Интеграционные тесты
Важная часть проекта — это интеграционные тесты с использованием TestContainers, реализованные как автотесты, то есть тесты, которые можно легко выполнить без трудоёмкого настройки окружения. Благодаря таким тестам при должном уровне покрытия разработчик может спокойно проводить рефакторинг, обновлять библиотеки, не опасаясь, что код может сломаться.
Приведём пример одного такого теста.
Сначала создадим базовый класс для теста, который будет запускать Docker-контейнер с PostgreSQL и конфигурировать сервис значениями, соответствующими тестовой базе данных.
package ru.maximserver.vmuserservice;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
public class BaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgreSQLContainer =
new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"))
.waitingFor(Wait.forListeningPort())
.withCommand("postgres", "-c", "max_connections=500");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.r2dbc.url", () -> postgreSQLContainer.getJdbcUrl().replaceFirst("jdbc:", "r2dbc:"));
registry.add("spring.r2dbc.username", postgreSQLContainer::getUsername);
registry.add("spring.r2dbc.password", postgreSQLContainer::getPassword);
registry.add("spring.r2dbc.properties.schema", () -> "public");
registry.add("spring.r2dbc.properties.database", postgreSQLContainer::getDatabaseName);
registry.add("spring.r2dbc.properties.host", postgreSQLContainer::getHost);
registry.add("spring.r2dbc.properties.port", postgreSQLContainer::getFirstMappedPort);
registry.add("spring.liquibase.url", postgreSQLContainer::getJdbcUrl);
registry.add("spring.liquibase.user", postgreSQLContainer::getUsername);
registry.add("spring.liquibase.password", postgreSQLContainer::getPassword);
registry.add("spring.liquibase.clear-checksums", () -> "false");
}
}
Теперь на основе этого класса создадим тест:
package ru.maximserver.vmuserservice.controller;
import org.jooq.DSLContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import ru.maximserver.vmuserservice.BaseIntegrationTest;
import ru.maximserver.vmuserservice.model.UpdateUser;
import static org.assertj.core.api.Assertions.assertThat;
import static ru.maximserver.vmuserservice.jooq.gen.Tables.USER_ACCOUNT;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
public class UserApiControllerTest extends BaseIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private DSLContext dslContext;
@Test
public void createUser() {
UpdateUser updateUser = new UpdateUser();
updateUser.setUsername("johndoe");
updateUser.setFirstName("John");
updateUser.setLastName("Doe");
updateUser.setEmail("johndoe@mail.com");
updateUser.setPhone("89000000000");
webTestClient.post()
.uri("/user/1")
.body(Mono.just(updateUser), UpdateUser.class)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isCreated();
StepVerifier.create(dslContext.selectFrom(USER_ACCOUNT).where(USER_ACCOUNT.USERNAME.eq("johndoe")))
.assertNext(userAccountRecord -> {
assertThat(userAccountRecord).isNotNull();
assertThat(userAccountRecord.getId()).isEqualTo(1);
assertThat(userAccountRecord.getFirstName()).isEqualTo("John");
assertThat(userAccountRecord.getLastName()).isEqualTo("Doe");
assertThat(userAccountRecord.getEmail()).isEqualTo("johndoe@mail.com");
assertThat(userAccountRecord.getPhone()).isEqualTo("89000000000");
})
.expectComplete()
.verify();
}
}
Выводы
JDK21, Postgres, Spring, WebFlux, JOOQ - один из возможных стеков для реализации микросервисов.
Использование рассмотренных генераторов может значительно сэкономить время разработки.
Генераторы имеют ограничения и могут не обладать необходимыми настройками или параметрами.
Использование генераторов целесообразно для микросервисных проектов ввиду ограниченого числа факторов при создании таких проектов.
Комментарии (9)

Snaret
04.01.2026 13:42Спасибо за статью, позволю себе несколько замечаний по коду (так сказать ревью):
Mono<@NonNullVoid>- крайне спорно и по-моему бессмысленнов UserServiceотсутствует обработка ошибок. Вообще.
В целом подозреваю, что все это сделано из-за быстроты и генерации части кода OpenAPI Generator. Советую поставить openApiNullable: "true".
Есть непонятный мне конфликт:
useOptional: "true", //Использовать Optional
openApiNullable: "false", // Но ничего не может быть null?!В тесте вообще не пойму откуда
User user =newUser();Ведь если это DTO из OpenAPI то как минимум в коде вижу
UpdateUser.Да и чтоUpdateUserделает в createUser методе?)
В github вообще не нашел папки по пути ru.maximserver.vmuserservice.model указанные в сервисном слое.
Очень много загадочного в проекте на мой взгляд))
Прошу простить, что надушнил, но кто-то(например я) захочет что-то изучить по вашему коду и столкнется с множеством проблем.
maximvasilievmaxim Автор
04.01.2026 13:42Добрый день! В проекте нет пакета model - верно . Классы пакета генерируются openApiGenerator. Чтобы они появились в папке build нужно запустить сборку проекта.

maximvasilievmaxim Автор
04.01.2026 13:42Поправил тест, сделал фикс, поменял User на UpdateUser. Спасибо за замечание.
https://github.com/maxmiracle/vm-user-service/commit/d27e55ed816cf45f91e33ddac7db5352b23c1f34
Действительно, UpdateUser используется и для post (create) и для update. А User для get. Согласен, что это не очень хорошее допущение для примера.
Эти классы генерируются из OpenApi на лету. По легенде, это придумал Системный Аналитик:)
Snaret
04.01.2026 13:42Я бы вам порекомендовал в качестве дто использовать не объекты из OpenApi, а из JOOQ. Там и record'ы есть для спокойной передачи данных

LeshaRB
04.01.2026 13:42createUser параметр передается update user
createUser передается user ID, а если такой id уже занят?
maximvasilievmaxim Автор
04.01.2026 13:42С точки зрения бизнес логики это было сделано в контексте хранения пользователей авторизовавшихся через vk. vk_id не должен повторятся по требованиям. Эта бизнес логика безотносительнп темы статьи. Прошу меня простить.

Neuronix
04.01.2026 13:42Почему выбран реактивный стек? (особенно учитывая вводные данные в виде java 21 в которой уже есть virtual threads) Стильно, модно, молодежно? Тогда еще напишите, если есть конечно опыт, во что он превратится через несколько лет активной разработки разными людьми, когда там будет что то сложнее этого hello world. И как его прекрасно будет дебажить. Статья явно рассчитана на начинающих, может не стоит вот так вот сразу испытывать устойчивость их психики реактивщиной?

maximvasilievmaxim Автор
04.01.2026 13:42Добрый день!
Реактивный движок актуален, когда важно быстро обработать запрос. Из своей практики приведу несколько примеров проектов: в одном случае это был платёжный шлюз, в другом — кредитный конвейер. Конечно, проблемы с отладкой возможны, однако при грамотной организации автотестов это уже не столь критично, поскольку достигается значительное преимущество. Если заказчик внимательно следит за производительностью через JMeter и учитывает каждую миллисекунду, выбор реактивного движка становится вполне обоснованным. По virtual threads надо проводить тесты и смотреть, будет ли это сопоставимо по эффекту. Кстати, отличная тема для изучения и написания статьи!
maximvasilievmaxim Автор
Проект можно посмотреть здесь:
https://github.com/maxmiracle/vm-user-service