高度なGraphQLフェデレーション: 分散型スーパーグラフの構築

Table of Contents
GraphQL Federation(GraphQLフェデレーション)は、単一のモノリシックなGraphQLスキーマでは対応しきれなくなった組織にとって、当然の進化です。エンジニアリングチームの規模が拡大するにつれて、統合されたスキーマの維持は組織のボトルネックとなります。新しいフィールドを追加するたびにスキーマの所有者との調整が必要になり、破壊的な変更はすべてのコンシューマーに波及します。
フェデレーションは、この問題を分散型スーパーグラフで解決します。複数のチームが独立したGraphQLのサブグラフを管理し、それらがゲートウェイによって単一の統合APIにまとめられます。クライアントは1つのエンドポイントにクエリを発行し、一貫性のあるスキーマを見ます。舞台裏では、ゲートウェイがクエリのフラグメントを適切なサブグラフにルーティングし、結果を結合します。
このガイドでは、Apollo Federation v2(現在の標準)、サブグラフの設計パターン、エンティティ解決、DataLoaderによるN+1問題の軽減、キャッシング、および本番環境でのパフォーマンスに関する考慮事項について説明します。
スーパーグラフアーキテクチャ
┌─────────────────────────────────┐
Client ────────────▶│ Apollo Router (Gateway) │
└─────┬─────────┬────────┬─────────┘
│ │ │
┌─────▼──┐ ┌────▼──┐ ┌──▼──────┐
│ Users │ │Orders │ │ Products │
│Subgraph│ │Subgraph│ │ Subgraph │
└────────┘ └────────┘ └──────────┘
│ │ │
Postgres MongoDB Postgres
クライアントは単一のクエリを発行します。
query GetOrderWithDetails {
order(id: "ord_123") {
id
total
user {
name
email
}
items {
product {
name
price
}
quantity
}
}
}
ルーターのクエリプランナーはこれをサブオペレーションに分割し、可能であればOrders、Users、Productsの各サブグラフからデータを並行してフェッチし、結果をマージします。クライアントは基盤となる分散について何も知ることなく、1つのレスポンスを受け取ります。
サブグラフスキーマの定義
各サブグラフは、Federation固有のディレクティブを使用して、自身が何を所有し、他のサブグラフに何を貢献できるかを宣言します。
@keyディレクティブ:エンティティ宣言
@keyディレクティブはエンティティを宣言します。これは、サブグラフ間で参照および拡張できる型です。キーは、インスタンスを一意に識別するフィールドを指定します。
Usersサブグラフ:
# users-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
createdAt: String!
}
type Query {
user(id: ID!): User
me: User
}
Ordersサブグラフ:
# orders-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@external", "@requires"])
# Reference the User entity defined in the Users subgraph
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
type Order @key(fields: "id") {
id: ID!
total: Float!
status: OrderStatus!
userId: ID!
user: User!
items: [OrderItem!]!
}
enum OrderStatus { PENDING, FULFILLED, CANCELLED }
type OrderItem {
productId: ID!
quantity: Int!
unitPrice: Float!
}
type Query {
order(id: ID!): Order
orders(userId: ID!): [Order!]!
}
OrdersサブグラフがUser @key(fields: "id")をidフィールドのみで宣言していることに注目してください。これはUserエンティティを所有せずに参照しています。ゲートウェイは、order.user.nameを解決するために、まずOrdersからuserIdをフェッチし、次に{ _entities(representations: [{__typename: "User", id: $userId}]) }を使用してUsersにクエリを発行する必要があることを知っています。
エンティティ解決:__resolveReference関数
すべてのエンティティは__resolveReferenceリゾルバーを実装する必要があります。ゲートウェイは、サブグラフ間の参照を解決する必要があるときにこれを呼び出します。
// users-subgraph/resolvers.ts
import { UserService } from "./services/UserService";
const resolvers = {
User: {
// Called by the gateway with the entity "representation" (partial object containing @key fields)
__resolveReference: async (reference: { id: string }, context: Context) => {
return context.dataSources.userService.findById(reference.id);
},
},
Query: {
user: (_: unknown, { id }: { id: string }, context: Context) => {
return context.dataSources.userService.findById(id);
},
me: (_: unknown, __: unknown, context: Context) => {
return context.dataSources.userService.findById(context.userId);
},
},
};
ゲートウェイは、単一のリクエストで(_entitiesクエリを介して)表現のバッチとともに__resolveReferenceを呼び出すことがあります。ここで、注意しないとN+1問題が発生します。
フェデレーションにおけるN+1問題
userフィールドを持つ50件の注文を返す注文リストクエリを考えてみましょう。最適化なしでは、ゲートウェイは次のようになります。
- Ordersサブグラフから50件の注文をフェッチ(1クエリ)
- 各注文のユーザーに対して、Usersサブグラフに
_entitiesを50回呼び出す(50クエリ)
合計:1ページを表示するために51回のサブグラフ呼び出し。
解決策:__resolveReferenceにおけるDataLoader
Apollo Routerはエンティティ解決リクエストを自動的にバッチ処理します。オペレーションからすべてのUser参照を収集し、単一の_entitiesクエリで送信します。しかし、あなたの__resolveReferenceはこれを効率的に処理する必要があります。
import DataLoader from "dataloader";
// Create a DataLoader per request (in context factory)
export function createUserLoader(db: Database): DataLoader<string, User> {
return new DataLoader(
async (userIds: readonly string[]) => {
// Single DB query fetches all users at once
const users = await db.query(
"SELECT * FROM users WHERE id = ANY($1)",
[userIds]
);
// DataLoader requires results in the same order as input keys
const userMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
},
{ cache: true } // cache within request — same user requested twice = one DB hit
);
}
// In context factory:
const context = {
loaders: { user: createUserLoader(db) }
};
// In resolver:
const resolvers = {
User: {
__resolveReference: async (reference: { id: string }, context: Context) => {
return context.loaders.user.load(reference.id);
},
},
};
このパターンでは:50件の注文 → Usersサブグラフへの1回のバッチ呼び出し → 1回のSQLクエリ。合計:2回のサブグラフ呼び出し。
@requiresディレクティブ:計算フィールド
サブグラフが自身のフィールドを計算するために、他のサブグラフのフィールドを必要とすることがあります。@requiresディレクティブは、この依存関係を宣言します。
# shipping-subgraph/schema.graphql
type User @key(fields: "id") {
id: ID!
address: String! @external # owned by Users subgraph
shippingZone: ShippingZone! # computed from address by Shipping subgraph
@requires(fields: "address")
}
const resolvers = {
User: {
__resolveReference: async (reference: { id: string; address: string }) => {
// `address` is provided by the gateway from the Users subgraph
return {
id: reference.id,
shippingZone: computeShippingZone(reference.address),
};
},
},
};
ゲートウェイのクエリプランナーは、Shippingサブグラフを呼び出す前に、Usersからaddressを自動的にフェッチします。あなたが依存関係を宣言すれば、ルーターが実行順序を決定します。
Rover CLIによるスキーマ構成
Apollo Roverは、スーパーグラフスキーマを管理するためのCLIです。
# Install Rover
curl -sSL https://rover.apollo.dev/nix/latest | sh
# Check subgraph schema for Federation compatibility
rover subgraph check my-graph@production \
--schema ./users-subgraph/schema.graphql \
--name users
# Publish subgraph schema to Apollo Studio
rover subgraph publish my-graph@production \
--schema ./users-subgraph/schema.graphql \
--name users \
--routing-url https://users.internal.example.com/graphql
# Compose supergraph schema locally (for CI validation)
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql
supergraph.yaml:
federation_version: =2.3.5
subgraphs:
users:
routing_url: https://users.internal.example.com/graphql
schema:
file: ./users-subgraph/schema.graphql
orders:
routing_url: https://orders.internal.example.com/graphql
schema:
file: ./orders-subgraph/schema.graphql
products:
routing_url: https://products.internal.example.com/graphql
schema:
file: ./products-subgraph/schema.graphql
サブグラフスキーマの変更をデプロイする前に、必ずCIでrover supergraph composeを実行してください。構成エラー(競合する型定義や無効な@requiresチェーンなど)は、実行時ではなく、ここで早期に失敗します。
キャッシング戦略
フェデレーションは複数のキャッシュ境界を導入します。適切に設計されたキャッシング戦略は、パフォーマンスにとって不可欠です。
1. サブグラフレベルのレスポンスキャッシング
@cacheControlディレクティブを使用して、フィールドごとのTTLをアノテーションします。
type Product @key(fields: "id") @cacheControl(maxAge: 300) {
id: ID!
name: String! @cacheControl(maxAge: 3600) # rarely changes
price: Float! @cacheControl(maxAge: 60) # changes more often
stockCount: Int! @cacheControl(maxAge: 0) # never cache
}
Apollo RouterはサブグラフからのCache-Controlヘッダーを尊重し、パブリック/認証済みクエリに対してキャッシュされたスーパーグラフレスポンスを個別に提供できます。
2. DataLoaderキャッシング(リクエストスコープ)
上記のように、DataLoaderは単一のリクエスト内でキャッシュします。DataLoaderインスタンスをリクエスト間で再利用しないでください。古いデータを提供することになります。
3. ホットエンティティのためのRedis
頻繁に読み取られるエンティティ(例:めったに変更されない製品カタログ)の場合:
async function findProductById(id: string): Promise<Product> {
const cached = await redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
const product = await db.products.findById(id);
await redis.setex(`product:${id}`, 300, JSON.stringify(product)); // 5-min TTL
return product;
}
Apollo Routerのデプロイ
Apollo Routerは、Node.jsのApollo Gatewayに代わる、Rustで書かれた本番グレードのゲートウェイです。
# router.yaml
supergraph:
path: /graphql
cors:
origins:
- https://app.example.com
headers:
all:
request:
- propagate:
matching: ^x-.* # forward all x-* headers to subgraphs
traffic_shaping:
all:
deduplicate_variables: true
router:
timeout: 30s
subgraphs:
users:
timeout: 5s
orders:
timeout: 10s
telemetry:
exporters:
tracing:
otlp:
endpoint: http://otel-collector:4317
実行方法:
docker run -p 4000:4000 \
-v $(pwd)/supergraph-schema.graphql:/app/supergraph.graphql \
-v $(pwd)/router.yaml:/app/router.yaml \
ghcr.io/apollographql/router:v1.46.0 \
--supergraph /app/supergraph.graphql \
--config /app/router.yaml
Apollo Routerは、単純なクエリの場合、シングルコアで約800万リクエスト/秒を処理します。これはNode.jsゲートウェイよりも桁違いに高速です。
クエリプランニングとパフォーマンス
Apollo Routerのクエリプランナーは、受信するすべてのオペレーションに対して実行計画を生成します。計画は次で検査できます。
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ order(id: \"123\") { user { name } items { product { name } } } }"}' \
-H "Apollo-Query-Plan-Experimental: true"
クエリプランを最適化するには:
- エンティティジャンプの削減: 一般的なクエリでサブグラフ間のホップが少なくなるようにスキーマを設計する
- 頻繁に結合されるデータの併置:
order.userが常に一緒にフェッチされる場合、代わりにOrdersサブグラフに直接Userフィールドを追加することを検討する @interfaceObjectの使用: サブグラフにまたがるポリモーフィックな型の場合 (Federation v2.3+)
大規模なスキーマガバナンス
複数のチームがサブグラフを所有する場合、スキーマガバナンスはドリフトや破壊的な変更を防ぎます。
- 破壊的変更の検出 — Apollo Studioのスキーマチェックは、既存のオペレーションを破壊するデプロイをブロックします。
- スキーマ提案 — 大規模な変更のチーム間RFCスタイルのレビューには、Apollo Schema Proposalsを使用します。
- 需要駆動型スキーマ — 各チームは自身のサービスドメインに一致する型を所有します。チーム間のミューテーションはありません。
- コントラクト — Apollo Contractsを使用して、クライアント(モバイル、ウェブ、パートナーAPI)ごとにフィルタリングされたスキーマビューを定義します。
よくある質問
Federation v1 vs v2 — 移行すべきですか?
可能であれば、はい。Federation v2は、@interfaceObject、@authenticated、@requiresScopes、段階的な移行のための改善された@override、およびより良い構成エラーメッセージを追加します。Apollo Gateway(v1ランタイム)はメンテナンスモードであり、Apollo Routerが現在の標準です。
Apollo StudioなしでFederationを使用できますか? はい。Rover CLIはローカルでスキーマを構成します。Apollo RouterはApollo Studioへの接続なしで実行されます。Studioはマネージドフェデレーション(サブグラフスキーマのプッシュ、ルーターの自動更新)を追加し、これは大規模では非常に有用ですが、必須ではありません。
サブグラフ間の認証はどのように処理しますか?
ルーターからすべてのサブグラフにAuthorizationヘッダーを伝播させます(headers.all.request.propagateを設定)。各サブグラフはトークンを独立して検証するか、共有認証ライブラリを使用できます。リクエストボディのuserIdクレームは決して信用せず、常にJWTを検証してください。
フェデレーションとモノリスのパフォーマンスオーバーヘッドはどれくらいですか? 単純なクエリの場合、サブグラフホップごとに5〜15msの追加レイテンシ(クラスター内のネットワークRTT)を予想してください。複雑なマルチサブグラフクエリの場合、並列実行がこれを最小限に抑えます。Apollo RouterのRustクエリプランナーは1ms未満のオーバーヘッドを追加します。大規模では、組織的なメリット(チームの自律性、独立したデプロイ)がレイテンシコストをはるかに上回ります。
まとめ
GraphQL Federationは単なる技術的なパターンではなく、組織のスケーリング戦略です。これを習得したチームは、サブグラフの変更を独立してデプロイし、調整のオーバーヘッドなしにスキーマを反復し、自身のAPI部分を独自のペースで進化させることができます。
主要な原則は、エンティティをきれいに所有し、DataLoaderで__resolveReferenceを実装し、@requiresの依存関係を明示的に宣言し、ルーターに結合を任せることです。CIでの構成検証は、最も一般的なチーム間の破損を防ぎます。
こちらもどうぞ
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

GraphQLとgRPC:2026年に最適なAPIパラダイムを選択する
2026年におけるGraphQLとgRPCのアーキテクチャ上のトレードオフ、ProtobufバイナリエンコーディングとJSONの比較、HTTP/2多重化、最適なBFFハイブリッドパターンについて解説します。
Read more
エンタープライズアプリケーションのためのTypeScript高度パターン
branded type、条件付き応答型、テンプレートリテラルルーティング、satisfies演算子など、エンタープライズ向けTypeScriptの高度なパターンを習得しましょう。
Read more
高度なGraphQLAPI設計
DataLoaderによるN+1問題の解決、カーソルベースのページネーション、フィールドレベル認証、スキーマフェデレーションの実装を通じて、本番環境レベルのGraphQLAPIを設計しましょう。
Read more