Backends for Frontends
คุณเอา API Gateway มาใช้แล้วและได้ผลดี แต่ client แต่ละชนิดไม่เหมือนกัน web app วิ่งบนเน็ตเร็วและต้องการ response แบบละเอียดครบถ้วนเพื่อ render dashboard ที่กว้าง ส่วน mobile app วิ่งบนสัญญาณมือถือที่ไม่นิ่ง จึงต้องการ response ที่เล็กกระชับ เอาเฉพาะ field ที่หน้าจอเล็กแสดงจริง ส่วน third-party integration ต้องการ endpoint ที่นิ่ง อนุรักษ์นิยม มีเวอร์ชันชัด และเปลี่ยนน้อยที่สุด gateway ตัวเดียวที่พยายามเอาใจทั้งสามฝ่ายจะค่อย ๆ งอก logic แบบมีเงื่อนไขและกรณีพิเศษ “ใส่ field นี้เฉพาะ mobile” เพิ่มขึ้นไม่หยุด
gateway ตัวเดียวที่ client ทุกชนิดใช้ร่วมกันจะกลายเป็นจุดที่ทุกทีมมาแย่งกันแก้และโตไม่หยุด ทีม client ทุกทีมต้องนัดแนะการเปลี่ยนแปลงผ่าน codebase ก้อนเดียวกัน ทีม mobile ต้องรอทีม web และทีม web ก็ต้องรอทีม mobile สุดท้าย API ก็ไหลไปหารูปร่างกลาง ๆ ที่ไม่เข้ากับ client ตัวไหนเลย หนักไปสำหรับ mobile บางไปสำหรับ web แถมความเป็นเจ้าของยังคลุมเครือ เพราะเมื่อสามทีมพึ่ง gateway ตัวเดียวกัน แล้วใครคือเจ้าของ คำถามคือคุณจะรักษาข้อดีของจุดเข้าจุดเดียวไว้ พร้อมกับให้ client แต่ละตัวพัฒนา API ของตัวเองตามจังหวะของตัวเองได้อย่างไร
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”Backends for Frontends (BFF) เอา gateway pattern มาใช้ซ้ำหนึ่งชุดต่อ client หนึ่งชนิด แทนที่จะมี gateway ตัวเดียว คุณรัน gateway แยกให้ client แต่ละชนิด ได้แก่ web BFF, mobile BFF, third-party BFF แต่ละ BFF เปิด API ที่ปั้นรูปร่างมาให้พอดีกับ client ของตัวเอง mobile BFF คืน payload ที่ตัดทอนแล้ว web BFF คืนผลรวมแบบละเอียดครบ ส่วน third-party BFF เปิด contract ที่นิ่งและเปลี่ยนเวอร์ชันช้า ๆ
จุดเปลี่ยนที่สำคัญที่สุดคือเรื่องความเป็นเจ้าของ แต่ละ BFF มีทีมที่สร้าง client นั้นเป็นเจ้าของ ทีม mobile ดูแล mobile BFF และปล่อยการเปลี่ยนแปลงทั้งฝั่ง client และฝั่ง backend พร้อมกันได้เลย ไม่ต้องนัดแนะข้ามทีม ส่วน service ภายในที่ใช้ร่วมกันยังคงอยู่หลัง BFF ทุกตัวโดยไม่ถูกแตะ
flowchart LR Web[Web App] --> WBFF[Web BFF] Mobile[Mobile App] --> MBFF[Mobile BFF] Third[Third-party client] --> TBFF[Third-party BFF] WBFF --> O[Order Service] WBFF --> C[Customer Service] MBFF --> O MBFF --> I[Inventory Service] TBFF --> O TBFF --> C
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”แต่ละ BFF เป็น deployable แยกของตัวเอง พร้อม configuration ที่ปรับมาให้เข้ากับ client ของตัวเอง ตัวอย่างข้างล่างคือ configuration ของ route สอง BFF ที่วางอยู่บน service ภายในชุดเดียวกัน สังเกตว่า mobile BFF ตัด field ทิ้งและตั้ง limit ไว้เข้ม ส่วน web BFF คืนผลรวมแบบละเอียดครบ
# mobile-bff.yaml — small payloads, conservative limits for cellular clientslisten: ":443"routes: - path: /m/order/:id aggregate: - get: http://order-service/orders/:id as: order - get: http://inventory-service/stock/:sku as: stock response_shape: fields: [order.id, order.status, order.total, stock.available] # trimmed rate_limit: requests_per_minute: 60---# web-bff.yaml — rich aggregates for a fast connection and a wide screenlisten: ":443"routes: - path: /w/order/:id aggregate: - get: http://order-service/orders/:id as: order - get: http://customer-service/customers/:customerId as: customer - get: http://inventory-service/stock/:sku as: stock response_shape: fields: [order, customer, stock] # full objects rate_limit: requests_per_minute: 240ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้:
- API ที่ตัดเย็บให้พอดีตัว client แต่ละตัวได้ response ที่ปั้นมาให้เข้ากับหน้าจอ เครือข่าย และความต้องการของตัวเองพอดี เล็กสำหรับ mobile ละเอียดสำหรับ web นิ่งสำหรับ third party โดยไม่ต้องประนีประนอมกับฝ่ายอื่น
- ความเป็นเจ้าของและความเป็นอิสระที่ชัดเจน ทีม client แต่ละทีมเป็นเจ้าของ BFF ของตน และส่งการเปลี่ยนแปลง client กับ backend พร้อมกัน ขจัดการประสานงานข้ามทีมที่ gateway ที่ใช้ร่วมกันบังคับให้ทำ
- blast radius เล็กลง การเปลี่ยนแปลงหรือบั๊กใน mobile BFF ทำให้ web client พังไม่ได้ เพราะเป็นคนละ deployable กัน
สิ่งที่คุณต้องจ่าย:
- มีการทำซ้ำบ้าง กฎการจัดเส้นทาง การเชื่อมต่อการยืนยันตัวตน และ logic การรวมผลลัพธ์เกิดซ้ำกันในหลาย BFF concern ที่ใช้ร่วมกันสามารถถูกแยกออกไปเป็น library ที่ใช้ร่วมกันได้ แต่คุณยอมรับว่ามีชิ้นส่วนเคลื่อนไหวมากกว่า gateway เดียว
- มีองค์ประกอบให้ทำงานมากขึ้น BFF หลายตัวหมายถึงหลายสิ่งที่ต้อง deploy เฝ้าติดตาม และทำให้พร้อมใช้งาน แทนที่จะเป็นเพียงตัวเดียว
- มีขอบเขตที่ต้องคอยควบคุม แต่ละ BFF ต้องคงสภาพเป็นชั้นปรับแต่งบาง ๆ ถ้า BFF เฉพาะ client เริ่มดูดซับ business logic คุณก็จะสร้างปัญหา God-object ที่ pattern นี้ตั้งใจจะแก้ขึ้นมาใหม่ — เพียงแต่กระจายอยู่ในหลายที่
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- API Gateway — pattern แบบ gateway เดียวที่ BFF ทำให้พิเศษขึ้นต่อ client หนึ่งชนิด
- Service Discovery — วิธีที่แต่ละ BFF หาตำแหน่งของ service ภายในที่ต้องเรียก
- API Composition — การรวมผลลัพธ์ที่แต่ละ BFF ทำเพื่อประกอบการตอบกลับที่ปรับให้เข้ากับ client
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| แต่ละ client type ได้ API ที่ optimize ให้ตัวเอง | เพิ่มจำนวน backend ที่ต้องดูแล — N client types = N BFF |
| mobile client ไม่ต้องรับ payload ที่ออกแบบสำหรับ web | logic ซ้ำระหว่าง BFF ถ้าไม่ระวัง |
| team ของแต่ละ frontend ดูแล BFF ของตัวเองได้ | BFF อาจกลายเป็น fat middleware ถ้าใส่ business logic |
| เปลี่ยน frontend โดยไม่กระทบ BFF ของ client อื่น | เพิ่ม hop ใน request chain — latency สูงขึ้นเล็กน้อย |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Shared BFF — BFF เดียวสำหรับทุก client type ทำให้กลับไปเป็นปัญหาเดิม
อาการ:
- BFF มีเงื่อนไข if mobile / if web ปะปนกัน
- ทีม mobile และ web ทะเลาะกันเรื่อง API shape
- release ของ BFF กระทบทุก client พร้อมกัน
Business Logic BFF — ใส่ business rule ไว้ใน BFF แทน service
อาการ:
- pricing logic, discount rule อยู่ใน BFF
- เปลี่ยน business rule ต้อง deploy BFF ใหม่ทุกตัว
💡 ตัวอย่างจากของจริง
SoundCloud:
- เป็นผู้บัญญัติคำว่า “Backend for Frontend” ในปี 2015
- มี BFF แยกสำหรับ web, mobile, และ public API
- แต่ละ BFF ดูแลโดย frontend team ของตัวเอง
Netflix:
- มี BFF แยกสำหรับ Smart TV, Mobile, Browser
- แต่ละ device type ได้ response format ที่ optimize สำหรับ screen size และ bandwidth