คนส่วนใหญ่ที่เปิดร้าน dropshipping มักจะมองหาทางลัด พวกเขาเลื่อนดูฟอรัมเพื่อหา "สินค้าทำเงิน" จ้างผู้ช่วยเสมือน (virtual assistants) ราคาถูก และหวังว่าอัลกอริทึมจะประทานความร่ำรวยมาให้เพียงชั่วข้ามคืน สิ่งเหล่านั้นไม่เคยดึงดูดผมเลย ผมมองว่า dropshipping คือปัญหาทางวิศวกรรม ผมไม่ได้วิ่งไล่ตามเงินด่วน แต่ผมต้องการแก้ปัญหาเรื่องการซิงค์สต็อกสินค้า สร้างอัลกอริทึมการตั้งราคาที่ตอบสนองต่อการเปลี่ยนแปลงของตลาดจริง และจัดการกับ API ของซัพพลายเออร์โดยไม่เสียสติไปเสียก่อน ร้านค้าแห่งนี้กลายเป็นเพียงผลพลอยได้จากระบบที่ผมสร้างขึ้นด้วย Node.js และ PostgreSQL
มองร้านค้าให้เหมือนกับบริการ Backend
วินาทีที่คุณเลิกมองว่า dropshipping คือการลุยการตลาด และเริ่มมองว่ามันคือความท้าทายด้านระบบแบบกระจายตัว (distributed systems) ปัญหาต่างๆ จะเริ่มน่าสนใจขึ้นทันที คุณจะรักษาความถูกต้องของหน้าร้านได้อย่างไรเมื่อซัพพลายเออร์สามรายต่างควบคุมสต็อกของคุณ? คุณจะตั้งราคาให้แข่งขันได้ได้อย่างไรเมื่อซัพพลายเออร์เหล่านั้นเปลี่ยนต้นทุนโดยไม่บอกคุณ? และคุณจะจัดการกับแคตตาล็อกที่เติบโตจาก 50 SKU ไปเป็น 5,000 SKU โดยไม่จมกองสเปรดชีตได้อย่างไร?
ผมสร้าง Pipeline ขึ้นมาเพื่อตอบคำถามเหล่านั้น Node.js ทำหน้าที่จัดการสถาปัตยกรรมแบบขับเคลื่อนด้วยเหตุการณ์ (event-driven architecture) เพราะผมต้องการ non-blocking I/O เพื่อจัดการกับการเชื่อมต่อของซัพพลายเออร์หลายรายพร้อมกัน ส่วน PostgreSQL ทำหน้าที่เป็นแหล่งข้อมูลที่ถูกต้องเพียงหนึ่งเดียว (source of truth) ผมให้ความสำคัญอย่างยิ่งกับการออกแบบ Schema เพราะตารางสต็อกสินค้าที่สะเพร่าจะกลายเป็นฝันร้ายทันทีที่คุณขายสินค้าเกินจำนวนที่มีอยู่จริง
การสร้าง Pipeline
งานหลักนั้นระบุได้ง่ายๆ คือ: ดึงข้อมูลสินค้าจาก API ของซัพพลายเออร์ แต่ในทางปฏิบัติ นั่นหมายถึงการนำเข้า SKU, รายละเอียดสินค้า, รูปภาพ, ระดับสต็อก และราคา จาก endpoint ที่ไม่เคยถูกออกแบบมาให้คุยกันได้เลย ผมเขียนบริการแบบ polling ใน Node.js เพื่อเรียกข้อมูลจาก feed ของซัพพลายเออร์เป็นช่วงเวลาที่เหลื่อมกัน ทุก payload ที่เข้ามาจะผ่านเลเยอร์การตรวจสอบความถูกต้อง (validation) และการทำ mapping ก่อนที่จะถูกบันทึกลงในฐานข้อมูลหน้าร้านภายในของเรา
ผมวางโครงสร้าง PostgreSQL โดยแยกตารางสำหรับสินค้า, สินค้าที่มีความหลากหลาย (variants), ประวัติการตั้งราคา และบันทึกการซิงค์ (sync logs) เมื่อซัพพลายเออร์เปลี่ยนชื่อฟิลด์เงียบๆ หรือส่งค่า null มาในจุดที่เคยเป็นตัวเลข Pipeline จะตรวจพบและเขียนบันทึกความล้มเหลวแทนที่จะปล่อยให้ข้อมูลในหน้าร้านเสียหาย ผมสามารถดูแถวใน log และรู้ได้ทันทีว่า endpoint ไหนที่พัง เกิดขึ้นเมื่อไหร่ และฟิลด์ไหนที่รูปแบบผิดเพี้ยน การสังเกตการณ์ (observability) นี้ช่วยผมไว้ได้หลายครั้ง เมื่อซัพพลายเออร์ตัดสินใจ "อัปเกรด" API ของพวกเขาในช่วงวันหยุดสุดสัปดาห์
สิ่งที่ทำได้ดี
ระบบอัตโนมัติช่วยประหยัดเวลาไปได้มหาศาล ในช่วงแรกผมลองใช้วิธีจัดการด้วยมือ ทั้งดาวน์โหลดสเปรดชีตของซัพพลายเออร์, ทำความสะอาดข้อมูลด้วยตัวเอง, จัดรูปแบบรูปภาพ และอัปโหลดไฟล์ CSV ขึ้นร้านค้า แต่วิธีนั้นกลายเป็นเรื่องที่เป็นไปไม่ได้ทันทีที่แคตตาล็อกมีสินค้าเกินไม่กี่สิบรายการ Pipeline อัตโนมัติสามารถจัดการทั้งรายการสินค้าใหม่, การอัปเดตราคา และการปรับสต็อก โดยที่ผมไม่ต้องแตะต้องสเปรดชีตอีกเลย
การขยายขนาดคำอธิบายสินค้าทำได้ผ่านเทมเพลต การเขียนคำบรรยายที่ไม่ซ้ำกันสำหรับสินค้าห้าร้อยรายการที่แทบจะเหมือนกันทุกประการนั้นไม่ใช่เรื่องที่ยั่งยืน แทนที่จะทำแบบนั้น ผมสร้างเลเยอร์เทมเพลตที่ดึงคุณลักษณะของซัพพลายเออร์ เช่น วัสดุ, ขนาด หรือสี แล้วนำมาใส่ในบล็อกคำอธิบายที่มีโครงสร้าง ผลลัพธ์ที่ได้นั้นสะอาดพอที่จะนำไปใช้งานต่อ และมีความสม่ำเสมอมากพอจนการเพิ่ม SKU ใหม่หนึ่งพันรายการไม่จำเป็นต้องใช้การเขียนคำโฆษณาด้วยมืออีกต่อไป
การตรวจสอบราคาก็ทำได้ดีเกินคาด ผมสร้างเลเยอร์การตรวจสอบน้ำหนักเบาที่คอยติดตามราคาคู่แข่งในกลุ่มสินค้าหลักบางส่วน เมื่อระบบตรวจพบการเปลี่ยนแปลง มันจะปรับอัตรากำไรของเราโดยอัตโนมัติภายใต้ขอบเขต (guardrails) ที่ผมกำหนดไว้ หากซัพพลายเออร์ลดราคาขายส่ง ราคาขายหน้าร้านก็สามารถสะท้อนการเปลี่ยนแปลงนั้นได้ภายในไม่กี่นาทีแทนที่จะเป็นหลายวัน ความรวดเร็วในการตอบสนองนี้สร้างความแตกต่างอย่างเห็นได้ชัดสำหรับสินค้าที่มีอัตรากำไรต่ำ
สิ่งที่พังและสาเหตุ
API ของซัพพลายเออร์ขาดความสม่ำเสมอ นี่ไม่ใช่การบ่น แต่มันคือความจริงที่เปลี่ยนแปลงไม่ได้ พาร์ทเนอร์รายหนึ่งส่ง JSON ที่สะอาดพร้อมการแบ่งหน้า (pagination) ที่คาดเดาได้ แต่อีกรายกลับส่ง XML ที่มีแท็กแบบ camelCase ในวันจันทร์ และเป็น snake_case ในวันพุธ ขีดจำกัดการเรียกใช้งาน (rate limits) มีตั้งแต่ใจดีไปจนถึงขั้นลงโทษ การหยุดทำงาน (downtime) ถูกแจ้งผ่านหน้า error ของ HTML แทนที่จะเป็น status code ที่ถูกต้อง คุณจึงต้องเขียนตัวแยกข้อมูลเชิงป้องกัน (defensive parsers) และตรรกะการลองใหม่ (retry logic) สำหรับ endpoint ที่ทำตัวเหมือนถูกออกแบบมาตั้งแต่ปี 2003
การซิงค์สต็อกสินค้า (Inventory sync) มีปัญหา race conditions ที่ทำให้ผมแทบไม่ได้นอน ลองนึกภาพดูนะครับ: ลูกค้าสองคนสั่งซื้อสินค้าชิ้นสุดท้ายพร้อมกันภายในเวลาไม่กี่วินาที หรือ webhook จากซัพพลายเออร์แจ้งว่าสต็อกเป็นศูนย์ในจังหวะเดียวกับที่ผู้ซื้อกำลังกดชำระเงินพอดี ตรรกะแบบ read-then-update ที่ผมใช้ในตอนแรกล้มเหลวอย่างไม่เป็นท่า ผมต้องเขียนเลเยอร์การซิงค์ใหม่โดยใช้ atomic PostgreSQL transactions และการทำ pessimistic locking สำหรับ SKU ที่มีการหมุนเวียนสูง (high-velocity SKUs) มันเป็นบทเรียนภาคปฏิบัติที่เจ็บปวดเรื่อง concurrency ซึ่งไม่มีบทเรียนไหนจะเตรียมคุณให้พร้อมได้ดีเท่ากับการที่มีเงินจริง ๆ เป็นเดิมพัน
ความล้มเหลวครั้งใหญ่ที่สุดของผมคือการละเลยระบบอัตโนมัติสำหรับฝ่ายสนับสนุนลูกค้า (customer support automation) ผมหมกมุ่นอยู่กับ data pipelines และมองว่าผลกระทบที่ตามมาต่อผู้คนเป็นเพียงเรื่องรอง คำสั่งซื้อมาถึงล่าช้า ซัพพลายเออร์ส่งสินค้าผิดสี ลูกค้าส่งอีเมลมาทิ้งไว้ในอินบ็อกซ์ของผมหลายชั่วโมงในขณะที่ผมกำลังไล่แก้ปัญหา API timeouts ผมไม่มีระบบจัดเส้นทางตั๋ว (ticket routing) ไม่มีระบบตอบกลับอัตโนมัติ และไม่มีการส่งต่อให้แชทบอท (chatbot handoffs) โครงสร้างพื้นฐานทางเทคนิคนั้นแข็งแกร่ง แต่โครงสร้างพื้นฐานด้านการจัดการคนกลับขาดหายไป และช่องว่างนั้นส่งผลเสียต่อธุรกิจมากกว่า webhook ที่ไม่เสถียรเสียอีก
การทดสอบรูปภาพแบบวิศวกร
ผมได้ทำการทดลองย่อย ๆ กับรูปภาพสินค้า ผมแสดงรูปภาพหลัก (hero images) ที่แตกต่างกันให้กับผู้ใช้แต่ละกลุ่ม โดยใช้การกำหนดเส้นทางผ่าน URL parameter แบบง่าย ๆ ที่เชื่อมโยงกับการแบ่งกลุ่มตามเซสชัน (session-based bucketing) รูปแบบหนึ่งแสดงสินค้าบนพื้นหลังสีขาวเรียบ ๆ ส่วนอีกรูปแบบแสดงสินค้าในสภาพแวดล้อมการใช้งานจริง (lifestyle setting) บนโต๊ะทำงาน ผมติดตามอัตราการเปลี่ยนเป็นยอดขาย (conversion rates) ของแต่ละกลุ่มโดยใช้การบันทึกเหตุการณ์ (event logging) พื้นฐานที่เชื่อมโยงโดยตรงกับขั้นตอนการสั่งซื้อ
การเปลี่ยนแปลงเล็ก ๆ น้อย ๆ ช่วยเพิ่มการมีส่วนร่วม (engagement) ได้ รูปภาพแนว lifestyle ไม่ได้ชนะเสมอไป แต่เมื่อมันชนะ ผลลัพธ์ที่เพิ่มขึ้น (lift) นั้นมีนัยสำคัญพอที่จะเปลี่ยนวิธีการจัดลำดับความสำคัญของผม
