«Когда пару лет назад я впервые столкнулась с реактивным программированием, — рассказывает моя коллега Екатерина, — казалось, что это что‑то слишком сложное и академическое. Но чем больше работаешь с современными высоконагруженными системами, тем яснее становится, что без реактивного подхода сложно обеспечить высокую отзывчивость и масштабируемость».

Екатерина,
разработчик Java в Programming Store
Введение
Сегодня реактивные технологии уже перестали быть экзотикой. Netflix, Uber, Alibaba — все они активно используют реактивные стеки, чтобы выдерживать миллионы одновременных подключений. И если вы Java-разработчик, то знание WebFlux, Reactor или R2DBC становится не просто преимуществом, а необходимостью.
Представьте обычное современное веб-приложение: пользователи лайкают, отправляют сообщения, загружают файлы, получают пуши — всё одновременно и в реальном времени. На классических синхронных потоках такое приложение быстро сталкивается с ограничениями производительности. Именно в этом случае преимущества реактивного программирования проявляются наиболее явно.
Идеи реактивности появились ещё в 90-х, когда разработчики заговорили о потоках данных и наблюдаемых последовательностях. Но настоящий прорыв случился ближе к 2014 году. Netflix столкнулся с тем, что традиционные синхронные архитектуры просто не тянут миллионы пользователей, постоянные запросы, гигантскую нагрузку на сеть. Решение родилось внутри компании — библиотека RxJava, которая позже была открыта миру и получила широкое распространение в Java-сообществе.
Reactive Extensions (Rx)
Примерно в то же время Microsoft активно развивал Reactive Extensions (Rx) под .NET. Это подтвердило, что концепция универсальна и применима в любых экосистемах.
Реактивное программирование в Java — это не абстрактная концепция, а вполне конкретные технологии и стандарты.
Reactive Streams API сначала появился как отдельная спецификация (org.reactivestreams), а в Java 9 — java.util.concurrent.Flow, семантически эквивалентный её интерфейсам. Он определяет:
Publisher — источник (поставщик) данных,
Subscriber — потребитель данных,
Subscription — управление потоком,
Processor — компонент, который является одновременно подписчиком и издателем (то есть потребляет элементы типа T и выпускает элементы типа R).
Project Reactor — флагманская реализация от Pivotal (создателей Spring), включающая:
Mono представляет асинхронную последовательность с максимум одним элементом,
Flux — последовательность от 0 до N элементов.
Spring WebFlux — реактивный веб-фреймворк, выпущенный в Spring 5 (2017):
работает по умолчанию на Netty,
обеспечивает лучшую масштабируемость и меньшие затраты потоков и памяти в I/O-bound сценариях по сравнению с классическим Spring MVC.
Таким образом, Reactive Streams — это спецификация, то есть набор интерфейсов, которые описывают, как именно должны взаимодействовать компоненты в реактивной системе. Project Reactor — это уже конкретная реализация этих интерфейсов. Его классы Flux и Mono — это, по сути, Publisher, но с мощным набором операторов (map, flatMap, filter, merge и т. д.), которые позволяют легко описывать асинхронные цепочки обработки данных. А Spring WebFlux — это надстройка над Reactor, которая применяет эти принципы в веб-контексте. Она позволяет строить неблокирующие REST-контроллеры, маршруты и обработчики запросов, используя Mono и Flux как стандартные типы возвращаемых значений.
Современная реактивная экосистема Java включает:
R2DBC — реактивный доступ к реляционным БД,
Reactive MongoDB/Cassandra драйверы,
RSocket — реактивный протокол для микросервисов,
Micrometer — продвинутые метрики для мониторинга.
Если сравнивать, то традиционная модель похожа на службу такси: одна машина — один пассажир, и, если где-то пробка, всё стоит. Реактивный подход ближе к метро: один состав перевозит тысячи людей одновременно, движение не останавливается, ресурсы используются эффективно.
Но реактивное программирование не волшебная таблетка. Это просто естественный шаг вперёд, когда классические блокирующие подходы перестают справляться. И самое приятное — начать можно постепенно. Вы можете внедрять реактивные компоненты точечно, не переписывая всё приложение с нуля.
Команда Spring отмечает, что WebFlux способен обрабатывать больше одновременных соединений при меньших затратах ресурсов по сравнению с классическим Spring MVC в I/O-bound сценариях. Ниже я покажу, как шаг за шагом перейти к реактивному подходу, не потеряв устойчивости и простоты сопровождения кода.
Reactive Manifesto: четыре принципа современных систем
Reactive Manifesto — это не сухая спецификация, а, скорее, набор идей о том, как строить живые, адаптивные системы. Представьте, что вы создаёте не просто приложение, а организм, который должен спокойно переносить стресс, меняться под давлением и оставаться в форме. Именно к этому и сводятся четыре базовых принципа реактивного подхода.
Ниже приведены краткие примеры кода, иллюстрирующие работу реактивного программирования. Они не являются универсальным стандартом и в реальных системах требуют дополнительной логики: логирования ошибок, метрик, обработки отказов и применения шаблонов, таких как bulkhead, circuit breaker, fallback, back‑pressure, partitioning и др., в зависимости от потребностей системы.
Responsive (отзывчивость) — основа пользовательского опыта
Любая система должна отвечать быстро и предсказуемо — независимо от того, что происходит внутри. Пользователь не должен долго ожидать ответа и думать, все ли в порядке. Даже под нагрузкой система должна сохранять ощущение плавности и контроля.
// Традиционный подход может "зависнуть"
public String loadUserDataBlocking(String userId) {
// Может блокировать поток на неопределенное время
return database.query("SELECT * FROM users WHERE id = ?", userId);
}
// Реактивный подход гарантирует ответ
public Mono<String> loadUserDataReactive(String userId) {
return userRepository.findById(userId) // userRepository должен быть реактивный
.timeout(Duration.ofSeconds(3)) // Гарантия максимального времени ответа
.onErrorReturn("Пользователь не доступен"); // Всегда возвращаем результат
}
Resilient (устойчивость) — искусство оставаться на плаву
Даже лучшие системы иногда ломаются, и это нормально. Главное, чтобы поломка в одном месте не тянула за собой всё остальное.
Реактивная архитектура как раз и помогает локализовать сбои, изолировать проблемы и восстанавливаться без вмешательства человека.
public Mono<Order> processOrder(Order order) {
return inventoryService.reserveItems(order) // если тут есть блокирующие вызовы то нужно добавлять .subscribeOn(Schedulers.boundedElastic()
.transformDeferred(circuitBreaker::run) // Защита от повторяющихся сбоев
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) // 3 попытки с растущей задержкой
.timeout(Duration.ofSeconds(30))
.onErrorResume(TimeoutException.class, e -> {
// Переход на упрощенную логику при таймауте
return processOrderWithLimitedFunctionality(order);
})
.onErrorResume(ServiceUnavailableException.class, e -> {
return processOrderWithoutReservation(order);
})
.onErrorReturn(createFallbackOrder(order));
}
Если проводить аналогию, это напоминает торговый центр: отключился эскалатор — включились лифты; пропало электричество — зажглось аварийное освещение. Система не останавливается, а просто переходит в другой режим.
Elastic (эластичность) — гибкость под нагрузкой
Нагрузки растут, трафик скачет, и система должна реагировать на это сама, без паники. Эластичность — это способность приложения масштабироваться туда, где нужно, и не держать лишние ресурсы, когда всё спокойно.
@Bean
public Scheduler elasticScheduler() {
return Schedulers.newBoundedElastic(
10, // лимит расширения при пиковой нагрузке
1000, // буфер для задач при превышении лимита потоков
"elastic-pool",
60, // Потоки, которые простаивают более 60 секунд, автоматически удаляются
true);
}
public Flux<String> processBatchReactive(List<String> items) {
return Flux.fromIterable(items)
.flatMap(item ->
Mono.fromCallable(() -> processItem(item)) // тут processItem - блокирующая операция, если тут будет реактивный метод, то subscribeOn не нужен
.subscribeOn(elasticScheduler())
// Если processItem выполняется дольше 30 секунд - операция прерывается
.timeout(Duration.ofSeconds(30))
// При ошибке будет 3 попытки с задержкой
.retryWhen(Retry.backoff(3, Duration.ofMillis(100)))
.name("processItem")
.metrics() // метрики нужно настраивать дополнительно
,
10 // Максимум 10 одновременных операций processItem()
)
// Если вся обработка списка занимает больше 5 минут - прерываем
.timeout(Duration.ofMinutes(5));
}
Эластичность работает на всех уровнях: Kubernetes поднимает больше подов при пике трафика, а Reactor эффективно распределяет нагрузку уже внутри каждого экземпляра.
Message-Driven (ориентированность на сообщения) — основа коммуникации
В реактивной архитектуре компоненты общаются через сообщения. Это снижает связанность и делает систему гибкой: один сервис может временно отвалиться, а остальные спокойно продолжат работу.
// сервис для отправки заказов в брокер сообщений
@Service
public class ReactiveOrderService {
private final StreamBridge streamBridge;
public ReactiveOrderService(StreamBridge streamBridge) {
this.streamBridge = streamBridge;
}
public Mono<Void> processOrder(Order order) {
return Mono.fromCallable(() -> streamBridge.send("orders-out-0", order))
.subscribeOn(Schedulers.boundedElastic())
.doOnSuccess(sent -> log.info("✅ Заказ отправлен: {}", order.getId()))
.then();
}
// обработчик входящих сообщений
@Component
public class OrderMessageHandler {
private final ReactiveOrderService orderService;
public OrderMessageHandler(ReactiveOrderService orderService) {
this.orderService = orderService;
}
@Bean
public Consumer<Flux<Order>> orderProcessor() {
return flux -> flux
.flatMap(order -> orderService.processOrder(order));
}
}
Вместо хрупкой сети прямых вызовов получается устойчивая экосистема, где компоненты общаются через чётко определённые каналы.
Эти принципы не работают по отдельности:
Message-Driven даёт основу для Elastic масштабирования,
Resilient помогает системе оставаться Responsive даже при сбоях,
а Elastic характер поддерживает устойчивость, когда нагрузка скачет.
В итоге система не просто работает: она живёт и предсказуемо себя ведёт в условиях постоянной нагрузки. Это и есть суть реактивного подхода: не бороться с хаосом, а научиться с ним сосуществовать.
Когда выбирать реактивный и традиционный подход?
Реактивный подход:
Приложение |
Почему подходит |
Пример |
Чат, мессенджер |
Тысячи сообщений в реальном времени |
Telegram |
Торговая платформа |
Мгновенные обновления цен |
Биржевые терминалы |
Стриминговый сервис |
Потоковая передача видео |
Netflix |
Игровой сервис |
Многопользовательские игры онлайн |
Игровые серверы |
Мониторинг систем |
Постоянный поток метрик |
Grafana, дашборды |
Традиционный подход:
Приложение |
Почему подходит |
Пример |
Интернет-магазин |
Синхронные транзакции с гарантированной согласованностью данных |
Amazon, OZON |
Банковское приложение |
Сложные транзакции со строгой согласованностью |
Мобильный банк |
Корпоративный портал |
Документооборот, CRM |
1С |
Аналитические отчёты |
Сложные расчёты, статистика |
Excel |
Реактивный подход, если нужно:
масштабирование: >10000 пользователей онлайн,
реальное время: данные обновляются каждую секунду,
потоковые данные: видео, аудио, события IoT,
позволяет эффективнее использовать серверные ресурсы и масштабироваться при высокой нагрузке
Традиционный подход, если:
простота: команда из 1-3 разработчиков,
логика больших вычислений и CPU-bound,
стандартный CRUD: формы, таблицы, отчёты,
сжатые сроки: нужно быстро выпустить MVP.
Все приведённые выше критерии не являются строгим руководством к действию. Они, скорее, служат поводом задуматься и проанализировать: действительно ли в вашем случае оправдан реактивный подход. То, что отлично работает в одном приложении, может оказаться неоправданно сложным или неэффективным в другом. Всё зависит от конкретных задач и контекста системы. Задайте себе несколько вопросов, которые помогут в анализе:
Какова основная нагрузка — I/O-bound или CPU-bound?
Требуется ли работа в реальном времени и минимальная задержка отклика?
Есть ли высокая параллельность, множество одновременных соединений или непредсказуемые пики нагрузки?
Насколько команда знакома с реактивным подходом?
Поддерживают ли используемые библиотеки и инфраструктура неблокирующий режим?
Практическое начало: первые шаги с Reactor. Базовые операции с Mono и Flux
Когда использовать MONO (один результат):
public class MonoUseCases {
// поиск по ID - всегда один объект или null
Mono<User> findUserById(String userId) {
return userRepository.findById(userId); // предполагается, что userRepository реактивный
}
// создание ресурса - возвращаем созданный объект
Mono<Order> createOrder(OrderRequest request) {
return orderRepository.save(request.toOrder());
}
// аутентификация - возвращаем токен или ошибку
Mono<AuthToken> login(String email, String password) {
return authService.authenticate(email, password);
}
// валидация - успех или ошибка
Mono<Void> validateEmail(String email) {
return emailValidator.isValid(email)
? Mono.empty()
: Mono.error(new InvalidEmailException());
}
}
Когда использовать FLUX (поток результатов):
// список элементов - много объектов
Flux<Product> getAllProducts() {
return productRepository.findAll();
}
// поиск с фильтрацией - несколько результатов
Flux<User> findUsersByCity(String city) {
return userRepository.findByCity(city);
}
// реальное время - поток событий
@GetMapping(value = "/notifications",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Notification> streamNotifications(String userId) {
return notificationService.getUserNotifications(userId)
.doOnCancel(() -> log.info("Клиент отключен")); // Обработка отключения
}
// IoT данные - непрерывный поток с датчиков
Flux<SensorData> streamSensorData(String deviceId) {
return sensorService.subscribeToDevice(deviceId)
.sample(Duration.ofSeconds(1)) // Дросселирование - 1 значение в секунду
.onBackpressureLatest() // Только последнее значение при перегрузке
.doOnSubscribe(sub -> log.info("Подписан на устройство: {}", deviceId))
.doOnComplete(() -> log.info("Устройство {} стрим завершен", deviceId));
}
// аудит и логи - поток событий системы
Flux<AuditEvent> getAuditLog(LocalDateTime from, LocalDateTime to) {
return auditRepository.findByTimestampBetween(from, to);
}
Разница между традиционным и реактивным подходом в поведении при нагрузке
Традиционный подход (проблемы):
// блокирующий подход
@GetMapping("/users/{id}")
public User findUserById(String userId) {
// Каждый запрос занимает один поток на всё время выполнения
User user = userRepository.findById(userId); // Поток БЛОКИРОВАН на 200ms
return user;
// Поток освобождается только после полного выполнения
}
// ПРИ 1000 одновременных запросов:
// 1000 потоков × 200ms = 200 секунд блокировки!
// Сервер "захлёбывается" - кончаются потоки
Реактивный подход (решение):
// неблокирующий подход
@GetMapping("/users/{id}")
public Mono<User> findUserById(String userId) {
return userRepository.findById(userId); // реактивный userRepository
// Поток НЕ блокируется - сразу освобождается!
// Запрос "подписывается" на результат и ждёт его асинхронно
}
// ПРИ 1000 одновременных запросов:
// 1 поток может обработать 10000+ таких запросов!
// Сервер использует ресурсы эффективно
Традиционный подход (проблемы):
// Вся коллекция загружается в память сразу
@GetMapping("/products")
public List<Product> getAllProducts() {
// Все продукты загружаются в память одновременно
List<Product> products = productRepository.findAll();
// Блокируем поток до полной загрузки всех данных
return products;
// При 1,000,000 товаров: 1GB памяти + блокировка на 2+ секунды
}
Реактивный подход (решение):
// Данные стримятся постепенно
@GetMapping(value = "/products", produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<Product> getAllProductsReactive() {
return productRepository.findAll(); реактивный productRepository
// Данные отправляются по мере готовности
// Поток освобождается сразу, клиент получает данные постепенно
// При 1,000,000 товаров: 1MB буфер + неблокирующая обработка
}
Ошибки при реактивном программировании
Далее приведу три самых частых случая, когда код вроде бы реактивный, но фактически работает синхронно, блокирует потоки или теряет данные.
1. Использование .block() и .subscribe() не там, где нужно.
Вызовы .block() или .subscribe() ломают асинхронность, если использовать их в контроллерах или сервисах.
@GetMapping("/users/{id}")
public User getUser(String id) {
// Ошибка: блокируем поток до получения результата
return userService.findById(id).block();
}
.block() заставляет поток ждать результат — теряется вся неблокирующая модель.
под высокой нагрузкой сервер быстро испытывает дефицит ресурсов, поскольку каждый запрос блокирует поток выполнения.
Фреймворк WebFlux сам подписывается на поток. Нужно просто вернуть Mono или Flux.
@GetMapping("/users/{id}")
public Mono<User> getUser(String id) {
// Реактивно: поток сразу освобождается
return userService.findById(id);
}
Ошибка: запускаем поток внутри контроллера
@GetMapping("/orders")
public void process() {
orderService.getOrders()
.subscribe(order -> log.info("Order: {}", order));
}
.subscribe() необходим, когда вы создаете поток и берете на себя управление его жизненным циклом. Иногда это может понадобиться.
@Bean
@Scheduled(fixedRate = 5000)
public Disposable backgroundCleanup() {
// Самостоятельно создаем и управляем потоком
return userService.cleanupExpiredSessions()
.subscribeOn(Schedulers.boundedElastic())
.subscribe(
count -> log.info("Cleaned up {} sessions", count),
error -> log.error("Cleanup failed", error)
);
}
@Component
public class OrderMessageProcessor {
private Disposable subscription;
@EventListener(ApplicationReadyEvent.class)
public void startProcessing() {
// Самостоятельно создаем поток из Kafka
this.subscription = kafkaReceiver.receive()
.flatMap(record ->
orderService.process(record.value())
.doOnSuccess(result -> record.receiverOffset().acknowledge())
.subscribeOn(Schedulers.boundedElastic())
)
.subscribe(
result -> log.debug("Processed order"),
error -> log.error("Order processing failed", error)
);
}
@PreDestroy
public void stopProcessing() {
if (subscription != null) {
subscription.dispose();
}
}
}
2. Блокирующие вызовы внутри реактивных потоков.
Иногда разработчик вроде бы пишет на Flux и Mono, но внутри всё равно вызывает блокирующий код — базы, API, файловые операции.
public Flux<User> findAllUsers() {
return Flux.fromIterable(userRepository.findAll()); // блокирующий JPA вызов
}
Такой код формально реактивный, но по факту «тормозной». Поток Reactor ожидает завершения findAll(), пока другие операции простаивают.
Если блокирующий вызов избежать нельзя, нужно вынести его на отдельный пул потоков с помощью Schedulers.boundedElastic():
public Flux<User> findAllUsers() {
return Mono.fromCallable(() -> userRepository.findAll()) // Безопасный вызов
.subscribeOn(Schedulers.boundedElastic()) // Вынос в отдельный пул потоков
.flatMapMany(Flux::fromIterable); // Преобразование в поток
}
boundedElastic — это динамический пул потоков с ограничением по количеству активных задач, оптимальный для кратковременных блокирующих операций (I/O, файловые операции, JDBC). Но он не предназначен для тяжёлых вычислений. Для этого лучше использовать Schedulers.parallel().
Проверяйте библиотеки. Если они не поддерживают Reactive Streams (например, JPA или старые HTTP-клиенты), не вызывайте их напрямую в реактивных цепочках.
3. Потеря управления backpressure (перегрузка потока).
Многие новички даже не подозревают о существовании backpressure — механизма контроля скорости потока. Без него реактивное приложение может «утонуть» в собственных данных: производитель шлёт миллионы событий, а потребитель не успевает обрабатывать.
Flux.interval(Duration.ofMillis(1))
.map(this::process)
.subscribe(); // Поток быстро переполнится
На первый взгляд, Flux.interval() безопасен, но если внутри цепочки тяжёлая обработка (например, flatMap без ограничения), поток событий может накапливаться быстрее, чем обрабатывается, отсюда и необходимость onBackpressure.
Используйте встроенные стратегии Reactor:
.onBackpressureBuffer() — временно хранить элементы в буфере,
.onBackpressureDrop() — отбрасывать лишние,
.onBackpressureLatest() — оставлять только последние.
Flux.interval(Duration.ofMillis(1))
.onBackpressureLatest() // ограничиваем поток
.flatMap(this::process)
.subscribe();
Backpressure — это как предохранитель на конвейере. Если рабочий не успевает, система притормаживает подачу деталей, а не засыпает его тысячами.
Посмотрим на реальном примере одного метода получения заказов клиента, как будет выглядеть код, если мы перейдём от традиционного подхода к реактивному.
Традиционный код.
Такой код работает, но блокирует потоки: каждый запрос ждёт завершения обращения к БД. Под большой нагрузкой это быстро становится узким местом.
@RestController
@RequiredArgsConstructor
@Slf4j
public class OrderController {
private final ClientOrderFacade clientOrderFacade;
@GetMapping
public List<Order> getAllOrders(String personId) {
return clientOrderFacade.getAllOrders(personId);
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class ClientOrderFacade {
private final OrderService orderService;
private final PersonService personService;
@Transactional
public List<Order> getAllOrders(String personId) {
Person person = personService.findById(personId);
return orderService.findAllByPerson(person)
.stream()
.sorted(Comparator.comparing(Order::getCreated).reversed())
.toList();
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
@Override
public Set<Order> findAllByPerson(Person person) {
return orderRepository.findAllByPerson(person);
}
}
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
Set<Order> findAllByPerson(Person person);
}
Реактивная версия (Spring WebFlux + Reactor).
В pom.xml нужно добавить:
<!-- Spring WebFlux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Для реактивного доступа к БД -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- Драйвер для PostgreSQL -->
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
@RestController
@RequiredArgsConstructor
@Slf4j
public class OrderController {
private final ClientOrderFacade clientOrderFacade;
@GetMapping
public Flux<Order> getAllOrders(@RequestParam String personId) {
log.info("Поиск заказов пользователя {}", personId );
return clientOrderFacade.getAllOrders(personId);
}
}
List<Order> заменён на Flux<Order>. Контроллер теперь возвращает поток данных, который WebFlux отдаёт клиенту по мере готовности — без блокировки.
@Component
@RequiredArgsConstructor
@Slf4j
public class ClientOrderFacade {
private final OrderService orderService;
private final PersonService personService;
public Flux<Order> getAllOrders(String personId) {
return personService.findById(personId)
.switchIfEmpty(Mono.error(new RuntimeException("Пользователь не найден")))
.flatMapMany(person -> orderService.findAllByPerson(person.getId()));
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
public Flux<Order> findAllByPerson(String personId) {
return orderRepository.findAllByPersonId(personId);
}
}
@Repository
public interface OrderRepository extends R2dbcRepository<Order, Long> {
@Query("SELECT * FROM orders WHERE person_id = :personId ORDER BY created DESC")
Flux<Order> findAllByPersonId(String personId);
}
Выводы
Реактивное программирование — это не модный тренд, а естественная эволюция архитектур, где важны отзывчивость, устойчивость и масштабируемость.
Оно не требует переписывать всё с нуля: начните с одного контроллера, одного реактивного потока, одного Flux. Постепенно вы увидите, как данные обрабатываются асинхронно и не блокирующе, отдавая элементы клиенту по мере их готовности. Реактивная модель с поддержкой backpressure гарантирует, что система не перегрузит ресурсы при большом количестве запросов, а серверные потоки используются эффективно. Такой подход превращает приложение в реактивную систему, которая масштабируется под нагрузку и реагирует на события в реальном времени, вместо того чтобы блокировать потоки и ожидать завершения каждого запроса
kmatveev
Чем плохи такие gpt-сгенерированные статьи, так это тем, что в начале идёт реклама и нахваливание, а потом будто начинается техническая часть, но все примеры какие-то обрезанные. Взять вот самый первый из реактивных примеров, loadUserDataReactive(). Из него непонятно, что такое возвращает userRepository и откуда его такой взять, чтобы он это возвращал (не говоря уже о том, что userRepository должен возвращать Mono<User> , а не Mono<String> , и вся эта хрень рассыпется). И таких бессмысленных вещей тут немало. Не надо таких статей, они не помогают.