Pattern: Backend for Frontend (BFF) (แพตเทิร์น: Backend for Frontend หรือ BFF)

ความแตกต่างหลักระหว่าง BFF กับ central aggregating gateway คือ BFF มีลักษณะเป็น single purpose คือถูกพัฒนาขึ้นมาเพื่อส่วนติดต่อผู้ใช้เฉพาะอย่างใดอย่างหนึ่ง แพตเทิร์นนี้พิสูจน์แล้วว่าประสบความสำเร็จอย่างมากในการช่วยจัดการประเด็นที่แตกต่างกันของส่วนติดต่อผู้ใช้ และผมเคยเห็นมันทำงานได้ดีในหลายองค์กร รวมถึง SoundCloud 6 และ REA ดังที่เราเห็นใน Figure 14-11 ที่เรากลับมาดู MusicCorp อีกครั้ง web และ mobile shopping interface ตอนนี้มี aggregating backend เป็นของตัวเองแล้ว

bms2 1411

Figure 14-11. ส่วนติดต่อผู้ใช้แต่ละแบบมี BFF ของตัวเอง

เพราะลักษณะเฉพาะทางของมัน BFF จึงหลีกเลี่ยงประเด็นบางอย่างเกี่ยวกับ central aggregating gateway ได้ เนื่องจากเราไม่ได้พยายามเป็นทุกอย่างให้กับทุกคน BFF จึงไม่กลายเป็น bottleneck สำหรับการพัฒนา ที่มีหลายทีมพยายามแบ่งความเป็นเจ้าของร่วมกัน เรายังกังวลน้อยลงเรื่อง coupling กับส่วนติดต่อผู้ใช้ด้วย เพราะ coupling เป็นสิ่งที่ยอมรับได้มากกว่า BFF หนึ่งๆ นั้นมีไว้สำหรับส่วนติดต่อผู้ใช้เฉพาะหนึ่งตัว สมมติว่าพวกมันเป็นของทีมเดียวกัน coupling ที่มีอยู่ก็จัดการได้ง่ายกว่ามาก ผมมักอธิบายการใช้ BFF กับส่วนติดต่อผู้ใช้ว่าเหมือนกับว่า UI ถูกแบ่งออกเป็นสองส่วนจริงๆ ส่วนหนึ่งอยู่บนอุปกรณ์ไคลเอนต์ (web interface หรือ native mobile application) ส่วนที่สองคือ BFF ซึ่งฝังอยู่ฝั่งเซิร์ฟเวอร์

BFF ถูก couple อย่างแน่นหนากับประสบการณ์ผู้ใช้เฉพาะหนึ่งตัว และโดยทั่วไปจะถูกดูแลโดยทีมเดียวกันกับส่วนติดต่อผู้ใช้ ทำให้ง่ายขึ้นในการกำหนดและปรับ API ตามที่ UI ต้องการ พร้อมทั้งยังช่วยลดความซับซ้อนของกระบวนการจัดเรียง release ของทั้ง client และ server component ให้สอดคล้องกันด้วย

How Many BFFs? (ควรมี BFF กี่ตัว)

เมื่อพูดถึงการส่งมอบประสบการณ์ผู้ใช้ที่เหมือนกัน (หรือคล้ายกัน) บนแพลตฟอร์มต่างๆ ผมเคยเห็นสองแนวทางที่แตกต่างกัน โมเดลที่ผมชอบ (และเป็นโมเดลที่ผมเห็นบ่อยที่สุด) คือให้มี BFF เดียวสำหรับไคลเอนต์แต่ละประเภทอย่างเคร่งครัด นี่เป็นโมเดลที่ผมเห็นถูกใช้ที่ REA ดังที่แสดงไว้ใน Figure 14-12 แอปพลิเคชัน Android และ iOS แม้จะรองรับ functionality ที่คล้ายกัน แต่ต่างก็มี BFF ของตัวเอง

bms2 1412

Figure 14-12. แอปพลิเคชัน iOS และ Android ของ REA มี BFF ที่แตกต่างกัน

อีกแบบหนึ่งคือการมองหาโอกาสที่จะใช้ BFF เดียวกันสำหรับไคลเอนต์มากกว่าหนึ่งประเภท แม้จะเป็นส่วนติดต่อผู้ใช้ประเภทเดียวกันก็ตาม แอปพลิเคชัน Listener ของ SoundCloud ให้ผู้คนฟังคอนเทนต์บนอุปกรณ์ Android หรือ iOS ของพวกเขาได้ SoundCloud ใช้ BFF เดียวสำหรับทั้ง Android และ iOS ดังที่แสดงใน Figure 14-13

bms2 1413

Figure 14-13. SoundCloud แชร์ BFF เดียวกันระหว่างแอปพลิเคชัน iOS และ Android

ความกังวลหลักของผมเกี่ยวกับโมเดลที่สองนี้คือ ยิ่งมีไคลเอนต์หลายประเภทใช้ BFF เดียวกันมากเท่าไร ก็ยิ่งมีแนวโน้มที่ BFF จะบวมขึ้นจากการจัดการประเด็นหลากหลายมากขึ้นเท่านั้น สิ่งสำคัญที่ต้องเข้าใจตรงนี้คือ แม้ไคลเอนต์จะแชร์ BFF กัน มันก็ยังคงเป็นสำหรับส่วนติดต่อผู้ใช้ประเภทเดียวกัน ดังนั้นแม้แอปพลิเคชัน Listener แบบ native ของ SoundCloud สำหรับ iOS และ Android จะใช้ BFF เดียวกัน แอปพลิเคชัน native อื่นๆ ก็จะใช้ BFF ที่ต่างกัน ผมรู้สึกสบายใจมากกว่าที่จะใช้โมเดลนี้ถ้าทีมเดียวกันเป็นเจ้าของทั้งแอปพลิเคชัน Android และ iOS และเป็นเจ้าของ BFF ด้วย ถ้าแอปพลิเคชันเหล่านี้ถูกดูแลโดยทีมที่ต่างกัน ผมจะมีแนวโน้มแนะนำโมเดลที่เคร่งครัดกว่ามากกว่า ดังนั้นคุณสามารถมองโครงสร้างองค์กรของคุณเป็นหนึ่งในตัวขับเคลื่อนหลักในการตัดสินใจว่าโมเดลไหนสมเหตุสมผลที่สุด (Conway's law ชนะอีกครั้ง)

Stewart Gleadow จาก REA ได้เสนอแนวทางว่า "หนึ่งประสบการณ์ หนึ่ง BFF" ดังนั้นถ้าประสบการณ์บน iOS และ Android คล้ายกันมาก ก็จะง่ายกว่าที่จะให้เหตุผลสนับสนุนการมี BFF เดียว 7 แต่ถ้าพวกมันแตกต่างกันมาก การมี BFF แยกกันก็สมเหตุสมผลกว่า ในกรณีของ REA แม้จะมีความซ้อนทับกันระหว่างสองประสบการณ์ แต่ทีมที่ต่างกันเป็นเจ้าของพวกมัน และ roll out ฟีเจอร์ที่คล้ายกันในรูปแบบที่ต่างกัน บางครั้งฟีเจอร์เดียวกันอาจถูก deploy ต่างกันบนอุปกรณ์มือถือคนละแบบ สิ่งที่ถือเป็นประสบการณ์แบบ native สำหรับแอปพลิเคชัน Android อาจต้องถูกปรับใหม่เพื่อให้รู้สึก native บน iOS

บทเรียนอีกข้อจากเรื่องราวของ REA (และเป็นสิ่งที่เราคุยกันไปแล้วหลายครั้ง) คือซอฟต์แวร์มักทำงานได้ดีที่สุดเมื่อจัดวางตามขอบเขตของทีม และ BFF ก็ไม่มีข้อยกเว้น การที่ SoundCloud มีทีมมือถือเดียวทำให้การมี BFF เดียวดูสมเหตุสมผลตั้งแต่แรกเห็น เช่นเดียวกับที่ REA มี BFF สองตัวที่ต่างกันสำหรับสองทีมที่แยกจากกัน ควรสังเกตว่าวิศวกรของ SoundCloud ที่ผมคุยด้วยแนะนำว่าการมี BFF เดียวสำหรับแอปพลิเคชัน listener ทั้ง Android และ iOS เป็นสิ่งที่พวกเขาอาจพิจารณาใหม่ พวกเขามีทีมมือถือเดียว แต่ในความเป็นจริงพวกเขาเป็นการผสมของผู้เชี่ยวชาญ Android และ iOS และพบว่าตัวเองส่วนใหญ่ทำงานกับแอปพลิเคชันใดแอปพลิเคชันหนึ่งเท่านั้น ซึ่งบ่งบอกว่าจริงๆ แล้วพวกเขาเป็นสองทีมในทางปฏิบัติ

บ่อยครั้งที่แรงผลักดันไปสู่การมี BFF จำนวนน้อยลงคือความต้องการนำ functionality ฝั่งเซิร์ฟเวอร์กลับมาใช้ใหม่เพื่อหลีกเลี่ยงความซ้ำซ้อนมากเกินไป แต่ก็มีวิธีอื่นในการจัดการเรื่องนี้ ซึ่งเราจะคุยกันต่อไป

Reuse and BFFs (การใช้ซ้ำและ BFF)

หนึ่งในข้อกังวลของการมี BFF เดียวต่อส่วนติดต่อผู้ใช้หนึ่งตัวคือ คุณอาจลงเอยด้วยความซ้ำซ้อนจำนวนมากระหว่าง BFF ต่างๆ ตัวอย่างเช่น พวกมันอาจทำ aggregation ประเภทเดียวกัน มีโค้ดที่เหมือนหรือคล้ายกันสำหรับติดต่อกับ downstream service และอื่นๆ ถ้าคุณกำลังมองหาการดึง functionality ที่ใช้ร่วมกันออกมา หนึ่งในความท้าทายมักเป็นการหามันให้เจอ ความซ้ำซ้อนนั้นอาจเกิดขึ้นใน BFF เอง แต่ก็อาจถูกฝังอยู่ในไคลเอนต์ที่ต่างกันด้วย เนื่องจากไคลเอนต์เหล่านี้ใช้ technology stack ที่แตกต่างกันมาก การระบุว่าความซ้ำซ้อนนี้กำลังเกิดขึ้นอาจทำได้ยาก ด้วยองค์กรที่มักมี technology stack ร่วมกันสำหรับ component ฝั่งเซิร์ฟเวอร์ การมี BFF หลายตัวที่มีความซ้ำซ้อนอาจสังเกตเห็นและแยกออกได้ง่ายกว่า

บางคนตอบสนองต่อเรื่องนี้ด้วยการอยากรวม BFF กลับเข้าด้วยกัน และลงเอยด้วย general-purpose aggregating gateway ความกังวลของผมเกี่ยวกับการถอยกลับไปสู่ aggregating gateway เดียวคือเราอาจสูญเสียมากกว่าที่ได้ โดยเฉพาะเมื่อมีวิธีอื่นในการจัดการความซ้ำซ้อนนี้

อย่างที่ผมเคยพูดไปก่อนหน้านี้ ผมค่อนข้างผ่อนคลายกับโค้ดที่ซ้ำซ้อนข้ามไมโครเซอร์วิส ซึ่งหมายความว่า ในขณะที่ภายในขอบเขตของไมโครเซอร์วิสหนึ่งตัว ผมมักจะพยายามทำทุกอย่างเพื่อ refactor ความซ้ำซ้อนออกไปเป็น abstraction ที่เหมาะสม แต่ผมไม่ได้มีปฏิกิริยาแบบเดียวกันเมื่อเจอความซ้ำซ้อนข้ามไมโครเซอร์วิส ส่วนใหญ่เป็นเพราะผมมักกังวลมากกว่าเรื่องที่การดึง shared code ออกมาอาจนำไปสู่ coupling ที่แน่นระหว่างเซอร์วิส (หัวข้อที่เราสำรวจไปแล้วใน “DRY and the Perils of Code Reuse in a Microservice World” ) แต่ก็มีบางกรณีที่แน่นอนว่าสมควรทำเช่นนั้น

เมื่อถึงเวลาที่ต้องดึงโค้ดร่วมออกมาเพื่อให้ใช้ซ้ำระหว่าง BFF มีสองตัวเลือกที่ชัดเจน ตัวเลือกแรกซึ่งมักถูกกว่าแต่ก็เสี่ยงกว่า คือการดึง shared library บางรูปแบบออกมา เหตุผลที่วิธีนี้อาจมีปัญหาคือ shared library เป็นแหล่งสำคัญของ coupling โดยเฉพาะเมื่อถูกใช้เพื่อสร้าง client library สำหรับเรียก downstream service ถึงอย่างนั้น ก็มีสถานการณ์ที่วิธีนี้รู้สึกถูกต้อง โดยเฉพาะเมื่อโค้ดที่ถูก abstract เป็นเพียงประเด็นภายในเซอร์วิสเท่านั้น

อีกตัวเลือกหนึ่งคือการดึง shared functionality ออกมาเป็นไมโครเซอร์วิสใหม่ วิธีนี้อาจทำงานได้ดีถ้า functionality ที่ถูกดึงออกมาเป็นตัวแทนของ business domain functionality จริงๆ อีกทางเลือกหนึ่งของแนวทางนี้อาจเป็นการผลักความรับผิดชอบด้าน aggregation ไปยังไมโครเซอร์วิสที่อยู่ปลายน้ำมากขึ้น ลองพิจารณาสถานการณ์ที่เราต้องการแสดงรายการสินค้าใน wishlist ของลูกค้า พร้อมข้อมูลว่าสินค้าเหล่านั้นมีในสต็อกหรือไม่และราคาปัจจุบัน ดังที่แสดงใน Table 14-1

Table 14-1. การแสดง wishlist สำหรับลูกค้าของ MusicCorp

The Brakes, Give Blood

มีในสต็อก!

$5.99

Blue Juice, Retrospectable

สินค้าหมด

$7.50

Hot Chip, Why Make Sense?

ใกล้หมดแล้ว! (เหลือ 2 ชิ้น)

$9.99

microservice Customer เก็บข้อมูลเกี่ยวกับ wishlist และ ID ของสินค้าแต่ละชิ้น microservice Catalog เก็บชื่อและราคาของสินค้าแต่ละชิ้น และระดับสต็อกถูกเก็บอยู่ใน microservice Inventory ของเรา ในการแสดง control เดียวกันนี้บนทั้งแอปพลิเคชัน iOS และ Android แต่ละ BFF จะต้องเรียกไปยังไมโครเซอร์วิสที่รองรับสามตัวนี้เหมือนกัน ดังที่แสดงใน Figure 14-14

bms2 1414

Figure 14-14. BFF ทั้งสองตัวกำลังทำ operation เดียวกันเพื่อแสดง wishlist

วิธีลดความซ้ำซ้อนของ functionality ตรงนี้คือการดึงพฤติกรรมร่วมนี้ออกมาเป็นไมโครเซอร์วิสใหม่ ใน Figure 14-15 เราจะเห็น microservice Wishlist เฉพาะทางใหม่ของเราที่แอปพลิเคชัน Android และ iOS ทั้งคู่สามารถใช้ร่วมกันได้

ผมต้องบอกว่าการที่โค้ดเดียวกันถูกใช้อยู่สองที่ ไม่ได้ทำให้ผมอยากดึงเซอร์วิสออกมาแบบนี้โดยอัตโนมัติ แต่ผมก็คงจะพิจารณาแน่นอนถ้า transaction cost ของการสร้างเซอร์วิสใหม่ต่ำพอ หรือถ้าผมใช้โค้ดนี้ในมากกว่าสองที่ ในสถานการณ์เฉพาะนี้ ถ้าเรายังแสดง wishlist บน web interface ของเราด้วย ตัวอย่างเช่น microservice เฉพาะทางก็จะเริ่มดูน่าสนใจยิ่งขึ้น ผมคิดว่าคำกล่าวเก่าแก่ที่ว่าให้สร้าง abstraction เมื่อคุณกำลังจะ implement บางสิ่งเป็นครั้งที่สามยังคงเป็นกฎง่ายๆ ที่ดี แม้แต่ในระดับเซอร์วิสก็ตาม

bms2 1415

Figure 14-15. functionality ร่วมถูกดึงออกมาเป็น microservice Wishlist ทำให้สามารถใช้ซ้ำข้าม BFF ได้

BFFs for Desktop Web and Beyond (BFF สำหรับเว็บบนเดสก์ท็อปและอื่นๆ)

คุณอาจมอง BFF ว่ามีประโยชน์แค่ในการแก้ข้อจำกัดของอุปกรณ์มือถือเท่านั้น ประสบการณ์เว็บบนเดสก์ท็อปมักถูกส่งมอบบนอุปกรณ์ที่ทรงพลังกว่าและมีการเชื่อมต่อที่ดีกว่า ซึ่งต้นทุนของการเรียกไปยัง downstream หลายครั้งนั้นจัดการได้ นี่ทำให้ web application ของคุณสามารถเรียกตรงไปยัง downstream service ได้หลายครั้งโดยไม่จำเป็นต้องมี BFF

ผมเคยเห็นสถานการณ์ที่การใช้ BFF สำหรับเว็บก็มีประโยชน์เช่นกัน เมื่อคุณกำลังสร้าง web UI ส่วนใหญ่ที่ฝั่งเซิร์ฟเวอร์ (เช่น การใช้ server-side templating) BFF คือที่ที่ชัดเจนที่จะทำสิ่งนี้ แนวทางนี้ยังช่วยลดความซับซ้อนของการทำ caching ได้บ้าง เพราะคุณสามารถวาง reverse proxy ไว้หน้า BFF ทำให้คุณ cache ผลลัพธ์ของการเรียกแบบ aggregated ได้

ผมเคยเห็นอย่างน้อยหนึ่งองค์กรใช้ BFF สำหรับ external party อื่นๆ ที่ต้องเรียกด้วย กลับมาที่ตัวอย่างเดิมของผมอย่าง MusicCorp ผมอาจเปิด BFF ให้ third party ดึงข้อมูลค่าลิขสิทธิ์ หรือให้ streaming ไปยังอุปกรณ์ set-top box ได้หลากหลาย ดังที่เราเห็นใน Figure 14-16 สิ่งเหล่านี้ไม่ใช่ BFF จริงๆ อีกต่อไปแล้ว เพราะ external party ไม่ได้นำเสนอ "ส่วนติดต่อผู้ใช้" แต่นี่เป็นตัวอย่างของแพตเทิร์นเดียวกันที่ถูกใช้ในบริบทที่ต่างออกไป ซึ่งผมคิดว่าน่าแบ่งปันไว้

bms2 1416

Figure 14-16. การใช้ BFF เพื่อจัดการ API ภายนอก

แนวทางนี้มีประสิทธิภาพเป็นพิเศษ เพราะ third party มักมีความสามารถ (หรือความต้องการ) จำกัดมากในการใช้หรือเปลี่ยนแปลงการเรียก API ที่พวกเขาทำ ด้วย central API backend คุณอาจต้องเก็บ API เวอร์ชันเก่าไว้เพียงเพื่อรองรับกลุ่มเล็กๆ ของ external party ที่ไม่สามารถเปลี่ยนแปลงได้ แต่กับ BFF ปัญหานี้ลดลงอย่างมาก มันยังจำกัดผลกระทบของ breaking change ด้วย คุณสามารถเปลี่ยน API สำหรับ Facebook ในแบบที่จะทำให้เข้ากันไม่ได้กับ party อื่นๆ แต่เนื่องจากพวกเขาใช้ BFF ที่ต่างกัน พวกเขาจึงไม่ได้รับผลกระทบจากการเปลี่ยนแปลงนี้

When to Use (เมื่อไรควรใช้)

สำหรับแอปพลิเคชันที่ให้บริการแค่ web UI เท่านั้น ผมคิดว่า BFF จะสมเหตุสมผลก็ต่อเมื่อคุณมี aggregation จำนวนมากที่จำเป็นฝั่งเซิร์ฟเวอร์เท่านั้น ไม่เช่นนั้น ผมคิดว่าเทคนิคการประกอบ UI อื่นๆ ที่เราคุยกันไปแล้วก็สามารถทำงานได้ดีเช่นกันโดยไม่ต้องมี server-side component เพิ่มเติม

ทันทีที่คุณต้องมอบ functionality เฉพาะสำหรับ mobile UI หรือ third party ผมขอแนะนำอย่างยิ่งให้ใช้ BFF สำหรับไคลเอนต์แต่ละตัวตั้งแต่แรกเริ่ม ผมอาจพิจารณาใหม่ถ้าต้นทุนของการ deploy เซอร์วิสเพิ่มเติมสูงมาก แต่การแยกความรับผิดชอบที่ BFF มอบให้นั้นทำให้มันเป็นข้อเสนอที่น่าสนใจในกรณีส่วนใหญ่ ผมจะยิ่งมีแนวโน้มที่จะใช้ BFF มากขึ้นถ้ามีการแยกกันอย่างชัดเจนระหว่างคนที่สร้าง UI กับ downstream service ด้วยเหตุผลที่ผมได้อธิบายไป

แล้วเราก็มาถึงคำถามว่าจะ implement BFF อย่างไร มาดู GraphQL และบทบาทที่มันสามารถมีกัน