Вопрос 25 · Раздел 19

В каких случаях лучше использовать CompletableFuture, а в каких — реактивное программирование?

CompletableFuture идеально покрывает 90% задач микросервисов:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Короткий ответ

  • CompletableFuture — когда нужно сделать один асинхронный запрос и получить один результат.
  • Реактивное программирование (Reactor, RxJava) — когда нужно обрабатывать поток данных (0..N элементов).
// CompletableFuture — один запрос, один ответ
CompletableFuture<User> cf = userService.findByIdAsync(1L);
User user = cf.join();

// Reactor — поток элементов
Flux<Event> events = eventService.streamAll();
events.subscribe(System.out::println);

Правило: Если вам нужно 2-3 параллельных вызова API — берите CompletableFuture. Если потоки данных — берите Reactor.


🟡 Middle Level

Когда CompletableFuture достаточно

// Типичный кейс: параллельные вызовы
CompletableFuture<User> userFuture = userClient.getByIdAsync(userId);
CompletableFuture<Order> orderFuture = orderClient.getLatestAsync(userId);

CompletableFuture<UserWithOrder> combined = userFuture.thenCombine(
    orderFuture, UserWithOrder::new
);

CompletableFuture идеально покрывает 90% задач микросервисов:

  • Сходить в БД
  • Вызвать внешний API
  • Объединить несколько результатов

Когда нужна реактивность

1. Backpressure (обратное давление)

В CompletableFuture нет механизма backpressure. Если продюсер генерирует данные быстрее, чем консьюмер обрабатывает, очередь в Executor растёт до OutOfMemoryError.

Реактивный Flux позволяет консьюмеру запрашивать столько элементов, сколько он может переварить:

// Консьюмер контролирует скорость
flux.subscribe(element -> process(element), error -> {}, () -> {},
    subscription -> subscription.request(10));  // только 10 элементов

2. Потоковая обработка

Если нужно обрабатывать данные по мере поступления (чанк за чанком из S3, Kafka-топики, WebSocket), CompletableFuture заставит либо собрать всё в память, либо писать сложные рекурсивные цепочки. Flux делает это из коробки.

Сравнение

Характеристика CompletableFuture Reactive (Reactor)
Результатов Ровно 1 0 … ∞
Backpressure Нет Да
Операторов ~20 ~200
Кривая обучения Низкая Высокая
Отладка Средняя Сложная

🔴 Senior Level

Влияние Virtual Threads (Java 21+)

С появлением Virtual Threads необходимость в реактивном программировании для типичных I/O задач снижается:

// Вместо реактивного кода — простой синхронный код
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    User user = userClient.getById(userId);       // blocking, но дёшево
    Order order = orderClient.getLatest(userId);  // масштабируется
    return new UserWithOrder(user, order);
}

Virtual Threads делают синхронный blocking-код масштабируемым. Реактивность остаётся актуальной только там, где важны:

  • Событийная природа (стриминг, WebSockets)
  • Backpressure (контроль нагрузки)

Project Loom и Project Valhalla

Project Loom (Virtual Threads) уже доступен в Java 21 и упрощает написание масштабируемого blocking-кода.

Project Valhalla (value types) в разработке — потенциально снизит overhead CompletableFuture за счёт примитивной специализации, но это не подтверждено.

Итоговая Decision Matrix

  1. CompletableFuture — 2-3 параллельных вызова API, объединить результаты.
  2. CompletableFuture + Virtual Threads — масштабируемые блокирующие операции.
  3. Reactive — шлюз для миллионов событий, Kafka Streams, нужен Backpressure.

Резюме

  • CF — для асинхронных запросов.
  • Реактивность — для асинхронных потоков.
  • Не используйте реактивность там, где достаточно CF; сложность поддержки реактивного кода огромна.

🎯 Шпаргалка для интервью

Обязательно знать:

  • CompletableFuture — 1 запрос, 1 результат. ~20 операторов, низкая кривая обучения
  • Reactive (Reactor) — 0..N элементов, backpressure, ~200 операторов, высокая кривая обучения
  • CF для 2-3 параллельных вызовов API. Reactor для стриминга, Kafka, WebSocket
  • Virtual Threads (Java 21+) снижают необходимость в Reactor для типичных I/O задач
  • Decision Matrix: CF → CF + Virtual Threads → Reactive (по сложности)

Частые уточняющие вопросы:

  • Когда выбрать CompletableFuture? — 2-3 параллельных вызова API, объединить результаты
  • Когда Reactive? — Backpressure нужен, событийная природа (Kafka, WebSocket), millions events
  • Virtual Threads убивают Reactor? — Нет. VT для I/O blocking, Reactor для streaming + backpressure
  • Backpressure что это? — Консьюмер контролирует скорость: request(N) элементов, предотвращает OOM

Красные флаги (НЕ говорить):

  • «Reactive всегда лучше CompletableFuture» — сложность поддержки огромна, для 90% задач CF достаточно
  • «Virtual Threads заменяют Reactive» — нет, VT не дают backpressure и streaming
  • «CompletableFuture поддерживает backpressure» — нет, при overload очередь растёт до OOM

Связанные темы:

  • [[2. Какие основные преимущества CompletableFuture перед Future]]
  • [[8. Как комбинировать результаты нескольких CompletableFuture]]
  • [[12. Какой пул потоков используется по умолчанию для async методов]]
  • [[23. Как тестировать код с CompletableFuture]]