Careeroza
API Composition
Queries That Need Several Services
The "Order details" screen shows the order, the customer, the product names and images, and the payment status. That data lives in four services and four databases, and you cannot JOIN across them. An API composer (in the gateway, a BFF, or order-service) calls each service and merges the answers.
GET /api/orders/1001/details
|
API composer ---- in parallel ---+-------------------+------------------+
| | | |
order-service user-service product-service payment-service
(60 ms) (40 ms) (80 ms) (50 ms)
| | | |
+--------------- merge into one JSON response <---------------------+
sequential: 60 + 40 + 80 + 50 = 230 ms parallel: max(...) = ~80 msExample: Parallel Composition with a Fallback
Example
Calls run in parallel with CompletableFuture. When payment-service is down, the composer still returns the rest of the page with a fallback value instead of failing the whole request:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
System.out.println(orderDetails(1001, pool, true));
System.out.println(orderDetails(1002, pool, false)); // payment-service is down
pool.shutdown();
}
static String orderDetails(long orderId, ExecutorService pool, boolean paymentUp) {
var order = CompletableFuture.supplyAsync(() -> call(60, "order " + orderId + ", 2 items, Rs.2998"), pool);
var user = CompletableFuture.supplyAsync(() -> call(40, "Asha Rao, Bengaluru 560001"), pool);
var products = CompletableFuture.supplyAsync(() -> call(80, "boAt Rockerz 450 x2"), pool);
var payment = CompletableFuture.supplyAsync(() -> {
if (!paymentUp) throw new IllegalStateException("payment-service timed out");
return call(50, "UPI, CAPTURED");
}, pool)
.exceptionally(ex -> "status unavailable, try again later"); // partial result
CompletableFuture.allOf(order, user, products, payment).join();
return """
---- order details %d ----
order : %s
customer : %s
items : %s
payment : %s
""".formatted(orderId, order.join(), user.join(), products.join(), payment.join());
}
static String call(int latencyMs, String data) {
try { Thread.sleep(latencyMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return data;
}
}Output
---- order details 1001 ---- order : order 1001, 2 items, Rs.2998 customer : Asha Rao, Bengaluru 560001 items : boAt Rockerz 450 x2 payment : UPI, CAPTURED ---- order details 1002 ---- order : order 1002, 2 items, Rs.2998 customer : Asha Rao, Bengaluru 560001 items : boAt Rockerz 450 x2 payment : status unavailable, try again later
Composer in Spring Boot
@RestController
@RequiredArgsConstructor
public class OrderDetailsController {
private final OrderClient orders; // RestClient / Feign clients with timeouts + Resilience4j
private final UserClient users;
private final ProductClient products;
private final PaymentClient payments;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
@GetMapping("/api/orders/{id}/details")
public OrderDetails details(@PathVariable long id) {
OrderDto order = orders.get(id); // needed first: has userId + productIds
var user = CompletableFuture.supplyAsync(() -> users.get(order.userId()), executor);
var items = CompletableFuture.supplyAsync(() -> products.getAll(order.productIds()), executor); // one batch call
var payment = CompletableFuture.supplyAsync(() -> payments.forOrder(id), executor)
.completeOnTimeout(PaymentDto.unknown(), 800, TimeUnit.MILLISECONDS)
.exceptionally(ex -> PaymentDto.unknown());
return new OrderDetails(order, user.join(), items.join(), payment.join());
}
}- Use batch endpoints (
GET /api/products?ids=1,2,3) instead of one call per item (an N+1 problem across the network). - Give every call a timeout and decide what is essential (order) and what may be degraded (payment status, recommendations).
- When composition becomes slow or complex (big lists, filtering and sorting across services), move to CQRS with a read model.