Питання 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]]