• Shift Elevate
  • Posts
  • Gateway Aggregation Pattern: Consolidating Multiple Service Calls into Single Requests

Gateway Aggregation Pattern: Consolidating Multiple Service Calls into Single Requests

In distributed systems, clients often need to make multiple service calls to gather all required data, leading to increased complexity, latency, and network overhead. The Gateway Aggregation pattern provides a solution by consolidating multiple service calls into a single request through an API gateway, reducing client complexity and improving performance.

This pattern is essential for modern microservices architectures where clients need data from multiple services to render a single view. By implementing intelligent aggregation strategies, you can significantly reduce client complexity while maintaining system performance and reliability.

Gateway Aggregation

This guide walks you through the Gateway Aggregation pattern from concept to practical implementation, covering parallel service calls, response aggregation, and handling multiple service requests.

Understanding the Gateway Aggregation Pattern

The Gateway Aggregation pattern uses an API gateway to collect data from multiple backend services and combine them into a single response. This reduces the number of round trips between clients and services, improving performance and simplifying client-side logic.

Core Architecture

Gateway Aggregation Interactions

Key Benefits

  • Reduced Client Complexity: Clients make fewer requests and handle less data aggregation logic.

  • Improved Performance: Parallel service calls reduce overall response time.

  • Reduced Network Overhead: Fewer round trips between clients and services.

  • Better Error Handling: Track errors from individual services and return partial results.

  • Consistent Data Format: Standardized response format across all aggregated endpoints.

Implementing the Gateway Aggregation Pattern

Let's build a gateway aggregation system that demonstrates the core pattern with parallel service calls and response aggregation.

Implementation Overview

  • AggregationGateway: Core gateway managing service calls, aggregation rules, and parallel execution

  • Service Clients: Mock implementations for calling backend services

  • Configuration Models: Simple data structures for rules, requests, and responses

  • E-commerce Example: Practical dashboard aggregation demonstration

Note on Implementation

This implementation uses CompletableFuture for parallel service calls, providing non-blocking aggregation. The gateway executes multiple service calls concurrently and waits for all results before combining them into a single response. For production systems, consider using reactive frameworks like Project Reactor or RxJava for better resource utilization and backpressure handling.

Core Aggregation Gateway Implementation

Gateway Class Structure

The core gateway manages service calls and aggregation rules:

public class AggregationGateway {
    private final Map<String, AggregationRule> aggregationRules = new ConcurrentHashMap<>();
    private final ExecutorService executorService;

    public AggregationGateway() {
        this.executorService = Executors.newFixedThreadPool(20);
    }

    public void registerRule(AggregationRule rule) {
        aggregationRules.put(rule.endpoint(), rule);
    }

    public void shutdown() {
        executorService.shutdown();
    }
}

Processing Requests and Parallel Execution

The gateway processes requests by executing multiple service calls in parallel:

public AggregatedResponse processRequest(String endpoint, Map<String, Object> requestData) {
    AggregationRule rule = aggregationRules.get(endpoint);
    if (rule == null) {
        return new AggregatedResponse(false, "Unknown endpoint: " + endpoint, null);
    }

    // Start all service calls in parallel
    List<CompletableFuture<ServiceResponse>> futures = new ArrayList<>();
    for (ServiceCall call : rule.serviceCalls()) {
        futures.add(CompletableFuture.supplyAsync(() ->
            callService(call, requestData), executorService));
    }

    // Wait for all calls to complete
    try {
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .get(rule.timeoutMs(), TimeUnit.MILLISECONDS);
    } catch (Exception e) {
        System.err.println("Some services timed out");
    }

    // Collect results
    Map<String, Object> aggregatedData = new HashMap<>();
    List<ServiceError> errors = new ArrayList<>();

    for (int i = 0; i < futures.size(); i++) {
        ServiceCall call = rule.serviceCalls().get(i);
        try {
            ServiceResponse response = futures.get(i).get(100, TimeUnit.MILLISECONDS);
            if (response.success()) {
                aggregatedData.put(call.responseKey(), response.data());
            } else {
                errors.add(new ServiceError(call.serviceName(), response.errorMessage()));
            }
        } catch (Exception e) {
            errors.add(new ServiceError(call.serviceName(), "Service unavailable"));
        }
    }

    boolean success = errors.isEmpty();
    return new AggregatedResponse(success,
        success ? "Success" : "Partial success", aggregatedData, errors);
}

private ServiceResponse callService(ServiceCall call, Map<String, Object> requestData) {
    try {
        // Simulate service call with mock data
        Thread.sleep(100 + (long)(Math.random() * 200));

        // Simulate occasional failures
        if (Math.random() < 0.1) {
            return new ServiceResponse(false, call.serviceName(), "Service error", null);
        }

        // Return mock response
        return new ServiceResponse(true, call.serviceName(), null,
            Map.of("service", call.serviceName(), "data", "mock data"));
    } catch (Exception e) {
        return new ServiceResponse(false, call.serviceName(), e.getMessage(), null);
    }
}

Configuration and Response Models

Simple data structures for the gateway:

// Aggregation rule defines which services to call
record AggregationRule(String endpoint, List<ServiceCall> serviceCalls, long timeoutMs) {}

// Individual service call configuration
record ServiceCall(String serviceName, String responseKey) {}

// Aggregated response from gateway
public class AggregatedResponse {
    private final boolean success;
    private final String message;
    private final Map<String, Object> data;
    private final List<ServiceError> errors;

    public AggregatedResponse(boolean success, String message, Map<String, Object> data) {
        this(success, message, data, new ArrayList<>());
    }

    public AggregatedResponse(boolean success, String message, Map<String, Object> data,
                             List<ServiceError> errors) {
        this.success = success;
        this.message = message;
        this.data = data != null ? new HashMap<>(data) : new HashMap<>();
        this.errors = errors != null ? new ArrayList<>(errors) : new ArrayList<>();
    }

    public boolean isSuccess() { return success; }
    public String getMessage() { return message; }
    public Map<String, Object> getData() { return data; }
    public List<ServiceError> getErrors() { return errors; }
}

// Service response and error records
record ServiceResponse(boolean success, String serviceName, String errorMessage, Object data) {}
record ServiceError(String serviceName, String errorMessage) {}

When to Use Gateway Aggregation Pattern

Understanding when to apply the Gateway Aggregation pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:

āœ… Ideal Scenarios:

  • Clients need data from multiple services to render a single view (dashboards, profiles, etc.).

  • You want to reduce network round trips and latency for mobile or low-bandwidth clients.

  • Different client types need different data aggregations from the same backend services.

  • You need to simplify client-side logic by moving complexity to the gateway.

  • You want to provide backward compatibility while refactoring monoliths to microservices.

āŒ Skip It When:

  • You only have simple single-service requests with no aggregation needs.

  • Services have vastly different SLAs making aggregation unreliable.

  • Real-time consistency across all services is critical and aggregation adds unacceptable latency.

  • The added complexity of maintaining aggregation rules doesn't justify the client-side simplification.

Best Practices

Set Appropriate Timeouts: Critical services need shorter timeouts, while non-critical services can tolerate longer ones.

Handle Partial Failures: Return partial results when some services fail. If notifications fail, still return user profile and orders.

Use Parallel Execution: Execute independent service calls concurrently. Use sequential calls only when services have dependencies.

Keep It Simple: Start with basic aggregation and parallel execution. Focus on the core pattern before adding complexity.

Found this helpful? Share it with a colleague who's struggling with client complexity in their microservices architecture. Got questions? We'd love to hear from you at [email protected]