В яких випадках краще використовувати 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]]