CompletableFuture — async composition trong Java
Future giới hạn như thế nào, CompletableFuture JEP 155 Java 8: supplyAsync, thenApply/thenCompose, allOf/anyOf, exceptionally/handle, và 3 pitfall nguy hiểm nhất production.
TL;DR: Future<T> (Java 5) chỉ cung cấp get() blocking — không chain, không combine, không xử lý lỗi per-task. CompletableFuture<T> (Java 8, JEP 155) giải quyết tất cả: fluent pipeline với thenApply/thenCompose, merge nhiều future với allOf/anyOf, error handling với exceptionally/handle. Nhưng 3 pitfall phổ biến nhất phá hỏng async: gọi get() giữa chain (block thread), dùng commonPool cho I/O-bound (starvation toàn JVM), và không truyền Executor riêng (mặc định dùng ForkJoinPool không phù hợp cho I/O). Hiểu đúng các pitfall này quan trọng hơn nhớ API.
Bài 03 của module này đã giới thiệu ExecutorService và threading model. Bài 09 sẽ nói về Structured Concurrency — cách Java 25 giải quyết vấn đề lifecycle management mà CompletableFuture xử lý phức tạp.
1. Scenario — 3 API call tuần tự vs song song
Backend service trả về trang profile của user. Mỗi request cần 3 call downstream:
userService.fetchUser(id)— 150msorderService.fetchOrders(id)— 200msrecommendationService.recommend(id)— 180ms
Phương án A — sequential blocking:
public ProfileView getProfile(long userId) {
User user = userService.fetchUser(userId); // 150ms
List<Order> orders = orderService.fetchOrders(userId); // 200ms
List<Item> recs = recommendationService.recommend(userId); // 180ms
return new ProfileView(user, orders, recs);
}
// Tong: 150 + 200 + 180 = 530ms
Phương án B — parallel với Future:
public ProfileView getProfile(long userId) throws Exception {
Future<User> userF = exec.submit(() -> userService.fetchUser(userId));
Future<List<Order>> ordF = exec.submit(() -> orderService.fetchOrders(userId));
Future<List<Item>> recF = exec.submit(() -> recommendationService.recommend(userId));
// Tong: max(150, 200, 180) = 200ms
return new ProfileView(userF.get(), ordF.get(), recF.get());
}
Tốt hơn về latency, nhưng Future có nhiều giới hạn nghiêm trọng.
2. Giới hạn của Future<T>
Future<T> (interface java.util.concurrent.Future, Java 5) đại diện cho kết quả của một tính toán bất đồng bộ. Chỉ có 5 method: get(), get(timeout), cancel(), isDone(), isCancelled().
4 giới hạn lớn:
Giới hạn 1 — get() chỉ blocking: không có callback khi kết quả sẵn sàng. Phải poll (isDone()) hoặc block (get()).
Giới hạn 2 — không chain: không thể nói "khi A xong, dùng kết quả A để chạy B". Phải A.get() (block) rồi mới submit B.
Giới hạn 3 — không combine: không có API sẵn cho "chờ cả A, B, C xong". Phải manually get() từng cái.
Giới hạn 4 — không xử lý lỗi per-task: nếu A throw exception, phải try/catch từng get(). Không có "nếu fail, dùng default value".
CompletableFuture<T> (Java 8, java.util.concurrent.CompletableFuture) implement cả Future<T> và CompletionStage<T>. CompletionStage là interface định nghĩa chuỗi callback (chain) — over 60 method cho mọi combination async/sync, success/error, combine/transform.
3. Tạo CompletableFuture
4 factory method phổ biến:
// supplyAsync -- tao CF tu supplier, chay tren pool
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> fetchData());
// supplyAsync voi executor tuong minh (best practice cho I/O)
ExecutorService ioPool = Executors.newFixedThreadPool(20);
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> fetchData(), ioPool);
// runAsync -- void task, khong co return value
CompletableFuture<Void> cf3 = CompletableFuture.runAsync(() -> sendEmail());
// completedFuture -- already done, dung de wrap gia tri sync
CompletableFuture<String> cf4 = CompletableFuture.completedFuture("cached_result");
// failedFuture -- already failed (Java 9+)
CompletableFuture<String> cf5 = CompletableFuture.failedFuture(new RuntimeException("DB down"));
completedFuture hữu ích khi có kết quả từ cache (không cần async) nhưng API caller expect CompletableFuture. Chain tiếp tục bình thường.
4. Executor mặc định và pitfall ForkJoinPool
Khi không truyền Executor, supplyAsync dùng ForkJoinPool.commonPool() — pool shared toàn JVM với số worker thread mặc định bằng Runtime.getRuntime().availableProcessors() - 1.
Trên máy 8 core: 7 thread trong commonPool, shared với mọi parallel stream, mọi CompletableFuture.supplyAsync() không có executor.
// Kiem tra kich thuoc commonPool
System.out.println(ForkJoinPool.commonPool().getParallelism()); // 7 tren may 8 core
Vì sao nguy hiểm với I/O-bound: nếu 7 task trong commonPool đều gọi HTTP (block 200-500ms), không còn thread để xử lý task khác. Toàn bộ JVM bị throttle.
Best practice: luôn truyền executor riêng cho I/O-bound task:
// I/O-bound pool rieng -- khong anh huong commonPool
ExecutorService ioPool = Executors.newFixedThreadPool(50);
// Hoac dung virtual thread (Java 21+) -- khong can size tuning
ExecutorService vtPool = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<User> userCF = CompletableFuture.supplyAsync(
() -> userService.fetchUser(id), ioPool
);
5. Chain operations — transform, compose, combine
5.1 thenApply — transform result (như map)
CompletableFuture<String> rawCF = CompletableFuture.supplyAsync(() -> fetchRawJson());
CompletableFuture<User> userCF = rawCF.thenApply(json -> parseUser(json));
thenApply nhận Function<T, U> — transform kết quả khi CF hoàn thành. Chạy trên thread hoàn thành CF (hoặc thread gọi nếu CF đã done).
5.2 thenAccept — side effect (như forEach)
userCF.thenAccept(user -> auditLog.record(user.id(), "profile_viewed"));
// Return: CompletableFuture<Void>
5.3 thenRun — no input, no output
CompletableFuture<Void> done = userCF.thenRun(() -> metrics.increment("profile.loaded"));
5.4 thenCompose — flatMap (như flatMap trong stream)
// DUNG khi callback tra ve CompletableFuture khac
CompletableFuture<Order> latestOrder = userCF
.thenCompose(user -> orderService.fetchLatestOrder(user.id()));
// fetchLatestOrder tra ve CF<Order>, khong phai Order
So sánh thenApply vs thenCompose:
thenApply(f)khiftrả vềT(sync transform) →CF<T>.thenCompose(f)khiftrả vềCF<T>(async op) →CF<T>(flat, không phảiCF<CF<T>>).
Dùng sai sẽ được CF<CF<T>> — nested future, phải unwrap thủ công.
5.5 thenCombine — merge 2 CF khi cả hai xong
CompletableFuture<User> userCF = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture<List<Order>> ordersCF = CompletableFuture.supplyAsync(() -> fetchOrders(id));
CompletableFuture<ProfileView> profileCF = userCF.thenCombine(
ordersCF,
(user, orders) -> new ProfileView(user, orders) // BiFunction
);
Cả 2 CF chạy song song. Khi cả hai xong, callback nhận cả 2 kết quả.
5.6 Async variants
Mỗi method trên có variant *Async:
// thenApply -- chay callback tren thread hoan thanh CF (co the main thread)
cf.thenApply(fn)
// thenApplyAsync -- chay callback tren ForkJoinPool.commonPool()
cf.thenApplyAsync(fn)
// thenApplyAsync voi executor -- chay tren pool chi dinh
cf.thenApplyAsync(fn, customPool)
Dùng *Async khi callback tốn thời gian và không muốn block thread hoàn thành CF trước. Không dùng *Async với commonPool cho I/O — lại rơi vào pitfall starvation.
6. Error handling — exceptionally, handle, whenComplete
exceptionally — only on error
CompletableFuture<User> userCF = CompletableFuture.supplyAsync(() -> fetchUser(id))
.exceptionally(ex -> {
log.warn("fetchUser failed: {}", ex.getMessage());
return User.anonymous(); // fallback value
});
Nếu upstream thành công, exceptionally bị skip. Nếu upstream throw, nhận Throwable, trả về fallback value. Result type phải giống upstream.
handle — always (both success and error)
CompletableFuture<User> userCF = CompletableFuture.supplyAsync(() -> fetchUser(id))
.handle((user, ex) -> {
if (ex != null) return User.anonymous(); // error path
return user; // success path
});
handle nhận BiFunction<T, Throwable, U>. Luôn chạy — nếu thành công thì ex == null, nếu fail thì result == null. Có thể transform sang type khác.
whenComplete — side effect, không transform
userCF.whenComplete((user, ex) -> {
if (ex != null) metrics.increment("fetch.error");
else metrics.increment("fetch.success");
// return void -- khong transform result
});
whenComplete không thay đổi kết quả của CF — chỉ side effect. Nếu upstream fail và whenComplete không throw, downstream vẫn thấy exception gốc.
7. Combinators — allOf và anyOf
allOf — chờ tất cả
CompletableFuture<User> userCF = CompletableFuture.supplyAsync(() -> fetchUser(id), pool);
CompletableFuture<List<Order>> ordersCF = CompletableFuture.supplyAsync(() -> fetchOrders(id), pool);
CompletableFuture<List<Item>> recCF = CompletableFuture.supplyAsync(() -> recommend(id), pool);
CompletableFuture<Void> allDone = CompletableFuture.allOf(userCF, ordersCF, recCF);
// Sau allOf.join(), collect ket qua:
allDone.thenRun(() -> {
User user = userCF.join(); // da done, khong block
List<Order> orders = ordersCF.join();
List<Item> recs = recCF.join();
result.set(new ProfileView(user, orders, recs));
});
Lưu ý quan trọng: allOf trả về CompletableFuture<Void> — không mang kết quả. Phải collect thủ công từ các CF con sau khi allOf done. Dùng join() (unchecked) thay vì get() (checked) sau allOf vì biết chắc CF đã done.
Nếu bất kỳ CF nào fail, allOf fail. Các CF còn lại tiếp tục chạy — không bị cancel tự động (đây là điểm khác với Structured Concurrency ở bài 09).
anyOf — kết quả đầu tiên thắng
// Goi 3 service redundant, lay ket qua nhanh nhat
CompletableFuture<Object> firstResult = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> primaryDb.query(id), pool),
CompletableFuture.supplyAsync(() -> replicaDb.query(id), pool),
CompletableFuture.supplyAsync(() -> cacheService.get(id), pool)
);
anyOf trả về CompletableFuture<Object> (untyped — vì không biết CF nào win). Cần cast kết quả. Các CF thua tiếp tục chạy đến cuối — không cancel.
8. Pitfall 1 — gọi get() giữa chain
// BAD -- get() block thread hien tai, defeat async
CompletableFuture<User> userCF = CompletableFuture.supplyAsync(() -> fetchUser(id), pool);
User user = userCF.get(); // BLOCK -- thread nay dung o day cho I/O
// Tiep tuc voi ket qua cua user -- sequential, khong async
CompletableFuture<List<Order>> ordersCF = CompletableFuture.supplyAsync(
() -> fetchOrders(user.id()), pool
);
Pattern này biến async thành sequential blocking — không khác gì Future.get(). Thread bị block không thể xử lý task khác.
Đúng: dùng thenCompose để chain tiếp async:
// DUNG -- chain thay vi block
CompletableFuture<List<Order>> ordersCF = CompletableFuture
.supplyAsync(() -> fetchUser(id), pool)
.thenCompose(user -> CompletableFuture.supplyAsync(
() -> fetchOrders(user.id()), pool
));
9. Pitfall 2 — ForkJoinPool commonPool starvation
// BAD -- task A submit task B vao cung commonPool
CompletableFuture.supplyAsync(() -> {
// Task A: chay tren commonPool
CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> {
// Task B: cung commonPool, nhung commonPool co the da het thread
return fetchSomething();
});
return inner.join(); // Task A block cho Task B -- nhung B khong co thread de chay
// DEADLOCK CLUSTER: A block, B chua start, khong co thread moi
}, /* no executor -- uses commonPool */);
Scenario: commonPool có 7 thread, 7 task A đang block chờ task B con, không còn thread để chạy B. Toàn bộ pool bị deadlock.
Fix: dùng executor riêng cho mỗi layer:
ExecutorService outerPool = Executors.newFixedThreadPool(10);
ExecutorService innerPool = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> {
CompletableFuture<String> inner = CompletableFuture.supplyAsync(
() -> fetchSomething(),
innerPool // pool rieng, khong anh huong outerPool
);
return inner.join();
}, outerPool);
10. Pitfall 3 — cancel() không cancel computation
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
// Chay 30 giay
return heavyCompute();
}, pool);
cf.cancel(true); // Mark CF as cancelled
// heavyCompute() van chay den cuoi trong pool thread!
CompletableFuture.cancel(true) chỉ mark CF sang trạng thái cancelled — thread đang chạy task không nhận được interrupt (khác với Thread.interrupt()). Task trong pool tiếp tục đến khi tự kết thúc.
Hệ quả: nhiều cancel() call không tiết kiệm resource như mong đợi. Task vẫn chiếm thread pool.
Workaround: thiết kế task responsive với interrupt:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
for (int i = 0; i < STEPS; i++) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Cancelled");
}
doStep(i);
}
return result;
}, pool);
Hoặc dùng Future submit từ ExecutorService thay — Future.cancel(true) gửi interrupt đến thread thực sự.
11. So sánh với reactive (Mono/Flux)
| Tiêu chí | CompletableFuture | Reactive (Reactor Mono/Flux) |
|---|---|---|
| Cardinality | 1 giá trị (1-shot) | 0..N giá trị (stream) |
| Backpressure | Không | Có — subscriber control rate |
| Operator library | Đủ dùng (~60 method) | Phong phú (window, buffer, retry, throttle…) |
| Debug stack trace | Readable | Fragmented (nhiều frame framework) |
| Học curve | Thấp | Cao |
| Java version | Java 8+ | Spring WebFlux / Project Reactor |
| Virtual thread interop | Tốt — executor VT | Ổn nhưng reactive vẫn cần model riêng |
Khi nào chọn:
- CompletableFuture: tác vụ async 1-shot (fetch user, call API, query DB), cần code dễ đọc, đội dev Java mainstream.
- Reactive: stream data (Kafka consumer, real-time feed, file streaming), cần backpressure, hoặc cần operator phức tạp (retry với exponential backoff, merge streams từ nhiều source).
- Java 21+ mới: ưu tiên Virtual Thread +
CompletableFuturevới VT executor. Reactive giữ cho use case stream.
12. Diagram — async pipeline với boundaries
flowchart TD
A["supplyAsync(fetchUser)\nioPool"] -->|"thenApply(parse)\nsync on completing thread"| B["CF<User>"]
C["supplyAsync(fetchOrders)\nioPool"] --> D["CF<List<Order>>"]
E["supplyAsync(recommend)\nioPool"] --> F["CF<List<Item>>"]
B --> G["allOf(B,D,F)"]
D --> G
F --> G
G -->|"thenRun: collect .join()"| H["ProfileView"]Tất cả 3 task chạy song song trên ioPool. allOf chờ cả 3. Tổng latency = max(150ms, 200ms, 180ms) = 200ms thay vì 530ms.
13. Deep Dive
- JEP 155: Concurrency Updates (Java 8) — openjdk.org/jeps/155 — motivation và design của
CompletableFuture,CompletionStage,StampedLock. Giải thích vì saoCompletionStagelà interface riêng (không gắn vàoFuture) và 60+ method được thiết kế như thế nào. CompletableFutureJavadoc Java 21 — docs.oracle.com/.../CompletableFuture.html — đọc class-level Javadoc (dài nhưng quan trọng): giải thích completion modes, async vs sync stage execution, và thread pool semantics.- Doug Lea paper trên CompletableFuture design —
CompletableFutuređược thiết kế bởi Doug Lea. gee.cs.oswego.edu/dl/concurrency-interest/ — mailing list archive có nhiều thread thảo luận về design decision. - JEP 462: Structured Concurrency (Java 21 preview) — openjdk.org/jeps/462 — đọc phần motivation để hiểu vấn đề lifecycle/cancellation mà
CompletableFuture.allOfkhông giải quyết được, dẫn đến Structured Concurrency ra đời (bài 09). - "Java Concurrency in Practice" — Brian Goetz: Chapter 6 "Task Execution" giải thích ExecutorService + Future; Chapter 8 "Applying Thread Pools" về sizing và ForkJoinPool starvation — đọc trước khi dùng
CompletableFutureproduction. - ForkJoinPool.commonPool và virtual thread interop: với Java 21+, dùng
Executors.newVirtualThreadPerTaskExecutor()thaycommonPoolcho I/O workload — không cần size tuning, không risk starvation vì virtual thread unmount khi block I/O.
14. Self-check
Q1Vì sao dùng thenCompose thay vì thenApply khi callback trả về CompletableFuture<T>?▸
thenCompose thay vì thenApply khi callback trả về CompletableFuture<T>?thenApply(fn) nhận Function<T, U> và wrap kết quả vào CF<U>. Nếu fn trả về CompletableFuture<User>, kết quả sẽ là CF<CF<User>> — nested future. Để lấy giá trị phải unwrap 2 lần.
thenCompose(fn) nhận Function<T, CompletionStage<U>> và "flatten": nếu fn trả về CF<User>, kết quả là CF<User> (flat). Tương đương flatMap trong stream.
Ví dụ sai:
fetchUser(id).thenApply(u -> fetchOrders(u.id())) — kết quả là CF<CF<List<Order>>>. Phải gọi join() 2 lần hoặc thenCompose(cf -> cf) để flatten.
Đúng: fetchUser(id).thenCompose(u -> fetchOrders(u.id())) — kết quả là CF<List<Order>> sạch.
Rule nhớ: callback trả về giá trị thường → thenApply. Callback trả về CompletableFuture → thenCompose.
Q2Khác biệt giữa exceptionally, handle, và whenComplete?▸
exceptionally, handle, và whenComplete?exceptionally(fn): chỉ kích hoạt khi upstream fail. Nhận Throwable, trả về fallback value cùng type với upstream. Nếu upstream thành công, exceptionally bị skip hoàn toàn — giống catch block.
handle(biFunction): luôn kích hoạt, cả success lẫn failure. Nhận (result, throwable) — một trong hai sẽ null. Có thể transform sang type khác (U không cần bằng T). Giống finally nhưng có thể thay đổi kết quả.
whenComplete(biConsumer): luôn kích hoạt, nhưng không thể transform kết quả (consumer, không phải function). Dùng cho side effect (log, metrics). Nếu upstream fail và whenComplete không throw, downstream CF vẫn mang exception gốc.
Chọn:
- Chỉ muốn fallback khi lỗi →
exceptionally. - Muốn transform cả 2 path →
handle. - Chỉ muốn log/metric, không đổi result →
whenComplete.
Q3CompletableFuture.allOf() trả về CF<Void>. Làm sao collect kết quả của các CF con sau khi allOf hoàn thành?▸
CompletableFuture.allOf() trả về CF<Void>. Làm sao collect kết quả của các CF con sau khi allOf hoàn thành?allOf trả về CF<Void> — không mang kết quả. Sau khi allOf done (tất cả CF con xong), phải gọi join() hoặc resultNow() trực tiếp trên từng CF con.
Pattern chuẩn:
CompletableFuture<Void> all = CompletableFuture.allOf(cfA, cfB, cfC);
Sau đó:
all.thenRun(() -> { A result = cfA.join(); B result2 = cfB.join(); ... });
Tại sao join() không block ở đây: vì chúng ta đang trong callback của allOf — khi callback này chạy, tất cả CF con đã done. join() trên CF đã done trả ngay lập tức.
Pattern gọn với stream:
List<CF<String>> cfs = ...; CF<List<String>> all = CompletableFuture.allOf(cfs.toArray(new CF[0])).thenApply(v -> cfs.stream().map(CF::join).toList());
Đây là workaround thường thấy trong production — allOf cho timing, stream join() cho collection. Structured Concurrency (bài 09) giải quyết gọn hơn với scope.fork() và subtask.get().
Q4Điều gì xảy ra khi gọi cf.cancel(true)? Task trong pool có dừng lại không?▸
cf.cancel(true)? Task trong pool có dừng lại không?CompletableFuture.cancel(true) chỉ thay đổi state của CF object sang CANCELLED — nó không gửi interrupt đến thread đang chạy task trong pool. Argument true (mayInterruptIfRunning) bị bỏ qua bởi implementation của CompletableFuture.
Hậu quả thực tế: task vẫn chạy đến khi tự kết thúc, vẫn chiếm thread trong pool. Downstream của CF bị cancel sẽ nhận CancellationException khi gọi get() hoặc join(), nhưng computation thực sự vẫn tiếp tục trong nền.
So sánh với Future từ ExecutorService.submit(): future.cancel(true) trên FutureTask sẽ gọi Thread.interrupt() trên thread đang chạy — task nhận interrupt signal và có thể thoát nếu check isInterrupted().
Workaround cho CompletableFuture: thiết kế task check Thread.currentThread().isInterrupted() định kỳ. Hoặc giữ tham chiếu đến Future gốc (từ pool.submit()) và cancel nó riêng, độc lập với CompletableFuture.
Đây là một trong những lý do Structured Concurrency hấp dẫn: cancel propagate tự động đến tất cả subtask trong scope.
Q5Vì sao không nên dùng ForkJoinPool.commonPool() cho I/O-bound CompletableFuture trong production?▸
ForkJoinPool.commonPool() là singleton shared toàn JVM. Số thread mặc định bằng số CPU core trừ 1 (8-core machine = 7 thread). Pool này cũng được dùng bởi mọi parallelStream(), mọi CompletableFuture.supplyAsync() không có executor riêng.
Với I/O-bound task (HTTP call, DB query): thread block 100-500ms mỗi call. Nếu 7 thread đều block vào I/O, không còn thread để xử lý bất kỳ task nào khác trong JVM. Throughput giảm về 0. Endpoint khác của service cũng bị ảnh hưởng.
Tình huống tệ hơn: task A submit task B vào commonPool. Task A block chờ B. B cần thread nhưng pool đã đầy (A và các task khác đang block). Deadlock cluster — không phải 1 deadlock, mà N task tạo vòng chờ nhau.
Fix đơn giản nhất: luôn truyền executor riêng. Với Java 21+, Executors.newVirtualThreadPerTaskExecutor() lý tưởng cho I/O — virtual thread unmount khi block I/O, không chiếm platform thread. Không cần size tuning. Không risk starvation.
Q6Viết code fetch 3 service song song (user, orders, recommendations) bằng CompletableFuture, tổng latency bằng latency service chậm nhất. Xử lý nếu recommendations fail thì dùng list rỗng.
(Không cần import, chỉ cần logic chính)▸
CompletableFuture, tổng latency bằng latency service chậm nhất. Xử lý nếu recommendations fail thì dùng list rỗng.(Không cần import, chỉ cần logic chính)
Pattern chuẩn:
ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<User> userCF = CompletableFuture
.supplyAsync(() -> userSvc.fetchUser(id), pool);
CompletableFuture<List<Order>> ordersCF = CompletableFuture
.supplyAsync(() -> orderSvc.fetchOrders(id), pool);
CompletableFuture<List<Item>> recCF = CompletableFuture
.supplyAsync(() -> recSvc.recommend(id), pool)
.exceptionally(ex -> List.of()); // fallback on failure
CompletableFuture.allOf(userCF, ordersCF, recCF).join();
return new ProfileView(userCF.join(), ordersCF.join(), recCF.join());Điểm cần đúng:
- 3
supplyAsynctạo trướcallOf— chúng chạy ngay song song. exceptionallytrênrecCFriêng — không ảnh hưởng userCF và ordersCF.allOf(...).join()chờ cả 3 xong. Sau đó 3join()không block (đã done).- Dùng executor riêng (virtual thread) — không starvation commonPool.
Cải thiện: nếu user hoặc orders fail, nên propagate exception (không fallback). allOf sẽ fail → join() throw CompletionException wrapping root cause. Caller catch và trả 500/503.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên