Реактивне програмування на Spring Boot: мій досвід, приклади та розбір загальноприйнятих підходів
Привіт усім, я Андрій Микитин, Software Engineer у Sombra, більше 5 років працюю з Java. Протягом цього часу мав справу з різними бекенд-рішеннями — від простих REST API до складних high-load сервісів. Коли з’явилась задача з оптимізації продуктивності на проєкті з великим навантаженням, я почав шукати альтернативу класичному підходу. І знайшов її в реактивному програмуванні.
Сьогодні хочу розповісти про свій досвід із реактивним програмуванням у Spring Boot: як ми до нього прийшли, що пробували, і чим це все закінчилося, а також покажу на прикладах, у чому суть реактивного підходу, як він відрізняється від класичного та асинхронного, і де він справді приносить користь.
Чому я взагалі почав дивитися в бік реактиву
Класичний підхід із блокуючими потоками добре працює, поки система не росте. Але щойно кількість запитів зростає — все починає «буксувати»: потоки блокуються, ресурси зжираються, і жодне масштабування вже не допомагає.
Один із проєктів мав саме такі вимоги:
- багато паралельних запитів;
- хмарна інфраструктура, де кожен додатковий ресурс — це $$$;
- відсутність «прав на помилку» у продуктивності.
Потрібно було вирішити, як:
- обробляти більше запитів без додаткових серверів;
- не втратити продуктивність на складності реалізації;
- масштабуватись без перепрацювання архітектури.
Розгляньмо основні підходи, які зараз найчастіше використовуються, а також реактивний підхід в порівнянні.
Підхід № 1 — блокуючий (старий добрий варіант) на мові Java з використанням Spring Framework
Ось звичний код у контролері:
@GetMapping("user/{id}")
public User getUserInfo(@PathVariable final Long id){
User user = userService.getUserById(id);
UserMetaData meta = userMetaDataService.getUserMetaData(id);
user.setMetaData(meta);
return user;
}
Якщо проаналізувати виконання даного коду та зобразити це у вигляді Request diagram то можна побачити, що все красиво, просто і зрозуміло. Але... цей код блокує потік на кожному кроці. Уявіть, що таких запитів — тисяча. Потоки простоюють, чекаючи відповідей від BE. Це як завантажити вантажівку і змусити її стояти, поки не прийде накладна.

Підхід № 2 — CompletableFuture: спроба асинхронності
Окей, вирішуємо писати паралельно:
@GetMapping("user/{id}")
public User getUserInfo(@PathVariable final Long id){
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUserInfo(id));
CompletableFuture<UserMetaData> metaFuture = CompletableFuture.supplyAsync(() -> userMetaDataService.getUserMetaData(id));
CompletableFuture.allOf(userFuture, metaFuture).join();
User user = userFuture.join();
user.setMetaData(metaFuture.join());
return user;
}
Виглядає розумно: обидві операції відбуваються паралельно. Але головний потік все одно блокується на .join(). Та й читабельність коду падає. Для простої операції ми отримали набагато складнішу реалізацію. У даному прикладі виклики виконуються паралельно, але зрештою об’єднання результату getUser() у getUserMetaData() блокується, доки не буде отримано відповіді від обох викликів.

Підхід № 3 — реактивність на Spring WebFlux
Реальна перевага проявилась, коли ми спробували WebFlux:
@GetMapping("user/{id}«)
public Mono<User> getUserInfo(@PathVariable final Long id ){
return userService.getUser(id)
.zipWith(userMetaDataService.getUserMetaData(id))
.map(tuple -> {
User user = tuple.getT1();
user.setUserMetaData(tuple.getT2());
return user;
});
}
Якщо зобразити виконання цього коду на діаграмі, то ми побачимо таку картину:

Це вже reactive. Ми отримуємо результат, коли обидва Mono готові, і весь цей час потік не блокується. Потік вивільняється, щойно доходить до очікування, і продовжує працювати далі — або з іншим запитом, або просто спить.
Mono vs Flux — простими словами
Mono<T> — результатом є один елемент або none.

Flux<T> — це потік об’єктів (від 0 до N).

Вони обидва реалізують Publisher, і за філософією працюють через патерн Observer — ви не тягнете результат, ви підписуєтесь, а коли він готовий — отримуєте.
Як це виглядає у виконанні
Запит приходить → Mono або Flux створюється
Нічого не виконується, поки немає subscribe()
Коли дані приходять — ми їх обробляємо
Потік не простоює → звільняється → масштабування йде вертикально
Реальні плюси
✅ немає блокування потоку
✅ ідеально підходить для хмарних середовищ
✅ можна масштабуватись без втрати продуктивності
✅ код стає гнучким для обробки великої кількості запитів
Але не все так веселково
Реактивність — не про «давайте одразу». Є й зворотна сторона:
🔻 відлагоджування — біль (нема класичного step-through debug)
🔻 складніший код для новачків
🔻 багато сторонніх бібліотек ще не підтримують реактивність
🔻 деякі архітектурні рішення треба буде переписати
Коли я рекомендую реактивність?
Так:
- коли система має 1000+ одночасних запитів
- коли інфраструктура обмежена
- коли у вас мікросервісна архітектура
- коли у вас WebSocket/Streaming API
Ні:
- коли це звичайний CRUD-REST сервіс
- коли треба швидко зробити MVP
- коли команда без досвіду у WebFlux
Висновок
Реактивне програмування — це не магія. Це підхід, який працює, коли його використовують правильно. У нашому випадку — воно дало ефект, який ми шукали: більше запитів, менше ресурсів, краща продуктивність.
Але ключове — використовувати там, де це має сенс.
Дякую, що дочитали. Якщо цікаво і маєте запитання, коментарі — пишіть
32 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів