Back to Communication: Sync, Async, gRPC
Q6Question 6 of 8

Design pattern for synchronous communication between microservices

Key Concepts
  • The API Gateway pattern is the single synchronous entry point that routes, authenticates, rate-limits, and aggregates calls to services.
  • Service Discovery resolves logical service names to live instances so the gateway does not hardcode addresses.
  • Client-side load balancing spreads synchronous calls across healthy instances of each service.
  • The Circuit Breaker pattern fails fast when a downstream service is unhealthy, preventing cascading failures.
  • Backend-for-Frontend (BFF) is a variant where a dedicated gateway is tailored to each client type.
  • The gateway must stay thin: it handles cross-cutting concerns but never business logic.
  • Request aggregation lets the gateway call multiple services and merge responses into one payload for the client.

Definition

The API Gateway pattern is the single synchronous entry point that routes, authenticates, rate-limits, and aggregates calls to services, supported by service discovery, load balancing, and circuit breakers.
  • API Gateway – a single entry point in front of all services that handles routing, authentication, rate limiting, request aggregation, and protocol translation so clients make one synchronous call instead of many.
  • Service Discovery – the gateway resolves logical names like payment-service to live instances through a registry like Eureka, removing hardcoded addresses.
  • Client-side Load Balancing – the gateway spreads calls across healthy instances so no single instance is overloaded.
  • Circuit Breaker – wraps each downstream synchronous call so that when a service is unhealthy the gateway fails fast with a fallback instead of hanging.
  • Backend-for-Frontend (BFF) – a variant where a dedicated gateway is tailored to each client type, so mobile gets a lean payload and web gets a richer one.

At National Bank of India, the API Gateway is the single reception desk at the front of the bank. Instead of Rajesh running from the accounts counter to the payments counter to the cards counter himself, he hands one request to reception. Reception checks his ID once, limits how many requests flood in per minute, and goes to the right counters on his behalf, bringing back a combined answer. Reception also knows which counter is open today (service discovery), sends him to whichever teller is free (load balancing), and if a counter is closed tells him quickly instead of making him wait forever (circuit breaker).

Understanding the Concepts

What is the API Gateway pattern?

The API Gateway pattern places a single reverse-proxy service in front of all backend microservices. Clients send every request to this one address, and the gateway routes each request to the correct service while handling cross-cutting concerns like authentication, rate limiting, and aggregation. The client never needs to know the internal topology.

JAVA
package com.bank.gateway;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayRoutes {

    @Bean
    public RouteLocator bankRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("accounts", r -> r
                        .path("/api/v1/accounts/**")
                        .uri("lb://account-service"))
                .route("payments", r -> r
                        .path("/api/v1/payments/**")
                        .uri("lb://payment-service"))
                .route("cards", r -> r
                        .path("/api/v1/cards/**")
                        .uri("lb://card-service"))
                .build();
    }
}
TEXT
// Client calls: POST /api/v1/payments/transfer
// Gateway routes to: lb://payment-service/api/v1/payments/transfer
// "lb://" means resolve via load balancer + service discovery

How does service discovery support the gateway?

Service Discovery means the gateway resolves lb://payment-service through a registry like Eureka to a live instance rather than a hardcoded address. This matters because instances scale up and down dynamically.

JAVA
package com.bank.gateway;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
YAML
# application.yml
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://eureka-server:8761/eureka/
TEXT
// Eureka registry at startup:
// account-service: [192.168.1.10:8081, 192.168.1.11:8081, 192.168.1.12:8081]
// payment-service: [192.168.1.20:8082, 192.168.1.21:8082]
// Gateway resolves "lb://account-service" to one of the three instances

How does the circuit breaker protect synchronous calls?

The Circuit Breaker wraps each downstream synchronous call. When the payment service is unhealthy, the gateway fails fast and returns a fallback instead of hanging and exhausting its own threads.

JAVA
package com.bank.gateway;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ResilientRoutes {

    @Bean
    public RouteLocator resilientBankRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("payments-resilient", r -> r
                        .path("/api/v1/payments/**")
                        .filters(f -> f
                                .circuitBreaker(cb -> cb
                                        .setName("paymentCircuitBreaker")
                                        .setFallbackUri("forward:/fallback/payments")))
                        .uri("lb://payment-service"))
                .build();
    }
}
JAVA
package com.bank.gateway;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

@RestController
public class FallbackController {

    @GetMapping("/fallback/payments")
    public Map<String, String> paymentFallback() {
        return Map.of(
                "status", "SERVICE_UNAVAILABLE",
                "message", "Payment service is temporarily unavailable. Please retry.");
    }
}
DIAGRAM
// Normal: Payment Service responds in 5ms → gateway forwards response
// Failure: Payment Service down → circuit OPEN → fallback in <1ms
// Recovery: After 30s → circuit HALF_OPEN → test call → if OK → CLOSED

How does request aggregation work?

A mobile dashboard needs account, payment, and card data. Without a gateway the app would make three round trips. With the gateway, the app makes one call and the gateway fans out to three services and merges the results.

JAVA
package com.bank.gateway;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;

@RestController
public class DashboardAggregator {

    private final WebClient webClient;

    public DashboardAggregator(WebClient.Builder builder) {
        this.webClient = builder.build();
    }

    @GetMapping("/api/v1/dashboard/{customerId}")
    public Mono<Map<String, Object>> getDashboard(
            @PathVariable String customerId) {

        Mono<Object> account = webClient.get()
                .uri("http://account-service/api/v1/accounts/{id}", customerId)
                .retrieve().bodyToMono(Object.class)
                .timeout(Duration.ofMillis(300))
                .onErrorReturn(Map.of("status", "unavailable"));

        Mono<Object> payments = webClient.get()
                .uri("http://payment-service/api/v1/payments/{id}/recent", customerId)
                .retrieve().bodyToMono(Object.class)
                .timeout(Duration.ofMillis(300))
                .onErrorReturn(Map.of("status", "unavailable"));

        Mono<Object> cards = webClient.get()
                .uri("http://card-service/api/v1/cards/{id}", customerId)
                .retrieve().bodyToMono(Object.class)
                .timeout(Duration.ofMillis(300))
                .onErrorReturn(Map.of("status", "unavailable"));

        return Mono.zip(account, payments, cards)
                .map(tuple -> Map.of(
                        "account", tuple.getT1(),
                        "payments", tuple.getT2(),
                        "cards", tuple.getT3()));
    }
}
DIAGRAM
// Without gateway: 3 round trips from mobile = 3 x 100ms = 300ms
// With gateway: 1 call, 3 parallel backend calls = max(100ms) = ~100ms
// If card service is slow → falls back to "unavailable" → rest still renders

What is the Backend-for-Frontend (BFF) variant?

BFF runs a dedicated gateway per client type. The mobile gateway returns a lean payload optimized for small screens and slow networks. The web gateway returns a richer payload for desktop browsers. Each BFF knows exactly what its client needs.

JAVA
// Mobile BFF: lean payload
// GET /mobile/api/v1/dashboard/CUST-1001
// Response: {balance: 75000, recentTxnCount: 5}

// Web BFF: rich payload
// GET /web/api/v1/dashboard/CUST-1001
// Response: {balance: 75000, transactions: [...], cards: [...], loans: [...]}

How is the API Gateway different from a service mesh?

The API Gateway handles north-south traffic (external clients to internal services). A service mesh handles east-west traffic (internal service-to-service communication). They are complementary.

DIAGRAM
| Aspect | API Gateway | Service Mesh (Istio, Linkerd) |
|---|---|---|
| Traffic direction | North-south (external → internal) | East-west (internal ↔ internal) |
| Handles | Routing, auth, rate limiting, aggregation | mTLS, retries, circuit breaking, observability |
| Deployment | One or few gateway instances | Sidecar proxy per service instance |
| Business logic | None (thin edge) | None (infrastructure) |

What are common mistakes? (wrong vs correct)

The most dangerous mistake is putting business logic in the gateway. The gateway should handle cross-cutting concerns only.

JAVA
// WRONG: business logic in the gateway
@GetMapping("/api/v1/transfers")
public Mono<TransferResult> transfer(@RequestBody TransferRequest req) {
    // Gateway checking balance, calculating fees, applying rules
    BigDecimal fee = req.amount().multiply(new BigDecimal("0.01"));
    if (fee.compareTo(new BigDecimal("500")) > 0) {
        fee = new BigDecimal("500"); // Business rule in gateway!
    }
    // This turns the gateway into a monolith
}

// CORRECT: gateway only routes, service owns the logic
// Gateway route: /api/v1/transfers → lb://payment-service
// Payment Service handles balance check, fee calculation, business rules

What should a senior engineer know?

A senior engineer keeps the gateway thin and generic. It handles routing, security, aggregation, and resilience, but the actual business rules live in the services. Overloading the gateway with domain logic recreates the coupling microservices were meant to remove. The gateway must also be horizontally scaled because it is a single point of failure by design.

JAVA
// Senior checklist for a healthy gateway:
// 1. Routes: predicate + URI, no business logic
// 2. Auth: JWT validation at the edge, forwarded as trusted context
// 3. Rate limiting: per-client token bucket backed by Redis
// 4. Circuit breaker: per-route with fallback
// 5. Aggregation: simple view composition, not domain orchestration
// 6. Horizontal scaling: multiple replicas behind a load balancer

Visual Overview

DIAGRAM
  API Gateway Pattern
  ────────────────────
  Mobile App    Web App    Partner API
       │           │           │
       └─────┬─────┘───────────┘
             │
             ▼
  ┌─────────────────────────────────┐
  │         API Gateway              │
  │  ┌─────┐ ┌──────┐ ┌──────────┐ │
  │  │Auth │ │Route │ │Rate Limit│ │
  │  └─────┘ └──────┘ └──────────┘ │
  │  ┌─────────┐  ┌───────────────┐│
  │  │Aggregate│  │Circuit Breaker││
  │  └─────────┘  └───────────────┘│
  └──────┬──────────┬──────────┬───┘
         │          │          │
         ▼          ▼          ▼
  ┌──────────┐ ┌─────────┐ ┌────────┐
  │ Account  │ │ Payment │ │  Card  │
  │ Service  │ │ Service │ │Service │
  │ (Eureka) │ │ (Eureka)│ │(Eureka)│
  └──────────┘ └─────────┘ └────────┘


  Supporting Patterns
  ───────────────────
  Service Discovery: Gateway → Eureka → [instance1, instance2, instance3]
  Load Balancing:    Gateway → round-robin → instance2
  Circuit Breaker:   CLOSED → (failures exceed threshold) → OPEN → HALF_OPEN → CLOSED

Key Takeaway

The primary design pattern for synchronous communication between microservices and their clients is the API Gateway pattern. It places a single entry point in front of all the services, so instead of a client making many direct synchronous calls and knowing every internal address, it makes one call to the gateway. The gateway handles routing, authentication, rate limiting, request aggregation, and fault tolerance, all as cross-cutting concerns that would otherwise be duplicated in every service.

At National Bank of India, the mobile app requests Rajesh's dashboard with a single authenticated call. The gateway validates the JWT once, routes the request, fans out to the account, payment, and card services in parallel using Mono.zip, and returns one merged response. When the card service went down, the circuit breaker tripped, the dashboard showed "unavailable" for cards but still rendered account and payment data, and Rajesh never saw a blank error screen.

The gateway relies on three supporting patterns: Service Discovery so it resolves logical names to live instances, client-side load balancing to spread calls across healthy instances, and the circuit breaker to fail fast when a downstream service is unhealthy. For different client types, the Backend-for-Frontend variant runs a tailored gateway per client, so mobile gets a lean payload and web gets a richer one.

Remember one sentence: the API Gateway is the single thin front door that routes, secures, aggregates, and protects, so clients talk to one address and the services behind it can change freely.

---

Share
Use ← → arrow keys to navigate