API Composition
O problema no contexto distribuído
Seção intitulada “O problema no contexto distribuído”Em um monólito, “listar pedidos com dados do cliente e status de entrega” é um JOIN entre três tabelas. Em Microservices com database-per-service, Order, Customer e Delivery têm bancos privados e o JOIN não existe. Precisamos compor os dados fora do banco.
A solução: o composer
Seção intitulada “A solução: o composer”O composer executa a consulta em passos:
- Chama o serviço-provedor que retorna a lista base (ex.: Pedidos).
- Para cada item (ou em lote), chama os demais serviços para enriquecer (Cliente, Entrega).
- Faz o join em memória e devolve o DTO agregado.
O composer pode ser um API Gateway, um BFF (Backend for Frontend) ou um serviço de composição dedicado. Quando as chamadas são independentes, devem ser paralelizadas para não somar latências.
sequenceDiagram
participant Client
participant Composer as API Composer
participant Ord as Pedidos
participant Cust as Clientes
participant Del as Entregas
Client->>Composer: GET /orders/{id}/details
Composer->>Ord: getOrder(id)
Ord-->>Composer: Order (contém custId)
par chamadas paralelas
Composer->>Cust: getCustomer(custId)
Composer->>Del: getDelivery(id)
end
Cust-->>Composer: Customer
Del-->>Composer: Delivery
Composer->>Composer: join em memória
Composer-->>Client: OrderDetails (agregado)
Variações
Seção intitulada “Variações”- In-memory join simples: poucos serviços, chamadas paralelas, agrega e retorna.
- Composição com paginação/filtro no provedor: o serviço-provedor filtra/pagina; os demais só enriquecem o subconjunto.
- Batch/bulk endpoints: para evitar N+1, o serviço enriquecedor expõe
getByIds([...])em vez de N chamadas unitárias.
Contraste com CQRS
Seção intitulada “Contraste com CQRS”API Composition compõe on-read (na hora da consulta). A alternativa é CQRS: manter um read model materializado — uma projeção denormalizada, atualizada de forma assíncrona por Domain Events — que já contém os dados agregados. A consulta vira uma leitura única e barata.
| Critério | API Composition | CQRS (read model) |
|---|---|---|
| Latência de leitura | Somada / paralela entre serviços | Baixa (leitura única) |
| Frescor dos dados | Sempre atual | Eventualmente consistente |
| Complexidade | Baixa | Alta (projeção + sincronização) |
| Fan-out / N+1 | Risco alto | Eliminado |
| Custo de escrita | Nenhum extra | Manter a projeção |
Regra prática: consulta fria e dados voláteis → composition; consulta quente com fan-out grande → CQRS.
Trade-offs
Seção intitulada “Trade-offs”- Prós: simples, sem duplicação de dados, sempre reflete o estado atual dos serviços.
- Contras:
- Latência: a resposta é tão lenta quanto a chamada mais lenta (se paralelo) ou a soma (se serial).
- Disponibilidade reduzida: depende de todos os serviços online; combine com Retry, Fallback e Circuit Breaker.
- Problema N+1: buscar a lista e depois uma chamada por item multiplica as requisições. Resolva com bulk endpoints.
- Joins grandes em memória podem estourar memória do composer.
Quando NÃO usar
Seção intitulada “Quando NÃO usar”- Quando a agregação exige join de grandes volumes (milhares de itens cruzados) — inviável em memória; use CQRS.
- Quando a consulta é muito quente e a latência somada é inaceitável — materialize um read model.
- Quando os dados agregados são naturais de um único serviço — não há composição a fazer.
Exemplo Java/Spring
Seção intitulada “Exemplo Java/Spring”Composer com chamadas paralelas via CompletableFuture e bulk para evitar N+1.
public record OrderDetails( UUID orderId, BigDecimal total, String status, String customerName, String deliveryStatus) {}
@Service@RequiredArgsConstructorpublic class OrderDetailsComposer {
private final OrderClient orderClient; private final CustomerClient customerClient; private final DeliveryClient deliveryClient; private final Executor executor;
public OrderDetails compose(UUID orderId) { var orderF = CompletableFuture.supplyAsync( () -> orderClient.getOrder(orderId), executor); var deliveryF = CompletableFuture.supplyAsync( () -> deliveryClient.getDelivery(orderId), executor);
var order = orderF.join();
var customer = customerClient.getCustomer(order.customerId()); var delivery = deliveryF.join();
return new OrderDetails( order.id(), order.total(), order.status(), customer.name(), delivery.status()); }
public List<OrderDetails> composeForCustomer(UUID customerId) { var orders = orderClient.getOrdersByCustomer(customerId);
var deliveryIds = orders.stream().map(OrderDto::id).toList(); var deliveries = deliveryClient.getDeliveries(deliveryIds); var customer = customerClient.getCustomer(customerId);
return orders.stream() .map(o -> new OrderDetails( o.id(), o.total(), o.status(), customer.name(), deliveries.get(o.id()).status())) .toList(); }}composeForCustomer evita N+1: busca todas as entregas com um getDeliveries(ids) em vez de uma chamada por pedido.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Chamadas seriais quando poderiam ser paralelas: latência que cresce linearmente com o número de dependências.
- Problema N+1 ignorado: um loop chamando
getByIdpor item. Exponha e use bulk endpoints. - Falta de resiliência: sem Circuit Breaker/Fallback, uma dependência lenta derruba a composição inteira.
- Composer gordo com regra de negócio: o composer deve orquestrar e agregar, não implementar domínio.
- Join de volume massivo em memória: sinal de que o caso pede CQRS, não composition.
Relações
Seção intitulada “Relações”- Alternativa: CQRS com read model materializado, alimentado por Domain Events.
- Precisa de resiliência: Retry, Fallback, Circuit Breaker.
- Frequentemente exposto via APIs - REST, GraphQL e gRPC (GraphQL é, em essência, um composer).
- Índice: Microservices Patterns.
Perguntas de revisão
Seção intitulada “Perguntas de revisão”- Por que API Composition existe apenas em arquiteturas de microsserviços?
Resposta
Porque o padrão database-per-service remove os JOINs entre tabelas de serviços diferentes: os dados que antes se cruzavam no banco agora vivem em bancos privados isolados. A composição refaz esse join em nível de aplicação, buscando de cada serviço e juntando em memória.
- O que é o problema N+1 nesse contexto e como mitigá-lo?
Resposta
É buscar uma lista de N itens (1 chamada) e então fazer 1 chamada por item para enriquecer, resultando em N+1 requisições de rede. Mitiga-se expondo bulk endpoints (ex.: getByIds([...])) que trazem todos os dados de enriquecimento em uma única chamada.
- Quando prefira CQRS a API Composition?
Resposta
Quando a consulta é quente (alto volume), o fan-out entre serviços é grande, o join de dados é volumoso demais para memória, ou a latência somada é inaceitável. O CQRS materializa um read model denormalizado atualizado por eventos, tornando a leitura única e barata — ao custo de consistência eventual e da complexidade de manter a projeção.
- Como a composição paralela ajuda na latência e qual seu limite?
Resposta
Chamando serviços independentes em paralelo (ex.: CompletableFuture), a latência total tende à da chamada mais lenta, não à soma. O limite é a dependência de dados: se a chamada B precisa do resultado de A (ex.: pegar customerId do pedido antes de buscar o cliente), esse trecho é inerentemente serial.