ข้ามไปยังเนื้อหา

Federation

GraphQL service เดียวเป็นคำตอบที่ใช้ได้ไปอีกนาน แต่พอบริษัทโตขึ้น schema เดียวที่ทีมเดียวดูแลจะกลายเป็นคอขวด เพราะทุกการเปลี่ยนแปลงต้องลอดผ่าน codebase เดียว deploy เดียว และ reviewer ชุดเดียว Federation คือ pattern ที่ให้หลายทีมเป็นเจ้าของกราฟคนละชิ้น โดยที่ client ยังเห็น API เดียวที่รวมเป็นหนึ่ง

แนวคิดคือการแยกกราฟออกเป็น subgraph — GraphQL service อิสระ แต่ละตัวเป็นเจ้าของ type และ field ของ domain เดียว ทีม accounts เป็นเจ้าของ User ทีม catalog เป็นเจ้าของ Product ทีม reviews เป็นเจ้าของ Review แต่ละ subgraph ก็คือ GraphQL server ปกติที่รันแยกเดี่ยว ๆ ได้

ด้านหน้าของ subgraph ทั้งหมดจะมี gateway (หรือ router) คอยรับงาน gateway อ่าน schema ของทุก subgraph แล้ว compose เข้าเป็น schema รวมหนึ่งเดียวที่เรียกว่า supergraph จากนั้นเปิด schema เดียวนั้นให้ client ใช้ ฝั่ง client ส่ง query แค่ครั้งเดียว gateway จะคิดเองว่าต้องเรียก subgraph ไหนบ้าง ตามลำดับใด แล้วเย็บชิ้นส่วนกลับมาเป็น response เดียว

flowchart TD
  Client["Client (one query)"]
  Gateway["Gateway / Router"]
  Super["Supergraph (composed schema)"]
  Accounts["accounts subgraph (User)"]
  Catalog["catalog subgraph (Product)"]
  Reviews["reviews subgraph (Review)"]
  Client --> Gateway
  Gateway --> Super
  Gateway --> Accounts
  Gateway --> Catalog
  Gateway --> Reviews
  Reviews -->|"author: User via @key id"| Accounts
แต่ละทีมเป็นเจ้าของ subgraph ของตัวเอง gateway compose ทั้งหมดเป็น supergraph เดียวที่ client ยิง query เข้ามา

ข้อได้เปรียบจริง ๆ เป็นเรื่อง องค์กร ทีม reviews ปล่อยการเปลี่ยนแปลงของ Review ได้เลย โดยไม่ต้องแตะหรือรอประสานงานกับทีม accounts กราฟจึงขยายไปตามผังองค์กร แทนที่จะขัดกับผังองค์กร

สิ่งที่ทำให้ Review เชื่อมไปหา User ได้ทั้งที่อยู่คนละ service คือ entity ซึ่งก็คือ type ที่ subgraph หลายตัวมาช่วยกันเติมได้ โดยระบุตัวตนด้วย key คุณ mark ด้วย @key พร้อมบอก field ที่ใช้ระบุตัวตนแบบไม่ซ้ำ

subgraph accounts นิยาม entity User:

# accounts subgraph
type User @key(fields: "id") {
id: ID!
name: String!
}
type Query {
me: User
}

subgraph reviews อ้างอิง User โดยไม่ได้เป็นเจ้าของข้อมูล จึงประกาศ entity ด้วย extend โดยใช้ key เดียวกัน พร้อม placeholder ของ field ที่ต้องใช้ แล้วเพิ่ม link author ของตัวเองเข้าไป:

# reviews subgraph
type Review {
id: ID!
body: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
}

เมื่อ client ขอ review { author { name } } gateway จะเรียก subgraph reviews เพื่อเอา review กับ author.id มาก่อน จากนั้นขอให้ subgraph accounts resolve the reference — “เอา User ที่มี id นี้มาให้หน่อย” แต่ละ subgraph จะ implement reference resolver ที่แปลง key ให้กลายเป็น entity เต็ม:

// accounts subgraph resolvers
const resolvers = {
User: {
// The gateway hands over the key; the owning subgraph returns the entity.
__resolveReference(ref: { id: string }) {
return findUserById(ref.id);
},
},
};

__resolveReference คือรอยต่อ (seam) ของ federation ฝั่ง subgraph reviews ไม่เคย query database ของ accounts เลย แค่ส่ง reference ของ User ออกมาเป็น id แล้ว subgraph เจ้าของข้อมูลก็เติมส่วนที่เหลือให้ ความเป็นเจ้าของจึงยังชัดเจน แม้ type เดียวจะมีหลาย service มาช่วยเสริม

gateway ไม่ได้รวม schema แบบมั่ว ๆ แต่จะ validate composition ก่อน subgraph สองตัวจะนิยาม field เดียวกันบน type เดียวกันแบบขัดแย้งกันไม่ได้ ทุก entity ที่ถูกอ้างถึงต้องมีนิยามอยู่ที่ใดที่หนึ่งพร้อม @key ที่ตรงกัน และ field @external ต้องมีอยู่จริงบน subgraph เจ้าของ ถ้า compose ไม่ผ่าน supergraph จะไม่ถูก publish แปลว่าการเปลี่ยนแปลงที่พังถูกจับได้ก่อน client คนไหนจะเห็น composition มีบทบาทกับ federation เหมือนที่ code generation มีกับบทที่แล้ว คือให้เครื่องตรวจว่า contract ที่รวมกันแล้วยังสอดคล้องกันอยู่

ความผิดพลาดที่พบบ่อยที่สุดของ federation คือการแยกตาม layer ทางเทคนิค เช่น “database subgraph” หรือ “validation subgraph” วิธีนี้สร้างคอขวดขึ้นมาใหม่ เพราะทุกฟีเจอร์ยังต้องแตะทุก layer อยู่ดี ให้แยกตาม domain แทน — accounts, catalog, reviews, billing แต่ละ subgraph จะได้ตรงกับทีมที่เป็นเจ้าของงานแนวตั้งทั้งชิ้นแบบ end to end และรอยต่อของ @key/reference ก็จะตกลงบนขอบเขต domain ตามธรรมชาติ (review มี author หนึ่งคน, product มี reviews)

ข้อดีข้อแลกเปลี่ยน
แต่ละ team ดูแล subgraph ของตัวเอง — schema ownership ชัดเจนinfrastructure ซับซ้อนกว่า monolithic schema
extend type ข้าม subgraph ได้ — User ใน product service มี field จาก review servicelearning curve สูง — ต้องเข้าใจ entity, @key, @external
deploy subgraph อิสระโดยไม่กระทบ subgraph อื่นgateway เป็น single point of failure ถ้าไม่ design ดี
scale แต่ละ subgraph ตาม load ของตัวเองsubgraph ที่ schema ไม่ compatible ทำให้ federation fail

Federation ตั้งแต่ต้น โดยไม่มีหลาย Team อาการ:

  • 1 team ดูแล subgraph ทั้งหมด
  • ซับซ้อนกว่า monolithic schema โดยไม่ได้ประโยชน์
  • เริ่มด้วย monolithic schema แล้ว federate เมื่อ team แยกกันจริง

Circular Entity Reference อาการ:

  • User subgraph reference Order และ Order subgraph reference User แบบ circular
  • gateway ต้องทำ round trip ซ้ำซ้อนเพื่อ resolve
  • ออกแบบ entity ownership ให้ชัดเจน — ใคร owns ใคร

💡 ตัวอย่างจากของจริง

Airbnb:

  • ใช้ Apollo Federation สำหรับ internal GraphQL platform
  • แต่ละ team ดูแล subgraph: listing, booking, user, review
  • schema gateway รวม subgraph สำหรับ mobile และ web client

Netflix:

  • Federated GraphQL ช่วยให้ 100+ team ทำงานอิสระ
  • schema registry ติดตาม breaking change ก่อน deploy
subgraph ในสถาปัตยกรรม GraphQL แบบ federated คืออะไร?
gateway สร้างอะไรขึ้นมาด้วยการรวม schema ของ subgraph เข้าด้วยกัน?
__resolveReference ทำอะไร?
ควรแยกกราฟออกเป็น subgraph อย่างไร?