В каких случаях лучше использовать CompletableFuture, а в каких — реактивное программирование?
CompletableFuture идеально покрывает 90% задач микросервисов:
🟢 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
- CompletableFuture — 2-3 параллельных вызова API, объединить результаты.
- CompletableFuture + Virtual Threads — масштабируемые блокирующие операции.
- 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]]