Protokol

GraphQL Federation dan Schema Stitching

Panduan lengkap Apollo Federation v2 β€” subgraph, supergraph, entity resolution, @key directive, @external, @requires, dan arsitektur GraphQL terdistribusi untuk microservices skala enterprise

1. Masalah GraphQL Monolith

GraphQL awalnya dirancang dengan satu monolithic schema yang menggabungkan semua types dan resolvers. Dalam arsitektur microservices, ini menjadi masalah besar karena setiap service memiliki domain data sendiri, dan mengelola satu mega-schema yang mencakup semua service menjadi tidak praktis.

Tantangan GraphQL Monolith

TantanganDampak
Schema couplingPerubahan di satu service bisa merusak schema global
Deployment bottleneckSemua tim harus koordinasi untuk deploy schema
PerformanceSatu resolver bisa N+1 query ke banyak service
Team autonomyTim tidak bisa mandiri mengelola schema mereka
Code sizeSchema dan resolver membengkak seiring pertumbuhan
πŸ’‘ Kapan Butuh Federation?

GraphQL Federation cocok jika: tim Anda sudah terbagi per domain (team-based microservices), schema lebih dari 500+ types, atau Anda mengalami scaling bottleneck pada single GraphQL server. Untuk proyek kecil-menengah, monolithic GraphQL masih lebih sederhana dan maintainable.

2. Apollo Federation v2

Apollo Federation adalah standar terdepan untuk menggabungkan beberapa GraphQL services (subgraphs) menjadi satu unified schema (supergraph) yang bisa di-query client secara seamless. Apollo Router bertindak sebagai gateway yang meng-orchestrate query ke subgraphs yang tepat.

Architecture Overview

Text
# Apollo Federation Architecture:

# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
# β”‚               Client (Browser/App)           β”‚
# β”‚          Query: user + orders + reviews       β”‚
# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
#                    β”‚
#                    β–Ό
# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
# β”‚            Apollo Router (Gateway)            β”‚
# β”‚                                              β”‚
# β”‚  - Parse query                               β”‚
# β”‚  - Query planning β†’ subgraph routing          β”‚
# β”‚  - Parallel execution ke subgraphs            β”‚
# β”‚  - Merge results                             β”‚
# β”‚  - Return unified response                   β”‚
# β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
#     β”‚          β”‚          β”‚          β”‚
#     β–Ό          β–Ό          β–Ό          β–Ό
# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
# β”‚ Users  β”‚ β”‚ Orders β”‚ β”‚Reviews β”‚ β”‚Productsβ”‚
# β”‚Subgraphβ”‚ β”‚Subgraphβ”‚ β”‚Subgraphβ”‚ β”‚Subgraphβ”‚
# β”‚        β”‚ β”‚        β”‚ β”‚        β”‚ β”‚        β”‚
# β”‚ /users β”‚ β”‚/orders β”‚ β”‚/reviewsβ”‚ β”‚/productsβ”‚
# β”‚ /auth  β”‚ β”‚ /items β”‚ β”‚ /ratingβ”‚ β”‚ /stock β”‚
# β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜
#  User DB    Order DB   Review DB  Product DB

3. Membuat Subgraph

Setiap subgraph adalah GraphQL server independen yang mendefinisikan types dan resolvers untuk domain spesifik. Subgraph menggunakan federation directives untuk menandai types yang bisa di-share antar subgraphs.

Users Subgraph

GraphQL
# ===== users-subgraph/schema.graphql =====

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.7",
        import: ["@key", "@shareable", "@external", "@requires"])

type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
  avatar: String
  createdAt: String!
}

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  me: User
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

input CreateUserInput {
  name: String!
  email: String!
}

input UpdateUserInput {
  name: String
  email: String
}

Users Subgraph Implementation (Node.js)

JavaScript
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { readFileSync } = require('fs');
const gql = require('graphql-tag');

const typeDefs = gql(readFileSync('./schema.graphql', 'utf-8'));

const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return await db.users.findById(id);
    },
    users: async (_, { limit = 10, offset = 0 }) => {
      return await db.users.findAll({ limit, offset });
    },
    me: async (_, __, { currentUser }) => {
      return currentUser;
    }
  },
  User: {
    // __resolveReference: dipanggil oleh Router saat entity
    // dari subgraph lain butuh data User
    __resolveReference: async (reference) => {
      // reference = { __typename: "User", id: "123" }
      return await db.users.findById(reference.id);
    },

    // Field-level resolvers
    createdAt: (user) => user.created_at.toISOString()
  },
  Mutation: {
    createUser: async (_, { input }) => {
      return await db.users.create(input);
    },
    updateUser: async (_, { id, input }) => {
      return await db.users.update(id, input);
    }
  }
};

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

server.listen({ port: 4001 }).then(({ url }) => {
  console.log(`Users subgraph running at ${url}`);
});

Orders Subgraph (extends User)

GraphQL
# ===== orders-subgraph/schema.graphql =====

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.7",
        import: ["@key", "@shareable", "@external", "@requires"])

type Order @key(fields: "id") {
  id: ID!
  userId: ID!
  items: [OrderItem!]!
  total: Float!
  status: OrderStatus!
  createdAt: String!
}

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

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}

# EXTEND User type β€” tambahkan field orders ke User
# yang dimiliki oleh Users subgraph
type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
  totalSpent: Float! @requires(fields: "id")
}

type Query {
  order(id: ID!): Order
  orders(userId: ID): [Order!]!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  updateOrderStatus(id: ID!, status: OrderStatus!): Order!
}

input CreateOrderInput {
  userId: ID!
  items: [OrderItemInput!]!
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
}

4. Entity Resolution & @key

Entity adalah type yang bisa di-referensikan dari subgraph lain. Di-tag dengan @key directive yang mendefinisikan field unik untuk identifikasi. Saat Router butuh data dari subgraph lain, ia mengirim representasi entity (typename + key fields) dan subgraph harus bisa me-resolve-nya.

Text
# Entity Resolution Flow:
#
# Client query: { user(id:"123") { name, orders { total } } }
#
# Step 1: Router tahu User.name ada di Users subgraph
#         Router tahu User.orders ada di Orders subgraph
#
# Step 2: Router kirim query ke Users subgraph:
#         { user(id:"123") { id, name } }
#
# Step 3: Router kirim entity representations ke Orders subgraph:
#         {
#           _entities(representations: [
#             { __typename: "User", id: "123" }
#           ]) {
#             ... on User { orders { total } }
#           }
#         }
#
# Step 4: Orders subgraph memanggil __resolveReference({ __typename: "User", id: "123" })
#         yang mengembalikan data user + orders
#
# Step 5: Router meng-merge results dan mengirim ke client

# Multiple @key:
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
  name: String!
  price: Float!
}
# β†’ Bisa di-resolve dengan "id" ATAU "sku"

5. Federation Directives

DirectiveFungsiContoh
@key(fields:)Menandai type sebagai entity dengan primary keytype User @key(fields: "id")
@externalField dimiliki subgraph lain, di-extend di siniid: ID! @external
@requires(fields:)Resolver butuh field external tertentutotalSpent: Float @requires(fields: "id")
@provides(fields:)Subgraph bisa menyediakan field untuk entityuser: User @provides(fields: "name")
@shareableField bisa di-resolve oleh multiple subgraphsname: String @shareable
@override(from:)Move field ownership dari subgraph lainemail: String @override(from: "users")
@inaccessibleSembunyikan field dari supergraphpassword: String @inaccessible
@tag(name:)Tag untuk contract testing / filteringemail: String @tag(name: "internal")

6. Supergraph & Apollo Router

Supergraph adalah gabungan schema dari semua subgraphs yang di-compose oleh Apollo Rover CLI dan di-serve oleh Apollo Router.

YAML
# ===== router.yaml β€” Apollo Router Configuration =====

supergraph:
  listen: 0.0.0.0:4000
  introspection: true

# CORS configuration
cors:
  allow_any_origin: false
  origins:
    - "https://app.example.com"
    - "http://localhost:3000"

# Subgraph override URLs (untuk local dev)
override_subgraph_url:
  users: http://localhost:4001/graphql
  orders: http://localhost:4002/graphql
  reviews: http://localhost:4003/graphql
  products: http://localhost:4004/graphql

# Telemetry & monitoring
telemetry:
  instrumentation:
    spans:
      mode: spec_compliant
  exporters:
    tracing:
      otlp:
        endpoint: http://jaeger:4317
        protocol: grpc

# Query planning cache
supergraph.query_planning:
  cache:
    in_memory:
      limit: 1000

# Traffic shaping
traffic_shaping:
  router:
    timeout: 30s
  all:
    timeout: 10s
  subgraphs:
    orders:
      timeout: 15s  # Orders butuh timeout lebih lama
    reviews:
      timeout: 5s
Bash
# Compose supergraph dari subgraph schemas
rover supergraph compose \
  --config ./supergraph-config.yaml \
  > supergraph.graphql

# Publish subgraph schema ke Apollo Studio
rover subgraph publish my-graph@production \
  --schema ./users-schema.graphql \
  --name users \
  --routing-url http://users-service:4001/graphql

# Run Apollo Router locally
docker run -p 4000:4000 \
  --mount "type=bind,source=./router.yaml,target=/dist/config/router.yaml" \
  ghcr.io/apollorouter/graph-router:latest \
  --config /dist/config/router.yaml

7. Federation vs Schema Stitching

AspekApollo FederationSchema Stitching
ApproachDecentralized β€” setiap tim kelola subgraph sendiriCentralized β€” gateway menggabungkan schemas
Schema ownershipPer-team, autonomousGateway-level
Entity resolutionBuilt-in (@key, __resolveReference)Manual stitching resolvers
Type mergingAutomatic via federation directivesManual configuration
PerformanceQuery planner optimized, parallel executionTergantung implementasi
ToolingApollo Studio, Rover CLI, Router@graphql-tools/stitch
Learning curveMedium β€” perlu paham directivesMedium-High β€” manual setup
MaturityIndustry standard (v2.7)Cocok untuk migrasi gradual
Best forEnterprise, multi-team, scale besarIntegrasi legacy GraphQL, simple needs

Quiz Pemahaman

Pertanyaan 1: Apa fungsi utama Apollo Router dalam Federation?

a) Menyimpan data
b) Meng-orchestrate query ke subgraphs dan merge hasil
c) Meng-generate database schema
d) Meng-handle authentication

Pertanyaan 2: Directive apa yang menandai type sebagai entity di Federation?

a) @entity
b) @shareable
c) @key
d) @external

Pertanyaan 3: Apa yang dilakukan __resolveReference?

a) Menghapus entity
b) Me-resolve entity berdasarkan key dari subgraph lain
c) Membuat entity baru
d) Meng-cache entity

Pertanyaan 4: Apa perbedaan @requires dan @external?

a) Sama saja
b) @external menandai field milik subgraph lain, @requires menyatakan field yang dibutuhkan resolver
c) @requires untuk type, @external untuk field
d) @external deprecated, @requires yang baru

Pertanyaan 5: Apa keuntungan utama Federation dibanding monolithic GraphQL?

a) Lebih sedikit kode
b) Team autonomy β€” setiap tim bisa mandiri develop dan deploy subgraph
c) Tidak butuh API gateway
d) Semua data di satu database
πŸ” Zoom
100%
🎨 Tema