Pular para o conteúdo

API Composition

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.

O composer executa a consulta em passos:

  1. Chama o serviço-provedor que retorna a lista base (ex.: Pedidos).
  2. Para cada item (ou em lote), chama os demais serviços para enriquecer (Cliente, Entrega).
  3. 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)
  • 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.

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.

  • 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 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.

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
@RequiredArgsConstructor
public 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.

  • 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 getById por 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.
  1. 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.

  1. 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.

  1. 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.

  1. 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.