Skip to content
Published on

API Gateway Patterns and BFF Design: Practical Implementation with Kong, Envoy, and GraphQL Federation

Authors
API Gateway BFF

Introduction

As microservices architecture has become widespread across industries, managing the complexity of inter-service communication has become a core challenge for every engineering organization. The global API management market has grown to approximately $5.1 billion as of 2026, with an explosive CAGR of 32.3%. As of 2025, 31% of organizations operate multiple API gateways, with 11% of them running three or more gateways in parallel.

The API gateway pattern is an architectural pattern that places a single entry point between clients and backend services to centralize cross-cutting concerns such as routing, authentication, rate limiting, and protocol translation. When combined with the BFF (Backend for Frontend) pattern, it can provide dedicated backends optimized for each frontend type including web, mobile, and IoT.

This article examines the core principles of the API gateway pattern and BFF pattern, covering practical configuration methods for three major tools: Kong Gateway, Envoy Proxy, and GraphQL Federation. It provides everything needed for production operations, including authentication/authorization integration, rate limiting, circuit breakers, and observability setup, all with practical code examples.

Understanding the API Gateway Pattern

Role of the Gateway

The API gateway serves as the single entry point for all client requests in a microservices architecture. It abstracts the complexity of internal service topology, providing clients with a clean and consistent interface. Its core responsibilities are as follows.

  • Request Routing: Forwarding traffic to appropriate backend services based on URL paths, headers, and methods
  • Protocol Translation: Converting between protocols such as REST-to-gRPC, HTTP-to-WebSocket
  • Response Aggregation: Combining responses from multiple services and returning them to the client (API Composition)
  • Cross-Cutting Concerns: Centrally managing authentication, authorization, rate limiting, logging, caching, CORS, etc.
  • Service Discovery: Dynamically discovering service instances and performing load balancing

Gateway Topology Architecture

                          API 게이트웨이 토폴로지

    ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
Web App │  │Mobile App│Partner  │  │IoT Device│
     (React) (Swift)  (B2B) (MQTT)    └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘
         │             │             │              │
         └──────┬──────┴──────┬──────┘              │
                │             │                     │
         ┌──────▼──────┐ ┌───▼──────────┐   ┌──────▼──────┐
Edge LB   │ │  Edge LB     │   │  Edge LB           (L4/L7)  (L4/L7)  (L4/L7)         └──────┬──────┘ └──────┬───────┘   └──────┬──────┘
                │               │                  │
         ┌──────▼───────────────▼──────────────────▼──────┐
API Gateway Cluster         │  ┌──────────────────────────────────────────┐   │
         │  │  AuthRate LimitLoggingTransform │   │
         │  └──────────────────────────────────────────┘   │
         │  ┌─────────────────────────────────────────┐    │
         │  │        Route Matching & Dispatch         │    │
         │  └─────────────────────────────────────────┘    │
         └───────┬──────────┬──────────┬──────────┬───────┘
                 │          │          │          │
          ┌──────▼───┐┌────▼────┐┌────▼────┐┌───▼──────┐
User Svc ││Order Svc││Pay Svc  ││Notify Svc│
           (gRPC)   ││ (REST)  ││ (REST)  ││(WebSocket│
          └──────────┘└─────────┘└─────────┘└──────────┘

Edge Gateway vs Internal Gateway

In practice, gateways are deployed in a hierarchical manner. The edge gateway serves as the entry point for external traffic, performing DDoS defense, TLS termination, and aggressive rate limiting. The internal gateway manages inter-service communication, handling service discovery, mTLS, and fine-grained access control.

    외부 트래픽
    ┌───▼────────────────────────┐
Edge Gateway (Kong)      │  ← TLS 종료, WAF, 레이트 리미팅
- DDoS 방어              │
- JWT 검증               │
- 글로벌 레이트 리미팅    │
    └───────────┬────────────────┘
    ┌───────────▼────────────────┐
Internal Gateway (Envoy)  │  ← mTLS, 서비스 라우팅
- mTLS 상호 인증         │
- 서비스 디스커버리       │
- 세밀한 RBAC    └──┬────────┬────────┬───────┘
       │        │        │
    ┌──▼──┐  ┌──▼──┐  ┌──▼──┐
    │Svc A│  │Svc B│  │Svc C    └─────┘  └─────┘  └─────┘

BFF (Backend for Frontend) Pattern

Why the BFF Pattern Is Needed

When serving all clients through a single API gateway, you inevitably face the "one size doesn't fit all" problem. Web applications want to fetch rich data all at once, while mobile apps need only minimal fields to save bandwidth. IoT devices expect binary protocols, and third-party partners expect stable REST APIs.

The BFF pattern is an architectural pattern proposed by Sam Newman that solves this problem by providing a dedicated backend for each frontend type. A BFF is closely coupled to a specific user experience, and the team responsible for the UI also manages the BFF.

BFF Architecture

    ┌──────────┐     ┌──────────┐     ┌──────────┐
Web App │     │Mobile App│Admin App│
     (React) (Flutter) (Vue.js)    └────┬─────┘     └────┬─────┘     └────┬─────┘
         │                │                │
    ┌────▼─────┐     ┌────▼─────┐     ┌────▼─────┐
Web BFF  │     │Mobile BFF│     │Admin BFF    (Node.js)  (Go)(Node.js)    │          │     │          │     │          │
- 풍부한  │     │- 경량    │     │- 대시보드│
    │  데이터  │     │  페이로드│     │  집계    │
- SSR 지원│     │- 오프라인│     │- 벌크    │
- SEO     │     │  캐싱    │     │  연산    │
    └──┬───┬───┘     └──┬───┬───┘     └──┬───┬───┘
       │   │            │   │            │   │
       │   └────────┬───┘   └────────┬───┘   │
       │            │                │       │
    ┌──▼──┐     ┌───▼──┐         ┌───▼──┐   │
    │User │     │Order │         │Admin │◄──┘
Svc │     │ Svc  │         │ Svc    └─────┘     └──────┘         └──────┘

BFF Implementation Example: Node.js Web BFF

// web-bff/src/server.ts
import express from 'express'
import axios from 'axios'

const app = express()

// Web BFF: 대시보드를 위한 데이터 집계 엔드포인트
app.get('/api/dashboard', async (req, res) => {
  const userId = req.headers['x-user-id'] as string

  try {
    // 여러 마이크로서비스에서 병렬로 데이터 수집
    const [userProfile, recentOrders, notifications, analytics] = await Promise.all([
      axios.get(`${process.env.USER_SERVICE_URL}/users/${userId}`),
      axios.get(`${process.env.ORDER_SERVICE_URL}/orders?userId=${userId}&limit=10`),
      axios.get(`${process.env.NOTIFICATION_SERVICE_URL}/notifications/${userId}?unread=true`),
      axios.get(`${process.env.ANALYTICS_SERVICE_URL}/users/${userId}/summary`),
    ])

    // Web 프론트엔드에 최적화된 응답 구조
    res.json({
      user: {
        name: userProfile.data.name,
        email: userProfile.data.email,
        avatar: userProfile.data.avatarUrl,
        memberSince: userProfile.data.createdAt,
      },
      orders: {
        recent: recentOrders.data.items.map((order: any) => ({
          id: order.id,
          status: order.status,
          total: order.totalAmount,
          date: order.createdAt,
          itemCount: order.items.length,
          // 웹에서만 필요한 상세 정보 포함
          trackingUrl: order.trackingUrl,
          invoice: order.invoiceUrl,
        })),
        totalCount: recentOrders.data.totalCount,
      },
      notifications: {
        unreadCount: notifications.data.count,
        items: notifications.data.items.slice(0, 5),
      },
      analytics: {
        totalSpent: analytics.data.totalSpent,
        orderFrequency: analytics.data.orderFrequency,
        favoriteCategories: analytics.data.topCategories,
      },
    })
  } catch (error) {
    // 부분 실패 시 가용한 데이터만 반환 (Graceful Degradation)
    res.status(207).json({
      partial: true,
      error: 'Some services are unavailable',
      available: {},
    })
  }
})

// Mobile BFF와의 차이점: 웹은 SSR을 위한 전체 데이터를 반환
// Mobile BFF는 동일 엔드포인트에서 경량 필드만 반환
app.listen(3001, () => {
  console.log('Web BFF running on port 3001')
})

Core BFF Design Principles

  1. One BFF corresponds to one frontend: If a web BFF starts handling mobile requirements, it becomes a "multi-purpose BFF" and regresses to the original problem.
  2. BFF should not contain business logic: It only performs data aggregation, transformation, and formatting; business rules must reside in downstream services.
  3. BFF team and frontend team should be the same team: It is most efficient when frontend developers directly manage their own BFF.
  4. BFF as a security boundary: Especially in SPA environments, the BFF handles token negotiation for OIDC/OAuth 2.0 flows as a confidential client, eliminating the security risks of public clients.

Gateway Tool Comparison

Comparing major API gateway solutions from a production operations perspective.

CategoryKong GatewayEnvoy ProxyAWS API GatewayGraphQL Federation
TechnologyNGINX + LuaC++ (Custom built)AWS 관리형Apollo Router (Rust)
DeploymentSelf-hosted / KonnectSelf-hosted / SidecarFully managedSelf-hosted / GraphOS
ProtocolsHTTP, gRPC, WebSocketHTTP/1.1, HTTP/2, gRPC, TCPREST, WebSocket, HTTPGraphQL
Performance~50,000 TPS/노드~100,000+ TPS/노드AWS 내부 최적화Proportional to subgraph count
ExtensibilityPlugin (Lua, Go)Filter (C++, Wasm, Lua)Lambda 연동Subgraph services
K8s IntegrationKIC (Gateway API 준수)Envoy Gateway / IstioEKS 통합Helm Chart
ObservabilityPrometheus, DatadogOpenTelemetry 네이티브CloudWatchApollo Studio
AuthPlugin (JWT, OAuth)Filter (JWT, ext_authz)Cognito, Lambda AuthSubgraph delegation
Rate LimitingPlugin (Local/Global)Filter (Local/Global)Built-in throttlingCustom implementation required
LicenseApache 2.0 / EnterpriseApache 2.0Pay-per-useElastic License
Learning CurveMediumHighLowHigh
Best ForGeneral API managementService mesh / L7 controlAWS 네이티브Multi-team graph integration

Selection Guide

  • Kong: When you need a rich plugin ecosystem and GUI management tools. It offers over 60 official plugins. However, note that core features like OIDC and advanced analytics are included in the Enterprise license.
  • Envoy: When you need a high-performance L7 proxy or integration with service meshes (Istio, Consul). Dynamic configuration via xDS API is a strength, but configuration complexity is high.
  • AWS API Gateway: When you want to minimize management overhead in an AWS-native environment. Lambda integration is powerful, but AWS lock-in and cold start latency are drawbacks.
  • GraphQL Federation: When multiple teams independently contribute to graphs and the frontend needs flexible queries. The initial investment in schema design is significant, but frontend productivity is maximized in the long run.

Kong Gateway Production Configuration

Kong Installation and Basic Configuration (Kubernetes)

Kong Ingress Controller (KIC) supports the Kubernetes Gateway API as a first-class citizen and is the first gateway to pass 100% of the Gateway API core conformance tests.

# kong-values.yaml - Helm Chart 설정
# helm install kong kong/ingress -f kong-values.yaml -n kong-system

gateway:
  image:
    repository: kong/kong-gateway
    tag: '3.9'
  env:
    database: 'off' # DB-less 모드 (선언적 설정)
    proxy_access_log: /dev/stdout
    admin_access_log: /dev/stdout
    proxy_error_log: /dev/stderr
    admin_error_log: /dev/stderr
    plugins: bundled,oidc,prometheus
  proxy:
    type: LoadBalancer
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-type: nlb
      service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    tls:
      enabled: true
  admin:
    enabled: true
    type: ClusterIP # 클러스터 내부에서만 접근
  resources:
    requests:
      cpu: 500m
      memory: 512Mi
    limits:
      cpu: 2000m
      memory: 2Gi

controller:
  image:
    repository: kong/kubernetes-ingress-controller
    tag: '3.4'
  ingressClass: kong
  resources:
    requests:
      cpu: 250m
      memory: 256Mi

Kong Service and Routing Configuration

# kong-gateway-api.yaml
# Kubernetes Gateway API를 사용한 선언적 라우팅 구성
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: kong-gateway
  namespace: kong-system
  annotations:
    konghq.com/gateway-operator: 'true'
spec:
  gatewayClassName: kong
  listeners:
    - name: http
      protocol: HTTP
      port: 80
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - name: api-tls-cert
            kind: Secret
---
# HTTPRoute로 서비스별 라우팅 정의
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: user-service-route
  namespace: default
  annotations:
    konghq.com/plugins: rate-limiting-global,jwt-auth,cors
spec:
  parentRefs:
    - name: kong-gateway
      namespace: kong-system
  hostnames:
    - 'api.example.com'
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api/v1/users
      backendRefs:
        - name: user-service
          port: 8080
          weight: 100
    - matches:
        - path:
            type: PathPrefix
            value: /api/v1/orders
      backendRefs:
        - name: order-service
          port: 8080
          weight: 90
        - name: order-service-canary
          port: 8080
          weight: 10 # 카나리 배포: 10% 트래픽
---
# Kong 플러그인: JWT 인증
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: jwt-auth
  namespace: default
config:
  key_claim_name: iss
  claims_to_verify:
    - exp
  header_names:
    - Authorization
plugin: jwt
---
# Kong 플러그인: 레이트 리미팅
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: rate-limiting-global
  namespace: default
config:
  minute: 100
  hour: 5000
  policy: redis
  redis:
    host: redis-master.redis-system.svc.cluster.local
    port: 6379
    database: 0
    timeout: 2000
  fault_tolerant: true # Redis 장애 시에도 요청 통과
  hide_client_headers: false
plugin: rate-limiting
---
# Kong 플러그인: CORS
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: cors
  namespace: default
config:
  origins:
    - 'https://app.example.com'
    - 'https://admin.example.com'
  methods:
    - GET
    - POST
    - PUT
    - DELETE
    - OPTIONS
  headers:
    - Authorization
    - Content-Type
    - X-Request-ID
  exposed_headers:
    - X-RateLimit-Remaining
  max_age: 3600
  credentials: true
plugin: cors

Envoy Proxy Configuration

Envoy Basic Configuration

Envoy is a high-performance C++-based proxy that natively supports HTTP/2 and gRPC at the L7 layer. Dynamic configuration updates via the xDS API are its core strength.

# envoy-config.yaml
# Envoy 프록시 정적 설정 (프로덕션에서는 xDS 동적 설정 사용 권장)
admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

static_resources:
  listeners:
    - name: api_listener
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8443
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                codec_type: AUTO
                # 액세스 로그 설정
                access_log:
                  - name: envoy.access_loggers.file
                    typed_config:
                      '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                      path: /dev/stdout
                      log_format:
                        json_format:
                          timestamp: '%START_TIME%'
                          method: '%REQ(:METHOD)%'
                          path: '%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%'
                          protocol: '%PROTOCOL%'
                          response_code: '%RESPONSE_CODE%'
                          duration_ms: '%DURATION%'
                          upstream_host: '%UPSTREAM_HOST%'
                          request_id: '%REQ(X-REQUEST-ID)%'
                # HTTP 필터 체인
                http_filters:
                  # JWT 인증 필터
                  - name: envoy.filters.http.jwt_authn
                    typed_config:
                      '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
                      providers:
                        auth0_provider:
                          issuer: 'https://your-tenant.auth0.com/'
                          audiences:
                            - 'https://api.example.com'
                          remote_jwks:
                            http_uri:
                              uri: 'https://your-tenant.auth0.com/.well-known/jwks.json'
                              cluster: auth0_jwks
                              timeout: 5s
                            cache_duration: 600s
                          forward: true
                          payload_in_metadata: jwt_payload
                      rules:
                        - match:
                            prefix: /api/
                          requires:
                            provider_name: auth0_provider
                        - match:
                            prefix: /health
                  # 레이트 리미팅 필터
                  - name: envoy.filters.http.local_ratelimit
                    typed_config:
                      '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
                      stat_prefix: http_local_rate_limiter
                      token_bucket:
                        max_tokens: 1000
                        tokens_per_fill: 100
                        fill_interval: 1s
                      filter_enabled:
                        runtime_key: local_rate_limit_enabled
                        default_value:
                          numerator: 100
                          denominator: HUNDRED
                      filter_enforced:
                        runtime_key: local_rate_limit_enforced
                        default_value:
                          numerator: 100
                          denominator: HUNDRED
                      response_headers_to_add:
                        - append_action: OVERWRITE_IF_EXISTS_OR_ADD
                          header:
                            key: x-local-rate-limit
                            value: 'true'
                  # 라우터 필터 (반드시 마지막)
                  - name: envoy.filters.http.router
                    typed_config:
                      '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                # 라우트 설정
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: api_service
                      domains: ['api.example.com']
                      routes:
                        - match:
                            prefix: '/api/v1/users'
                          route:
                            cluster: user_service
                            timeout: 10s
                            retry_policy:
                              retry_on: '5xx,connect-failure,reset'
                              num_retries: 3
                              per_try_timeout: 3s
                              retry_back_off:
                                base_interval: 0.1s
                                max_interval: 1s
                        - match:
                            prefix: '/api/v1/orders'
                          route:
                            cluster: order_service
                            timeout: 15s
                            retry_policy:
                              retry_on: '5xx,connect-failure'
                              num_retries: 2
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      filename: /etc/envoy/certs/server.crt
                    private_key:
                      filename: /etc/envoy/certs/server.key

  clusters:
    - name: user_service
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: user_service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: user-service.default.svc.cluster.local
                      port_value: 8080
      # 서킷 브레이커 설정
      circuit_breakers:
        thresholds:
          - priority: DEFAULT
            max_connections: 1024
            max_pending_requests: 1024
            max_requests: 1024
            max_retries: 3
      # 헬스 체크
      health_checks:
        - timeout: 5s
          interval: 10s
          unhealthy_threshold: 3
          healthy_threshold: 2
          http_health_check:
            path: /health
      # Outlier Detection (이상 탐지)
      outlier_detection:
        consecutive_5xx: 5
        interval: 10s
        base_ejection_time: 30s
        max_ejection_percent: 50

    - name: order_service
      type: STRICT_DNS
      lb_policy: LEAST_REQUEST
      load_assignment:
        cluster_name: order_service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: order-service.default.svc.cluster.local
                      port_value: 8080
      circuit_breakers:
        thresholds:
          - priority: DEFAULT
            max_connections: 512
            max_pending_requests: 512

    - name: auth0_jwks
      type: LOGICAL_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: auth0_jwks
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: your-tenant.auth0.com
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          sni: your-tenant.auth0.com

GraphQL Federation

Apollo Federation v2 Overview

GraphQL Federation is an architectural pattern that composes subgraphs independently managed by multiple teams into a unified supergraph. Apollo Federation v2 provides an improved shared ownership model, enhanced type merging, and cleaner syntax.

    ┌──────────────────────────────────────────────────┐
    │                  클라이언트                        │
                  (Web / Mobile App)    └────────────────────┬─────────────────────────────┘
GraphQL 쿼리
    ┌────────────────────▼─────────────────────────────┐
Apollo Router             (슈퍼그래프 실행 엔진)    │                                                  │
    │  ┌────────────────────────────────────────────┐   │
    │  │       Query Plan (쿼리 실행 계획)           │   │
    │  │  1. users 서브그래프에서 User 조회          │   │
    │  │  2. orders 서브그래프에서 Order 조회        │   │
    │  │  3. reviews 서브그래프에서 Review 조회      │   │
    │  │  4. 결과 병합 후 클라이언트에 반환          │   │
    │  └────────────────────────────────────────────┘   │
    └───────┬───────────────┬───────────────┬──────────┘
            │               │               │
    ┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
Users        │ │ Orders      │ │ ReviewsSubgraph     │ │ Subgraph    │ │ Subgraph     (Team A) (Team B) (Team C)    │              │ │             │ │             │
- User       │ │ - Order     │ │ - Review- Profile    │ │ - LineItem  │ │ - Rating    │              │ │ - extends   │ │ - extends    │              │ │   User      │ │   User    └──────────────┘ └─────────────┘ └─────────────┘

Federation Subgraph Implementation

// subgraphs/users/src/index.ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { gql } from 'graphql-tag'

const typeDefs = gql`
  extend schema
    @link(
      url: "https://specs.apollo.dev/federation/v2.11"
      import: ["@key", "@shareable", "@external", "@provides"]
    )

  type Query {
    user(id: ID!): User
    users(limit: Int = 10, offset: Int = 0): UserConnection!
    me: User
  }

  type User @key(fields: "id") {
    id: ID!
    email: String!
    name: String!
    avatarUrl: String
    role: UserRole!
    createdAt: String!
    updatedAt: String!
  }

  type UserConnection {
    nodes: [User!]!
    totalCount: Int!
    pageInfo: PageInfo!
  }

  type PageInfo @shareable {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
  }

  enum UserRole {
    ADMIN
    USER
    MODERATOR
  }
`

const resolvers = {
  Query: {
    user: async (_: any, { id }: { id: string }, context: any) => {
      return context.dataSources.usersAPI.getUser(id)
    },
    users: async (_: any, args: any, context: any) => {
      return context.dataSources.usersAPI.getUsers(args)
    },
    me: async (_: any, __: any, context: any) => {
      if (!context.userId) throw new Error('Not authenticated')
      return context.dataSources.usersAPI.getUser(context.userId)
    },
  },
  User: {
    __resolveReference: async (ref: { id: string }, context: any) => {
      // Federation이 다른 서브그래프에서 User를 참조할 때 호출
      return context.dataSources.usersAPI.getUser(ref.id)
    },
  },
}

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers }),
})

const { url } = await startStandaloneServer(server, {
  listen: { port: 4001 },
  context: async ({ req }) => ({
    userId: req.headers['x-user-id'],
    dataSources: {
      usersAPI: new UsersDataSource(),
    },
  }),
})

console.log(`Users subgraph ready at ${url}`)
// subgraphs/orders/src/index.ts
import { ApolloServer } from '@apollo/server'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { gql } from 'graphql-tag'

const typeDefs = gql`
  extend schema
    @link(
      url: "https://specs.apollo.dev/federation/v2.11"
      import: ["@key", "@external", "@requires"]
    )

  type Query {
    order(id: ID!): Order
    ordersByUser(userId: ID!, status: OrderStatus): [Order!]!
  }

  # User 타입을 확장하여 orders 필드 추가
  type User @key(fields: "id") {
    id: ID! @external
    orders(status: OrderStatus, limit: Int = 10): [Order!]!
    totalSpent: Float! @requires(fields: "id")
  }

  type Order @key(fields: "id") {
    id: ID!
    userId: ID!
    user: User!
    items: [OrderItem!]!
    status: OrderStatus!
    totalAmount: Float!
    currency: String!
    createdAt: String!
    shippingAddress: Address
  }

  type OrderItem {
    productId: ID!
    productName: String!
    quantity: Int!
    unitPrice: Float!
  }

  type Address {
    street: String!
    city: String!
    country: String!
    zipCode: String!
  }

  enum OrderStatus {
    PENDING
    CONFIRMED
    SHIPPED
    DELIVERED
    CANCELLED
  }
`

const resolvers = {
  Query: {
    order: async (_: any, { id }: { id: string }, ctx: any) => {
      return ctx.dataSources.ordersAPI.getOrder(id)
    },
    ordersByUser: async (_: any, args: any, ctx: any) => {
      return ctx.dataSources.ordersAPI.getOrdersByUser(args.userId, args.status)
    },
  },
  User: {
    orders: async (user: { id: string }, args: any, ctx: any) => {
      return ctx.dataSources.ordersAPI.getOrdersByUser(user.id, args.status)
    },
    totalSpent: async (user: { id: string }, _: any, ctx: any) => {
      return ctx.dataSources.ordersAPI.getTotalSpent(user.id)
    },
  },
  Order: {
    user: (order: { userId: string }) => ({ __typename: 'User', id: order.userId }),
  },
}

Supergraph Configuration

# supergraph-config.yaml
# rover supergraph compose --config supergraph-config.yaml > supergraph.graphql
federation_version: =2.11.2
subgraphs:
  users:
    routing_url: http://users-subgraph:4001/graphql
    schema:
      subgraph_url: http://users-subgraph:4001/graphql
  orders:
    routing_url: http://orders-subgraph:4002/graphql
    schema:
      subgraph_url: http://orders-subgraph:4002/graphql
  reviews:
    routing_url: http://reviews-subgraph:4003/graphql
    schema:
      subgraph_url: http://reviews-subgraph:4003/graphql
# router-config.yaml (Apollo Router 설정)
supergraph:
  listen: 0.0.0.0:4000
  introspection: false # 프로덕션에서는 비활성화

# 헤더 전파
headers:
  all:
    request:
      - propagate:
          named: 'Authorization'
      - propagate:
          named: 'X-Request-ID'
      - propagate:
          named: 'X-User-ID'

# 서브그래프별 설정
traffic_shaping:
  all:
    timeout: 10s
  subgraphs:
    orders:
      timeout: 15s # 주문 서비스는 더 긴 타임아웃

# 쿼리 깊이 제한 (DoS 방지)
limits:
  max_depth: 15
  max_height: 200
  max_aliases: 30

# 텔레메트리
telemetry:
  exporters:
    tracing:
      otlp:
        enabled: true
        endpoint: http://otel-collector:4317
        protocol: grpc
    metrics:
      prometheus:
        enabled: true
        listen: 0.0.0.0:9090
        path: /metrics

# 응답 캐싱
preview_entity_cache:
  enabled: true
  subgraph:
    all:
      enabled: true
      ttl: 60s

Authentication and Authorization Integration

Gateway Layer JWT Verification Middleware

The standard pattern is to verify JWTs at the gateway, convert verified claims to headers, and pass them to downstream services. Tokens signed with RS256 or ES256 algorithms are used, and signatures are verified with the IdP's public key.

// gateway/src/middleware/auth.ts
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
import { Request, Response, NextFunction } from 'express'

// JWKS 클라이언트 (키 캐싱 포함)
const client = jwksClient({
  jwksUri: `${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
  cache: true,
  cacheMaxEntries: 5,
  cacheMaxAge: 600000, // 10분
  rateLimit: true,
  jwksRequestsPerMinute: 10,
})

function getSigningKey(header: jwt.JwtHeader): Promise<string> {
  return new Promise((resolve, reject) => {
    client.getSigningKey(header.kid, (err, key) => {
      if (err) return reject(err)
      const signingKey = key?.getPublicKey()
      if (!signingKey) return reject(new Error('No signing key found'))
      resolve(signingKey)
    })
  })
}

// 경로별 인증 정책
const AUTH_POLICIES: Record<string, { required: boolean; scopes?: string[] }> = {
  '/api/v1/users/me': { required: true, scopes: ['read:profile'] },
  '/api/v1/orders': { required: true, scopes: ['read:orders'] },
  '/api/v1/admin': { required: true, scopes: ['admin:all'] },
  '/health': { required: false },
  '/api/v1/products': { required: false }, // 공개 API
}

export async function authMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  // 경로별 정책 조회
  const policy = Object.entries(AUTH_POLICIES).find(([path]) => req.path.startsWith(path))

  if (policy && !policy[1].required) {
    return next()
  }

  const authHeader = req.headers.authorization
  if (!authHeader?.startsWith('Bearer ')) {
    res.status(401).json({
      error: 'unauthorized',
      message: 'Missing or invalid Authorization header',
    })
    return
  }

  const token = authHeader.substring(7)

  try {
    // JWT 디코딩 (검증 전 헤더 확인)
    const decoded = jwt.decode(token, { complete: true })
    if (!decoded || typeof decoded === 'string') {
      throw new Error('Invalid token format')
    }

    // 알고리즘 화이트리스트 검증
    if (!['RS256', 'ES256'].includes(decoded.header.alg)) {
      throw new Error(`Unsupported algorithm: ${decoded.header.alg}`)
    }

    // JWKS에서 공개 키를 가져와 서명 검증
    const signingKey = await getSigningKey(decoded.header)
    const payload = jwt.verify(token, signingKey, {
      algorithms: ['RS256', 'ES256'],
      audience: process.env.API_AUDIENCE,
      issuer: `${process.env.AUTH0_DOMAIN}/`,
      clockTolerance: 30, // 30초 시계 오차 허용
    }) as jwt.JwtPayload

    // 스코프 검증
    if (policy?.[1]?.scopes) {
      const tokenScopes = (payload.scope || '').split(' ')
      const requiredScopes = policy[1].scopes
      const hasAllScopes = requiredScopes.every((s) => tokenScopes.includes(s))

      if (!hasAllScopes) {
        res.status(403).json({
          error: 'insufficient_scope',
          message: `Required scopes: ${requiredScopes.join(', ')}`,
        })
        return
      }
    }

    // 검증된 클레임을 헤더로 전파 (하위 서비스에서 사용)
    req.headers['x-user-id'] = payload.sub || ''
    req.headers['x-user-email'] = payload.email || ''
    req.headers['x-user-roles'] = JSON.stringify(payload['https://api.example.com/roles'] || [])
    req.headers['x-auth-time'] = String(payload.iat || 0)

    // 원본 Authorization 헤더는 제거 (하위 서비스에 토큰 노출 방지)
    delete req.headers.authorization

    next()
  } catch (error: any) {
    const errorMap: Record<string, { status: number; message: string }> = {
      TokenExpiredError: { status: 401, message: 'Token has expired' },
      JsonWebTokenError: { status: 401, message: 'Invalid token' },
      NotBeforeError: { status: 401, message: 'Token not yet valid' },
    }

    const mapped = errorMap[error.name] || { status: 401, message: 'Authentication failed' }
    res.status(mapped.status).json({ error: error.name, message: mapped.message })
  }
}

Rate Limiting and Circuit Breaker

Multi-tier Rate Limiting Strategy

Effective rate limiting is configured in multiple tiers rather than a single layer. The edge defends against DDoS with aggressive IP-based limits, while the gateway applies fine-grained limits based on users/API keys.

// gateway/src/middleware/rateLimiter.ts
import Redis from 'ioredis'

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  maxRetriesPerRequest: 3,
  retryStrategy: (times) => Math.min(times * 100, 3000),
  enableReadyCheck: true,
})

interface RateLimitConfig {
  windowMs: number // 시간 창 (밀리초)
  maxRequests: number // 최대 요청 수
  keyPrefix: string // Redis 키 프리픽스
}

// 슬라이딩 윈도우 레이트 리미터 (Redis Sorted Set 활용)
async function slidingWindowRateLimit(
  identifier: string,
  config: RateLimitConfig
): Promise<{ allowed: boolean; remaining: number; retryAfter?: number }> {
  const key = `${config.keyPrefix}:${identifier}`
  const now = Date.now()
  const windowStart = now - config.windowMs

  // Redis 트랜잭션으로 원자적 처리
  const pipeline = redis.pipeline()
  pipeline.zremrangebyscore(key, 0, windowStart) // 만료된 항목 제거
  pipeline.zadd(key, now.toString(), `${now}:${Math.random()}`) // 현재 요청 추가
  pipeline.zcard(key) // 현재 윈도우 내 요청 수
  pipeline.expire(key, Math.ceil(config.windowMs / 1000)) // TTL 설정

  const results = await pipeline.exec()
  if (!results) throw new Error('Redis pipeline failed')

  const currentCount = results[2]?.[1] as number
  const allowed = currentCount <= config.maxRequests
  const remaining = Math.max(0, config.maxRequests - currentCount)

  if (!allowed) {
    // 가장 오래된 요청이 만료되는 시점 계산
    const oldestInWindow = await redis.zrange(key, 0, 0, 'WITHSCORES')
    const retryAfter =
      oldestInWindow.length >= 2
        ? Math.ceil((parseInt(oldestInWindow[1]) + config.windowMs - now) / 1000)
        : Math.ceil(config.windowMs / 1000)

    return { allowed: false, remaining: 0, retryAfter }
  }

  return { allowed: true, remaining }
}

// 티어별 레이트 리미팅 정책
const RATE_LIMIT_TIERS: Record<string, RateLimitConfig> = {
  free: { windowMs: 60_000, maxRequests: 60, keyPrefix: 'rl:free' },
  pro: { windowMs: 60_000, maxRequests: 600, keyPrefix: 'rl:pro' },
  enterprise: { windowMs: 60_000, maxRequests: 6000, keyPrefix: 'rl:enterprise' },
  // IP 기반 글로벌 제한 (DDoS 방어)
  global_ip: { windowMs: 1_000, maxRequests: 50, keyPrefix: 'rl:ip' },
}

export async function rateLimitMiddleware(req: any, res: any, next: any) {
  const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip
  const userId = req.headers['x-user-id']
  const tier = req.headers['x-user-tier'] || 'free'

  try {
    // 1단계: IP 기반 글로벌 레이트 리미팅
    const ipResult = await slidingWindowRateLimit(clientIp, RATE_LIMIT_TIERS.global_ip)
    if (!ipResult.allowed) {
      res.set('Retry-After', String(ipResult.retryAfter))
      return res.status(429).json({ error: 'Too many requests (IP limit)' })
    }

    // 2단계: 사용자/티어 기반 레이트 리미팅
    if (userId) {
      const config = RATE_LIMIT_TIERS[tier] || RATE_LIMIT_TIERS.free
      const userResult = await slidingWindowRateLimit(userId, config)

      res.set('X-RateLimit-Limit', String(config.maxRequests))
      res.set('X-RateLimit-Remaining', String(userResult.remaining))

      if (!userResult.allowed) {
        res.set('Retry-After', String(userResult.retryAfter))
        return res.status(429).json({ error: 'Rate limit exceeded', tier })
      }
    }

    next()
  } catch (error) {
    // Redis 장애 시 요청을 통과시킴 (fault-tolerant)
    console.error('Rate limiter error:', error)
    next()
  }
}

Circuit Breaker Pattern

The circuit breaker opens the circuit when the failure rate exceeds a threshold, returning fast failure responses, and then gradually attempts recovery in a half-open state after a cooldown period.

// gateway/src/middleware/circuitBreaker.ts
enum CircuitState {
  CLOSED = 'CLOSED', // 정상 - 요청 통과
  OPEN = 'OPEN', // 차단 - 즉시 실패 반환
  HALF_OPEN = 'HALF_OPEN', // 테스트 - 제한적 요청 허용
}

interface CircuitBreakerConfig {
  failureThreshold: number // 실패 임계치 (예: 5)
  successThreshold: number // 복구 판정 성공 횟수 (예: 3)
  timeout: number // Open 상태 유지 시간 (ms)
  monitorWindow: number // 모니터링 윈도우 (ms)
  halfOpenMaxRequests: number // Half-Open에서 허용할 최대 요청
}

class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED
  private failureCount = 0
  private successCount = 0
  private lastFailureTime = 0
  private halfOpenRequests = 0

  constructor(
    private name: string,
    private config: CircuitBreakerConfig
  ) {}

  async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (Date.now() - this.lastFailureTime >= this.config.timeout) {
        this.transitionTo(CircuitState.HALF_OPEN)
      } else {
        console.warn(`[CircuitBreaker:${this.name}] OPEN - rejecting request`)
        if (fallback) return fallback()
        throw new Error(`Service ${this.name} is unavailable (circuit open)`)
      }
    }

    if (this.state === CircuitState.HALF_OPEN) {
      if (this.halfOpenRequests >= this.config.halfOpenMaxRequests) {
        if (fallback) return fallback()
        throw new Error(`Service ${this.name} is testing recovery`)
      }
      this.halfOpenRequests++
    }

    try {
      const result = await fn()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      if (fallback && this.state === CircuitState.OPEN) return fallback()
      throw error
    }
  }

  private onSuccess() {
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++
      if (this.successCount >= this.config.successThreshold) {
        this.transitionTo(CircuitState.CLOSED)
      }
    }
    this.failureCount = 0
  }

  private onFailure() {
    this.failureCount++
    this.lastFailureTime = Date.now()
    if (this.failureCount >= this.config.failureThreshold) {
      this.transitionTo(CircuitState.OPEN)
    }
  }

  private transitionTo(newState: CircuitState) {
    console.log(`[CircuitBreaker:${this.name}] ${this.state} -> ${newState}`)
    this.state = newState
    if (newState === CircuitState.CLOSED) {
      this.failureCount = 0
      this.successCount = 0
    }
    if (newState === CircuitState.HALF_OPEN) {
      this.halfOpenRequests = 0
      this.successCount = 0
    }
  }

  getStatus() {
    return {
      name: this.name,
      state: this.state,
      failureCount: this.failureCount,
      lastFailureTime: this.lastFailureTime ? new Date(this.lastFailureTime).toISOString() : null,
    }
  }
}

// 서비스별 서킷 브레이커 인스턴스
export const circuitBreakers = {
  userService: new CircuitBreaker('user-service', {
    failureThreshold: 5,
    successThreshold: 3,
    timeout: 30_000, // 30초 후 Half-Open 전환
    monitorWindow: 60_000, // 1분 모니터링 윈도우
    halfOpenMaxRequests: 3,
  }),
  orderService: new CircuitBreaker('order-service', {
    failureThreshold: 3,
    successThreshold: 2,
    timeout: 60_000, // 결제 관련은 보수적으로 60초
    monitorWindow: 60_000,
    halfOpenMaxRequests: 1,
  }),
}

Observability Integration

Since the gateway is the point through which all traffic passes, it becomes the central hub for observability. All three pillars of observability -- logs, metrics, and traces -- should be collected at the gateway layer.

OpenTelemetry Integration Configuration

# otel-collector-config.yaml
# OpenTelemetry Collector 설정 - 게이트웨이 트래픽 수집
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  # 게이트웨이 메타데이터 추가
  attributes:
    actions:
      - key: deployment.environment
        value: production
        action: upsert
      - key: service.layer
        value: gateway
        action: upsert
  # 샘플링 (전체의 10%만 상세 트레이스)
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: error-policy
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: latency-policy
        type: latency
        latency:
          threshold_ms: 1000
      - name: probabilistic-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

exporters:
  otlphttp/jaeger:
    endpoint: http://jaeger:4318
  prometheus:
    endpoint: 0.0.0.0:8889
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
    labels:
      resource:
        service.name: 'service_name'
        service.layer: 'layer'

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, attributes, tail_sampling]
      exporters: [otlphttp/jaeger]
    metrics:
      receivers: [otlp]
      processors: [batch, attributes]
      exporters: [prometheus]
    logs:
      receivers: [otlp]
      processors: [batch, attributes]
      exporters: [loki]

Key Metrics Dashboard Items

The metrics that must be monitored at the gateway are as follows.

  • Request Rate: Requests per second. Abnormal spikes indicate DDoS attacks or client bugs.
  • Error Rate: Ratio of 4xx and 5xx responses. Track separately per service.
  • Latency: p50, p95, p99 percentiles. Alert when p99 exceeds SLA.
  • Circuit Breaker Status: Display circuit state of each service in real-time.
  • Rate Limiting Hit Rate: Ratio of requests hitting rate limits. If normal users are frequently limited, threshold adjustments are needed.
  • Upstream Connection Pool: Track active connections, pending requests, and timeout counts.

Troubleshooting

Failure Scenario 1: Gateway Memory Leak

Symptom: Gateway Pod memory usage continuously increases, resulting in OOM Kill.

Cause: Failing to release memory while buffering large request/response bodies. This commonly occurs when proxying file upload APIs or large JSON responses.

Solution:

# Kong: 요청/응답 본문 버퍼링 제한
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: request-size-limiting
config:
  allowed_payload_size: 10 # MB
  size_unit: megabytes
  require_content_length: true
plugin: request-size-limiting
# Envoy: 버퍼 제한 설정
http_filters:
  - name: envoy.filters.http.buffer
    typed_config:
      '@type': type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer
      max_request_bytes: 10485760 # 10MB

Failure Scenario 2: Cascading Failure

Symptom: A single backend service failure propagates through the gateway to the entire system.

Cause: Circuit breaker not configured or timeout set too long. Requests to the failed service exhaust the gateway's connection pool.

Recovery Procedure:

  1. Immediately manually open the circuit breaker for the failed service
  2. Set up static fallback responses for the service route
  3. Check connection pool status and perform rolling restart of gateway Pods if needed
  4. After the failed service recovers, gradually restore traffic (canary)

Failure Scenario 3: TLS Certificate Expiration

Symptom: Sudden 503 errors across all APIs. SSL/TLS handshake failure logs observed on clients.

Prevention: Automatic certificate renewal using cert-manager, and alerting 30 days before expiration.

# 인증서 만료일 확인 스크립트
#!/bin/bash
DOMAIN="api.example.com"
EXPIRY=$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2>/dev/null \
  | openssl x509 -noout -dates 2>/dev/null \
  | grep notAfter | cut -d= -f2)

EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

echo "Certificate expires: $EXPIRY ($DAYS_LEFT days remaining)"

if [ "$DAYS_LEFT" -lt 30 ]; then
  echo "WARNING: Certificate expiring soon!"
  # 슬랙/PagerDuty 알림 전송
fi

Failure Scenario 4: GraphQL Federation Subgraph Schema Conflict

Symptom: rover supergraph compose fails. Two subgraphs define the same field of the same type with different return types.

Solution: Use Federation v2's @shareable directive to explicitly declare shared type fields and the @override directive to transfer field ownership. Run rover subgraph check in your CI/CD pipeline to pre-validate compatibility.

Production Checklist

Pre-deployment Verification

  • Configure gateway cluster with at least 3 nodes for HA
  • Enforce TLS 1.3 and disable weak cipher suites
  • Configure health check endpoints (/health, /ready)
  • Set request/response size limits (default 10MB or less)
  • Timeout settings: connection 5s, read 30s, write 30s
  • Set appropriate DNS TTL (too long delays failure recovery)

Security

  • Apply TLS for all external communication, mTLS for internal communication
  • Perform JWT verification at the gateway, do not pass tokens to downstream services
  • Apply CORS allowed domain whitelist
  • Rate limiting: Apply both IP-based (DDoS) and user-based (fair usage)
  • Mask sensitive headers (Authorization, Cookie) in logging
  • Restrict Admin API access to internal network only

Observability

  • Distributed tracing: Assign and propagate X-Request-ID for all requests
  • Metrics: Collect RED (Rate, Errors, Duration) metrics and build dashboards
  • Alerting: Immediate alerts when error rate exceeds 5%, p99 latency exceeds SLA, or circuit breaker opens
  • Logging: Structured JSON logs, log request/response bodies only in debug mode

Operations

  • Canary deployment: Verify with 5-10% traffic before full rollout for gateway configuration changes
  • Configuration version control: Manage all gateway configurations with Git (GitOps)
  • Rollback procedure: Ensure ability to rollback to previous configuration within 30 seconds
  • Load testing: Perform regular load tests at 2x or more of expected peak traffic
  • Chaos engineering: Verify automatic recovery when a single gateway node fails

Anti-patterns and Failure Cases

Anti-pattern 1: God Gateway

A pattern where all business logic is implemented in the gateway. When the gateway handles request validation, data transformation, business rule application, and response aggregation, the gateway becomes a monolith. The gateway should only handle cross-cutting concerns such as routing, authentication, and rate limiting.

Anti-pattern 2: Shared BFF

When a single BFF starts serving both web and mobile, the requirements from both sides conflict and complexity increases exponentially. Conditional statements like "this field is only needed for mobile" spread throughout the BFF, resulting in a worse situation than without a BFF. Maintain a dedicated BFF for each frontend type.

Anti-pattern 3: Excessive Data Transformation at the Gateway

When the gateway performs excessive response transformation (field renaming, data structure rearrangement, field filtering based on business logic), gateway CPU usage spikes and debugging becomes difficult. Data transformation should be performed in the BFF or the service itself.

Anti-pattern 4: Deployment Without Rate Limiting

"Our service has low traffic, so we don't need rate limiting" is the most common mistake. Unexpected crawlers, infinite retries from poorly implemented clients, and abuse from leaked API keys occur regardless of traffic volume. Rate limiting is not optional but mandatory for production deployments.

Anti-pattern 5: Synchronous Call Chains Without Circuit Breakers

In a synchronous chain where the gateway calls Service A and Service A calls Service B, without circuit breakers, a Service B failure will cascade and bring down Service A and the gateway (cascading failure). Apply circuit breakers at all external call points, and switch to asynchronous communication where possible.

Failure Case: GraphQL Federation N+1 Query Problem

In a Federation environment, when the Router fetches a list of users from Subgraph A and then individually queries each user's orders from Subgraph B, an N+1 problem occurs. Apollo Router's Query Plan optimizes with batch requests where possible, but performance degrades significantly if subgraphs don't support batch queries. The DataLoader pattern must be applied to ensure __resolveReference in subgraphs supports batching.

References