GraphQL Federation — Gộp API Microservices thành một Supergraph thống nhất

Posted on: 4/22/2026 2:12:26 AM

Trong thế giới microservices, mỗi team sở hữu một service riêng biệt với schema API của riêng mình. Nhưng phía client — web app, mobile app — chỉ muốn một endpoint duy nhất để query tất cả dữ liệu cần thiết. Đây chính là bài toán mà GraphQL Federation giải quyết: cho phép nhiều subgraph (mỗi subgraph là một microservice) được compose thành một supergraph thống nhất, với một gateway ở giữa điều phối mọi thứ.

330+ Edge locations (Apollo Router Cloud)
<0.5ms Query planning overhead
Netflix, Expedia Production adopters
10K+ QPS ~120MB RAM (Router)

1. Vấn đề: API Gateway truyền thống không scale theo tổ chức

Khi hệ thống microservices mở rộng lên hàng chục, hàng trăm service, mô hình API Gateway truyền thống (REST) gặp phải nhiều vấn đề nghiêm trọng:

  • Over-fetching / Under-fetching: Client phải gọi nhiều endpoint riêng lẻ, nhận về dữ liệu thừa hoặc thiếu, rồi tự ghép nối.
  • Gateway monolith: Một team trung tâm phải maintain toàn bộ routing logic, trở thành bottleneck của tổ chức.
  • Schema coupling: Khi service A thêm field mới, gateway phải deploy lại — dù service B, C không liên quan.
  • API versioning hell: REST buộc phải duy trì v1, v2, v3... song song, tăng complexity theo thời gian.

Tại sao không dùng GraphQL thường?

GraphQL (monolithic) giải quyết over-fetching nhưng lại tạo ra single schema monolith — một file schema khổng lồ mà mọi team phải commit vào cùng repo. Federation giải phóng constraint đó bằng cách cho mỗi team sở hữu subgraph riêng.

2. GraphQL Federation là gì?

GraphQL Federation là một kiến trúc cho phép bạn chia GraphQL schema thành nhiều subgraph độc lập, mỗi subgraph do một team/service sở hữu. Một router (gateway) sẽ tự động compose (gộp) tất cả subgraph thành một supergraph duy nhất mà client tương tác.

graph TD
    Client["🖥️ Client App"] --> Router["Apollo Router
(Supergraph Gateway)"] Router --> S1["Subgraph: Users
Team A"] Router --> S2["Subgraph: Products
Team B"] Router --> S3["Subgraph: Orders
Team C"] Router --> S4["Subgraph: Reviews
Team D"] style Client fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style Router fill:#e94560,stroke:#fff,color:#fff style S1 fill:#2c3e50,stroke:#fff,color:#fff style S2 fill:#2c3e50,stroke:#fff,color:#fff style S3 fill:#2c3e50,stroke:#fff,color:#fff style S4 fill:#2c3e50,stroke:#fff,color:#fff
Kiến trúc GraphQL Federation: Client → Router → Subgraphs

2.1. Ba khái niệm cốt lõi

Khái niệm Mô tả Ví dụ
Subgraph Một GraphQL service độc lập, định nghĩa một phần schema. Mỗi team sở hữu ≥1 subgraph. Users subgraph định nghĩa type User
Supergraph Schema tổng hợp, được compose tự động từ tất cả subgraph. Client chỉ thấy supergraph. Query { user { orders { items } } } xuyên suốt 3 subgraph
Router Gateway nhận query từ client, phân tích query plan, gọi đúng subgraph cần thiết, rồi merge kết quả. Apollo Router, Cosmo Router, Grafbase Gateway

3. Cách Federation compose schema: Directives quan trọng

Federation sử dụng một số directive đặc biệt để các subgraph "nói chuyện" với nhau qua router, mà không cần biết nhau trực tiếp.

3.1. @key — Định danh entity liên kết

Directive @key đánh dấu một type là entity — có thể được resolve bởi nhiều subgraph khác nhau dựa trên primary key.

# Subgraph: Users (Team A sở hữu)
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
  avatar: String
}

type Query {
  user(id: ID!): User
  users: [User!]!
}
# Subgraph: Reviews (Team D sở hữu)
# Team D "mở rộng" type User mà KHÔNG cần sửa Users subgraph
type User @key(fields: "id") {
  id: ID!
  reviews: [Review!]!    # Team D thêm field này
}

type Review @key(fields: "id") {
  id: ID!
  rating: Int!
  comment: String!
  author: User!
}

Entity Resolution

Khi router nhận query { user(id: "1") { name reviews { rating } } }, nó sẽ: (1) gọi Users subgraph lấy name, (2) gọi Reviews subgraph với __resolveReference({ id: "1" }) để lấy reviews, rồi (3) merge kết quả. Client không biết data đến từ 2 service khác nhau.

3.2. @external, @requires, @provides — Cross-subgraph dependencies

# Subgraph: Products
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
  weight: Float!
}

# Subgraph: Shipping
type Product @key(fields: "id") {
  id: ID!
  weight: Float! @external        # Khai báo field từ subgraph khác
  shippingCost: Float! @requires(fields: "weight")  # Cần weight để tính
}

Khi client query shippingCost, router tự động fetch weight từ Products subgraph trước, rồi truyền cho Shipping subgraph để tính phí.

3.3. @shareable — Nhiều subgraph cùng resolve một field

# Cả Users và Auth subgraph đều có thể resolve email
type User @key(fields: "id") {
  id: ID!
  email: String! @shareable
}

4. Apollo Router — Bộ não của Supergraph

Apollo Router (viết bằng Rust) là thành phần trung tâm trong kiến trúc Federation. Nó thay thế Apollo Gateway (Node.js) với hiệu năng vượt trội.

graph LR
    Q["Client Query"] --> QP["Query Planner
(phân tích cần subgraph nào)"] QP --> EE["Execution Engine
(gọi song song các subgraph)"] EE --> S1["Subgraph A"] EE --> S2["Subgraph B"] EE --> S3["Subgraph C"] S1 --> MR["Response Merger
(ghép kết quả)"] S2 --> MR S3 --> MR MR --> R["Response → Client"] style Q fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style QP fill:#e94560,stroke:#fff,color:#fff style EE fill:#e94560,stroke:#fff,color:#fff style MR fill:#e94560,stroke:#fff,color:#fff style R fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style S1 fill:#2c3e50,stroke:#fff,color:#fff style S2 fill:#2c3e50,stroke:#fff,color:#fff style S3 fill:#2c3e50,stroke:#fff,color:#fff
Luồng xử lý query trong Apollo Router
Tiêu chí Apollo Gateway (Node.js) Apollo Router (Rust)
RAM @ 10K QPS ~800 MB ~120 MB
Query planning overhead 3–5 ms ~0.5 ms
Cold start Vài giây (Node.js boot) <100 ms
Plugin system JavaScript middleware Rhai scripting + Rust native
Ngôn ngữ JavaScript/TypeScript Rust
Trạng thái Legacy (không khuyến khích) Production-ready, actively developed

4.1. Cấu hình Router cơ bản

# router.yaml
supergraph:
  listen: 0.0.0.0:4000
  introspection: true

cors:
  origins:
    - https://app.example.com

headers:
  all:
    request:
      - propagate:
          named: "Authorization"
      - propagate:
          named: "X-Request-Id"

telemetry:
  exporters:
    tracing:
      otlp:
        enabled: true
        endpoint: http://otel-collector:4317

limits:
  max_depth: 15
  max_height: 200

coprocessor:
  url: http://auth-service:8080
  router:
    request:
      headers: true

5. Thiết kế Subgraph đúng cách

5.1. Nguyên tắc Separation of Concerns

Mỗi subgraph nên đại diện cho một bounded context trong domain — tương tự Domain-Driven Design.

graph TD
    subgraph "E-commerce Supergraph"
        UG["Users Subgraph
Profiles, Auth, Preferences"] PG["Products Subgraph
Catalog, Inventory, Pricing"] OG["Orders Subgraph
Cart, Checkout, Fulfillment"] RG["Reviews Subgraph
Ratings, Comments, Moderation"] SG["Search Subgraph
Full-text, Filters, Suggestions"] end UG -.->|"@key: User.id"| RG PG -.->|"@key: Product.id"| OG PG -.->|"@key: Product.id"| RG UG -.->|"@key: User.id"| OG PG -.->|"@key: Product.id"| SG style UG fill:#e94560,stroke:#fff,color:#fff style PG fill:#2c3e50,stroke:#fff,color:#fff style OG fill:#4CAF50,stroke:#fff,color:#fff style RG fill:#ff9800,stroke:#fff,color:#fff style SG fill:#9C27B0,stroke:#fff,color:#fff
Ví dụ phân chia subgraph theo bounded context trong hệ thống e-commerce

5.2. Entity ownership vs. contribution

Quy tắc vàng

Chỉ một subgraph sở hữu (own) một entity — nó định nghĩa các field "core" và resolver chính. Các subgraph khác chỉ contribute (đóng góp) thêm field mới cho entity đó qua directive @key. Ví dụ: Users subgraph owns User.name, User.email; Reviews subgraph contributes User.reviews.

5.3. Tránh circular dependencies

# ❌ TRÁNH: Circular reference sâu
# Users subgraph
type User @key(fields: "id") {
  id: ID!
  orders: [Order!]!      # User → Order
}

# Orders subgraph
type Order @key(fields: "id") {
  id: ID!
  buyer: User!            # Order → User
  items: [OrderItem!]!
}

type OrderItem {
  product: Product!       # OrderItem → Product
}

# Products subgraph
type Product @key(fields: "id") {
  id: ID!
  reviews: [Review!]!    # Product → Review
}

# Reviews subgraph
type Review {
  author: User!           # Review → User → vòng lặp!
# ✅ TỐT HƠN: Giữ reference một chiều, dùng ID khi cần
# Reviews subgraph
type Review @key(fields: "id") {
  id: ID!
  rating: Int!
  comment: String!
  authorId: ID!           # Chỉ trả ID, client tự resolve nếu cần
  author: User!           # Hoặc vẫn resolve nhưng shallow (chỉ @key fields)
}

6. Schema Composition và CI/CD

Trong production, schema composition diễn ra trong CI/CD pipeline — không phải runtime. Điều này đảm bảo mọi breaking change được phát hiện trước khi deploy.

graph LR
    Dev["Developer pushes
subgraph change"] --> CI["CI Pipeline"] CI --> Check["rover subgraph check
(composition + breaking change detection)"] Check -->|Pass| Pub["rover subgraph publish
(update schema registry)"] Check -->|Fail| Fix["Fix schema
incompatibility"] Pub --> Router["Router hot-reloads
new supergraph schema"] Fix --> Dev style Dev fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style CI fill:#2c3e50,stroke:#fff,color:#fff style Check fill:#e94560,stroke:#fff,color:#fff style Pub fill:#4CAF50,stroke:#fff,color:#fff style Fix fill:#ff9800,stroke:#fff,color:#fff style Router fill:#2c3e50,stroke:#fff,color:#fff
CI/CD workflow cho schema composition với Apollo GraphOS

6.1. Rover CLI — Công cụ quản lý schema

# Kiểm tra subgraph change có tương thích không
rover subgraph check my-graph@production \
  --name users \
  --schema ./users/schema.graphql

# Publish subgraph lên registry
rover subgraph publish my-graph@production \
  --name users \
  --schema ./users/schema.graphql \
  --routing-url http://users-service:4001/graphql

# Fetch supergraph schema hiện tại
rover supergraph fetch my-graph@production

6.2. Breaking change detection

Loại thay đổi Severity Ví dụ
Xóa field đang được query 🔴 Breaking Xóa User.email khi 50 operation đang dùng
Đổi type của field 🔴 Breaking price: Float!price: String!
Thêm required argument 🟡 Potentially breaking users(limit: Int!) — client cũ thiếu argument
Thêm field mới 🟢 Safe Thêm User.lastLoginAt
Deprecate field 🟢 Safe email: String! @deprecated(reason: "Use contactEmail")

7. Query Planning và Execution — Hiểu sâu cách Router hoạt động

Khi router nhận một query, nó thực hiện query planning — phân tích query thành execution plan gọi đúng subgraph cần thiết, theo đúng thứ tự dependency.

# Client gửi query này:
query GetUserDashboard($userId: ID!) {
  user(id: $userId) {
    name                    # → Users subgraph
    email                   # → Users subgraph
    orders(last: 5) {       # → Orders subgraph
      id
      total
      items {
        product {
          name              # → Products subgraph
          price             # → Products subgraph
        }
      }
    }
    reviews {               # → Reviews subgraph
      rating
      comment
    }
  }
}

Query Plan của Router

Router tạo execution plan: Fetch User (parallel) → Fetch Orders + Reviews → Fetch Products. Các subgraph call không phụ thuộc nhau sẽ chạy song song. Router cũng cache query plan (plan caching) nên query giống nhau lần sau chỉ mất ~0.01ms thay vì ~0.5ms.

8. Bảo mật và Authorization trong Federation

Authorization trong federated graph phức tạp hơn monolith vì data đến từ nhiều service. Có 2 pattern phổ biến:

8.1. Router-level authentication + Subgraph-level authorization

# router.yaml — xác thực JWT tại router
authentication:
  router:
    jwt:
      jwks:
        url: https://auth.example.com/.well-known/jwks.json
      header_name: Authorization
      header_value_prefix: Bearer
# Subgraph: Orders — phân quyền tại subgraph
type Order @key(fields: "id") @authenticated {
  id: ID!
  total: Float!
  buyerId: ID!
  items: [OrderItem!]! @requiresScopes(scopes: [["order:read"]])
  internalNotes: String @requiresScopes(scopes: [["admin"]])
}

8.2. Directive @policy cho business rules

type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String! @policy(policies: [["self_or_admin"]])
  salary: Float! @policy(policies: [["hr_department"]])
}

9. Performance Optimization

9.1. DataLoader pattern — Giải quyết N+1 query

Khi query danh sách orders có buyer, nếu không dùng DataLoader, Users subgraph sẽ bị gọi N lần (mỗi order 1 lần). DataLoader batch các request thành 1 call duy nhất.

// Users subgraph — Reference resolver với DataLoader
import DataLoader from 'dataloader';

const userLoader = new DataLoader<string, User>(async (ids) => {
  const users = await db.users.findMany({
    where: { id: { in: ids as string[] } }
  });
  const userMap = new Map(users.map(u => [u.id, u]));
  return ids.map(id => userMap.get(id)!);
});

const resolvers = {
  User: {
    __resolveReference(ref: { id: string }) {
      return userLoader.load(ref.id);
    }
  }
};

9.2. @defer — Streaming response

# Client nhận name/email ngay, reviews load sau (streaming)
query {
  user(id: "1") {
    name
    email
    ... @defer(label: "reviews") {
      reviews {
        rating
        comment
      }
    }
  }
}

9.3. Persisted Queries

# router.yaml
persisted_queries:
  enabled: true
  safelist:
    enabled: true        # Chỉ cho phép query đã đăng ký
    require_id: true     # Client gửi hash thay vì full query text

Lưu ý hiệu năng

Tránh thiết kế subgraph có quá nhiều entity reference qua lại — mỗi hop giữa các subgraph thêm ~2-5ms network latency. Nếu một query phải qua 4-5 subgraph lần lượt (sequential), tổng latency có thể lên 20-30ms chỉ riêng overhead. Thiết kế sao cho phần lớn query chỉ cần 1-2 hop.

10. Federation trong .NET với Hot Chocolate

Nếu bạn dùng .NET, Hot Chocolate (ChilliCream) hỗ trợ Federation 2 đầy đủ:

// Program.cs — .NET 10 subgraph
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddGraphQLServer()
    .AddApolloFederationV2()
    .AddQueryType<Query>()
    .AddType<UserType>()
    .AddType<ReviewType>()
    .RegisterService<IReviewRepository>();

var app = builder.Build();
app.MapGraphQL();
app.Run();
// UserType.cs — Entity type với @key
[Key("id")]
public class UserType : ObjectType<User>
{
    protected override void Configure(IObjectTypeDescriptor<User> descriptor)
    {
        descriptor.Field(u => u.Id).Type<NonNullType<IdType>>();
        descriptor.Field("reviews")
            .ResolveWith<UserResolvers>(r => r.GetReviews(default!, default!));
    }
}

// Reference resolver — router gọi khi cần resolve User entity
[ReferenceResolver]
public static async Task<User?> GetUserById(
    [ID] int id,
    IReviewRepository repo)
{
    return await repo.GetUserReferenceAsync(id);
}

11. Monitoring và Observability

Trong federated architecture, observability đặc biệt quan trọng vì một query có thể đi qua nhiều subgraph:

11.1. Distributed tracing

# router.yaml — OpenTelemetry tracing
telemetry:
  exporters:
    tracing:
      otlp:
        enabled: true
        endpoint: http://otel-collector:4317
        protocol: grpc
  instrumentation:
    spans:
      router:
        attributes:
          graphql.document: true
      subgraph:
        attributes:
          subgraph.name: true
          graphql.operation.name: true

11.2. Các metric cần monitor

Metric Mô tả Threshold cảnh báo
apollo.router.query_planning.duration Thời gian tạo query plan >5ms (có thể cần plan cache warming)
apollo.router.http.request.duration Tổng thời gian request end-to-end p99 >500ms
subgraph.request.duration per subgraph Latency từng subgraph Subgraph nào chậm nhất = bottleneck
apollo.router.cache.hit_rate Tỷ lệ query plan cache hit <90% là đáng lo
graphql.error.count Số lỗi GraphQL (partial response) Tăng đột biến = subgraph down

12. Khi nào KHÔNG nên dùng Federation?

Federation không phải silver bullet

  • Team nhỏ (<3 backend devs): Overhead quản lý nhiều subgraph, router, schema registry vượt quá lợi ích. Dùng monolithic GraphQL là đủ.
  • Schema ít thay đổi: Nếu API schema ổn định, REST + OpenAPI vẫn đơn giản hơn nhiều.
  • Latency-critical (sub-ms): Mỗi hop qua router thêm overhead — high-frequency trading hoặc game server nên dùng gRPC trực tiếp.
  • Chưa có domain boundaries rõ ràng: Federation ép bạn phải phân chia subgraph — nếu domain model chưa mature, bạn sẽ phải refactor liên tục.

13. Alternatives so sánh

Giải pháp Ưu điểm Nhược điểm Phù hợp khi
GraphQL Federation Unified schema, team autonomy, type-safe Complexity, learning curve, router overhead Large org, nhiều team, domain phức tạp
REST + API Gateway Đơn giản, tooling mature, caching dễ Over-fetching, nhiều endpoint, versioning CRUD apps, public API, team nhỏ
gRPC + Envoy Hiệu năng cao, strong typing, streaming Không friendly cho frontend, binary protocol Service-to-service, latency-critical
Schema Stitching Merge schema đơn giản hơn Không scale theo tổ chức, fragile 2-3 service, prototype nhanh
2019
Apollo giới thiệu Federation v1 — lần đầu tiên microservices có thể compose GraphQL schema phân tán.
2021
Federation v2 ra mắt: progressive migration, @shareable, @override, @inaccessible directives. Giải quyết pain points lớn nhất của v1.
2023
Apollo Router (Rust) thay thế Apollo Gateway (Node.js). Hiệu năng tăng 6-8x, RAM giảm 85%.
2024
GraphQL Foundation chính thức chuẩn hóa Federation spec — không còn là "Apollo-only".
2025–2026
@defer/@stream hỗ trợ production. Cosmo Router, Grafbase Gateway — hệ sinh thái đa dạng hơn. Netflix, Expedia, Volvo triển khai supergraph quy mô lớn.

Kết luận

GraphQL Federation là giải pháp kiến trúc mạnh mẽ cho bài toán "làm sao để nhiều team cùng xây dựng một API thống nhất mà không tạo bottleneck". Với Apollo Router (Rust), schema composition trong CI/CD, và hệ sinh thái ngày càng đa dạng (Hot Chocolate cho .NET, federation-jvm cho Java/Kotlin), Federation không còn là experiment mà đã trở thành production-proven pattern cho các tổ chức quy mô lớn.

Tuy nhiên, hãy nhớ: Federation giải quyết vấn đề tổ chức (organizational scaling) trước vấn đề kỹ thuật. Nếu team bạn nhỏ và domain đơn giản, một monolithic GraphQL server vẫn là lựa chọn tốt hơn.

Nguồn tham khảo: