ออกแบบระบบแจ้งเตือนบนเว็บแบบมืออาชีพ: In-app, Email และ Push ทีละขั้น
บทความนี้สรุปแนวทางออกแบบระบบแจ้งเตือนบนเว็บให้ส่งถึงผู้ใช้โดยไม่รบกวน พร้อมตรวจสอบย้อนหลัง ป้องกันการส่งซ้ำ และรองรับการขยายระบบในระยะยาว ครอบคลุมตั้งแต่โครงสร้างข้อมูล การจัดคิว การตั้งค่า retry ไปจนถึงประสบการณ์ใช้งาน

ออกแบบระบบแจ้งเตือนบนเว็บแบบมืออาชีพ: In-app, Email และ Push ทีละขั้น
ระบบแจ้งเตือนที่ดีไม่ได้มีหน้าที่แค่ “ส่งข้อความออกไป” เท่านั้น แต่ต้องส่งให้ถึงผู้ใช้ในเวลาที่เหมาะสม ไม่สร้างความรำคาญ ตรวจสอบย้อนหลังได้ และพร้อมเติบโตไปกับระบบในอนาคต แนวคิดสำคัญคือการแยกให้ชัดระหว่าง เหตุการณ์ต้นทาง (Event) กับ กระบวนการส่ง (Delivery) เพื่อให้ระบบมีความยืดหยุ่นและควบคุมได้ง่ายขึ้น
บทความนี้จะเรียบเรียงแนวทางออกแบบระบบแจ้งเตือนบนเว็บแบบเป็นขั้นตอน ตั้งแต่การวางโครงสร้างข้อมูลไปจนถึงการดูแลเสถียรภาพของระบบในงานจริง
1) วางแนวคิดพื้นฐานให้ถูกตั้งแต่ต้น
ก่อนเริ่มลงมือสร้างระบบ ควรกำหนดเป้าหมายของ Notification ให้ชัดเจนว่า ต้องมีคุณสมบัติหลักดังนี้
- ส่งถึงผู้ใช้ได้จริง
- ไม่รบกวนเกินความจำเป็น
- ตรวจสอบย้อนหลังได้
- ขยายระบบได้เมื่อปริมาณงานเพิ่มขึ้น
หลักการสำคัญคือแยก 2 ส่วนออกจากกัน
- Event: เหตุการณ์ที่เกิดขึ้นในระบบ เช่น ชำระเงินสำเร็จ มีคนตอบคอมเมนต์ หรือมีการล็อกอินผิดปกติ
- Delivery: วิธีการนำเหตุการณ์นั้นไปส่งยังผู้ใช้ผ่านช่องทางต่าง ๆ เช่น In-app, Email หรือ Push
เมื่อแยกสองส่วนนี้ชัดเจน จะช่วยลดปัญหาการส่งซ้ำ จัดการ retry ได้ง่าย และตรวจสอบย้อนหลังได้ละเอียดกว่าเดิม
2) กำหนดประเภทการแจ้งเตือนให้เป็นมาตรฐาน
ควรกำหนด notification_type ที่ชัดเจนตั้งแต่ต้น เช่น
order_paidcomment_replysecurity_loginweekly_digest
แต่ละประเภทควรมีข้อมูลกำกับ เช่น
- ระดับความสำคัญ (
priority) - ช่องทางเริ่มต้น (
default channels)
ตัวอย่างเช่น
security_loginอาจส่งทั้ง In-app และ Email เป็นค่าเริ่มต้นweekly_digestอาจส่งเฉพาะ Emailcomment_replyอาจส่ง In-app และ Push
การออกแบบส่วนนี้ให้ดี จะช่วยให้เพิ่ม notification ประเภทใหม่ได้ง่ายและควบคุมพฤติกรรมของแต่ละประเภทได้เป็นระบบ
3) ออกแบบตารางหลักสำหรับ In-app Notification
สำหรับการแจ้งเตือนภายในแอปหรือหน้าเว็บ ควรมีตาราง notifications เป็นแกนกลาง เช่น
id(uuid)user_idtypetitlebodydata_jsonสำหรับ payload เช่นorder_id,urlurl(optional)statusเช่นunread,read,archivedcreated_atread_at
ดัชนีที่แนะนำ
(user_id, created_at)(user_id, status)
ตารางนี้ช่วยให้ดึงรายการแจ้งเตือนของผู้ใช้ได้เร็ว รองรับการแสดงผลแบบยังไม่อ่าน อ่านแล้ว หรือเก็บถาวรได้สะดวก
4) เก็บเหตุการณ์ต้นทางเพื่อกันยิงซ้ำและตรวจสอบย้อนหลัง
ควรมีตาราง notification_events เพื่อเก็บต้นตอของการแจ้งเตือน เช่น
id(uuid)event_keyเช่นorder:123:paidtypeactor_idถ้ามีผู้กระทำtarget_user_idpayload_jsoncreated_at
สิ่งสำคัญมากคือการตั้ง unique index ที่ event_key เพื่อป้องกันเหตุการณ์เดียวกันถูกสร้างซ้ำจากการยิง request ซ้ำหรือระบบทำงานซ้ำโดยไม่ตั้งใจ
ข้อดีของการมีตารางนี้คือ
- กันการสร้าง notification ซ้ำ
- ตรวจสอบย้อนหลังได้ว่าเหตุการณ์ใดถูกสร้างเมื่อไร
- ใช้เป็นจุดอ้างอิงหลักสำหรับการส่งผ่านหลายช่องทาง
5) แยกตาราง Delivery สำหรับแต่ละช่องทาง
เมื่อมี Event แล้ว ควรสร้างตาราง notification_deliveries สำหรับติดตามการส่งแต่ละช่องทางโดยเฉพาะ เช่น
id(uuid)event_iduser_idchannelเช่นinapp,email,pushproviderเช่นses,sendgrid,fcm,webpushstatusเช่นpending,sending,sent,failed,canceledattemptsnext_retry_atlast_erroridempotency_key(unique)created_atsent_at
แนะนำให้ตั้ง unique index ที่ (channel, idempotency_key) เพื่อป้องกันการส่งซ้ำในช่องทางเดียวกัน
6) ใช้ idempotency_key เพื่อกันส่งซ้ำ
แนวทางที่เข้าใจง่ายและใช้งานได้ดี คือกำหนด
idempotency_key = event_key + ":" + channel
เช่น
order:123:paid:emailorder:123:paid:push
ผลคือแม้ worker จะทำงานซ้ำ หรือมีการ retry เพราะปัญหาชั่วคราว ระบบก็จะไม่ส่งข้อความเดิมซ้ำในช่องทางเดิมโดยไม่จำเป็น
7) เปิดให้ผู้ใช้ตั้งค่า Preference ได้ละเอียด
ระบบแจ้งเตือนที่ดีควรให้ผู้ใช้เลือกรับหรือปิดการแจ้งเตือนแยกตามประเภทและช่องทางได้ โดยอาจใช้ตาราง notification_preferences เช่น
user_idtypechannelenabled(true/false)quiet_hours_json(optional)created_atupdated_at
ควรตั้ง unique constraint ที่ (user_id, type, channel) และมี fallback ไปใช้ค่าเริ่มต้นจาก notification_type หากยังไม่มี record ของผู้ใช้
จุดนี้ช่วยลดความรำคาญและทำให้ผู้ใช้รู้สึกว่าควบคุมระบบได้
8) รองรับ Push และ WebPush ด้วยตาราง Device/Endpoint
สำหรับช่องทาง Push Notification ควรมีตาราง notification_endpoints เช่น
iduser_idplatformเช่นios,android,webtoken_or_subscription_jsonstatusเช่นactive,revokedlast_seen_atcreated_at
พร้อมกำหนด unique เช่น
unique(user_id, token)หรือunique(token)
ตารางนี้ช่วยให้จัดการอุปกรณ์ของผู้ใช้ได้ดีขึ้น เช่น ปิด token ที่ใช้ไม่ได้ หรือล้าง endpoint ที่หมดอายุ
9) วาง Flow การทำงานให้เรียบง่ายและชัดเจน
เส้นทางการทำงานที่แนะนำมีดังนี้
- ระบบธุรกิจสร้าง event เช่น
OrderPaid - บันทึกลง
notification_eventsพร้อมevent_key - สร้าง
notificationsสำหรับ In-app ทันทีหรือผ่าน job - สร้าง
notification_deliveriesตาม preference ของผู้ใช้ - ส่ง delivery เข้า queue เพื่อให้ worker แต่ละช่องทางนำไปส่ง
Flow นี้ช่วยให้ส่วนธุรกิจไม่ต้องผูกติดกับรายละเอียดการส่ง และทำให้ระบบตอบสนอง request หลักได้เร็วขึ้น
10) เลือก Queue และ Worker ให้เหมาะกับขนาดงาน
หากเริ่มต้นจากระบบขนาดเล็กถึงกลาง อาจใช้
- Redis Queue
- BullMQ
- Sidekiq
- RQ
หากเป็นระบบขนาดใหญ่ที่ต้องรองรับงานปริมาณมาก อาจพิจารณา
- Kafka
- RabbitMQ
หลักการสำคัญคือ event ควรถูกสร้างให้เร็ว ส่วนการส่งสามารถทำแบบ asynchronous ได้ เพื่อไม่ให้ request หลักของผู้ใช้ช้าเพราะต้องรอ Email หรือ Push ส่งสำเร็จก่อน
11) แยก Worker ตามช่องทางเพื่อลดผลกระทบข้ามระบบ
ควรแยก worker ตามประเภทการส่ง เช่น
worker-emailสำหรับ Email โดยเฉพาะworker-pushสำหรับ Push โดยเฉพาะworker-inappสำหรับ In-app ซึ่งมักเป็นการบันทึกข้อมูลลงฐานข้อมูล
ข้อดีคือ
- ควบคุม rate limit ของแต่ละช่องทางได้
- แก้ปัญหาเฉพาะทางได้ง่าย เช่น bounce ของ email หรือ invalid token ของ push
- หาก Email provider มีปัญหา ก็ไม่ทำให้ Push ล่มตามไปด้วย
12) ตั้งค่า Retry แบบมืออาชีพ
ระบบแจ้งเตือนควรมี retry policy ที่ชัดเจน โดยเฉพาะสำหรับความผิดพลาดชั่วคราว เช่น provider timeout หรือเครือข่ายขัดข้อง
ตัวอย่างการใช้ exponential backoff
- ครั้งที่ 1 รอ 1 นาที
- ครั้งที่ 2 รอ 5 นาที
- ครั้งที่ 3 รอ 15 นาที
- ครั้งที่ 4 รอ 60 นาที
- ครั้งที่ 5 รอ 6 ชั่วโมง
ควรกำหนด attempts_max เช่น 5 ครั้ง และเมื่อเกินเพดานให้เปลี่ยนสถานะเป็น failed พร้อมบันทึก last_error เพื่อใช้วิเคราะห์ปัญหาในภายหลัง
13) ป้องกันการส่งซ้ำเมื่อ Worker ล่มกลางทาง
ปัญหาคลาสสิกของระบบ asynchronous คือ worker อาจ crash ระหว่างการส่ง ทำให้เกิดความไม่แน่ใจว่าส่งสำเร็จหรือยัง
แนวทางที่ควรใช้คือ
- อัปเดต
status = sendingแบบ atomic ก่อนเรียก provider - เมื่อส่งสำเร็จแล้วจึงเปลี่ยนเป็น
sentและบันทึกsent_at - หากรายการค้างอยู่ในสถานะ
sendingเกินเวลาที่กำหนด ให้มีระบบ recovery เพื่อนำกลับเข้าคิวใหม่
วิธีนี้ช่วยลดโอกาสส่งซ้ำและทำให้ระบบกู้คืนงานค้างได้อย่างปลอดภัย
14) สร้าง Delivery ตาม Preference และ Quiet Hours
ตรรกะการสร้าง Delivery ควรทำงานตามลำดับดังนี้
- อ่าน default channels จาก
notification_type - ตรวจสอบ
notification_preferencesของผู้ใช้ - ตัดช่องทางที่ผู้ใช้ปิดออก
- หากอยู่ในช่วง
quiet hoursให้เลื่อนเวลาไปส่งภายหลังผ่านnext_retry_at
แนวคิดนี้ช่วยให้ระบบฉลาดขึ้น ไม่รบกวนผู้ใช้ในเวลาที่ไม่เหมาะสม เช่น เวลากลางคืน หรือเวลาที่ผู้ใช้เลือกปิดการแจ้งเตือนชั่วคราว
15) ใช้ Dedup Window เพื่อลดการสแปมจากเหตุการณ์ถี่เกินไป
บางเหตุการณ์อาจเกิดขึ้นต่อเนื่องในเวลาสั้น ๆ เช่น การตอบคอมเมนต์หลายครั้งในเธรดเดียว หากส่งทันทีทุกครั้งอาจสร้างประสบการณ์ที่ไม่ดี
วิธีแก้ทำได้หลายแบบ เช่น
- ใส่ช่วงเวลาใน
event_keyเช่นthread:55:reply:2026-01-19T10:00 - ใช้
aggregate_keyแล้วรวมหลายเหตุการณ์เป็น digest
แนวคิดนี้เหมาะกับ notification ที่ไม่จำเป็นต้อง real-time เป๊ะทุกเหตุการณ์ แต่ควรลดความถี่เพื่อความสบายของผู้ใช้
16) เพิ่ม Observability เพื่อดูแลระบบในระยะยาว
ระบบแจ้งเตือนที่ใช้งานจริงควรมีเครื่องมือสำหรับติดตามสุขภาพของระบบ เช่น
- metrics: จำนวน
sent,failed,retryแยกตาม channel - log ที่ผูกกับ
event_idและdelivery_id - dashboard สำหรับดู backlog ของ queue และ retry rate
เมื่อมีข้อมูลเหล่านี้ ทีมพัฒนาจะสามารถวิเคราะห์ปัญหาได้เร็วขึ้น เช่น Email ส่งช้าผิดปกติ, Push ล้มเหลวมากขึ้น หรือ queue เริ่มค้างสะสม
17) คำนึงถึงความปลอดภัยและความเป็นส่วนตัว
ใน payload_json ไม่ควรเก็บข้อมูลอ่อนไหว เช่น
- เลขบัตรประชาชน
- token ลับ
- ข้อมูลทางการเงินที่ละเอียดเกินจำเป็น
สำหรับ Email ควรใส่เฉพาะข้อมูลเท่าที่จำเป็น และลิงก์ที่แนบไปต้องตรวจสอบสิทธิ์การเข้าถึงเสมอ เพื่อไม่ให้เกิดปัญหาข้อมูลรั่วไหลหากอีเมลถูกส่งต่อหรือถูกเข้าถึงโดยผู้ไม่เกี่ยวข้อง
18) ปรับ UX ให้ระบบดูเป็นมืออาชีพมากขึ้น
นอกจาก backend ที่ดีแล้ว ประสบการณ์ใช้งานก็สำคัญไม่แพ้กัน ตัวอย่างฟีเจอร์ที่ช่วยให้ดูเป็นระบบมากขึ้น ได้แก่
- จัดกลุ่ม In-app เป็น วันนี้ / เมื่อวาน / ก่อนหน้านี้
- มีปุ่ม
mark all as read - คลิกแล้วพาไปยังหน้าปลายทางได้ทันที
- สำหรับ Push ให้รองรับ deep link และ action button หากแพลตฟอร์มรองรับ
รายละเอียดเล็ก ๆ เหล่านี้ทำให้ระบบแจ้งเตือนใช้งานง่ายขึ้นและเพิ่มความรู้สึกเป็นมืออาชีพได้อย่างชัดเจน
19) ตัวอย่างเคสจริงแบบสั้น
สมมติว่าเกิดเหตุการณ์ OrderPaid
- กำหนด
event_key = order:123:paid - สร้าง In-app notification 1 รายการ
- สร้าง deliveries 2 รายการ คือ
emailและpushตาม preference ของผู้ใช้ - ให้ worker แยกกันส่ง Email และ Push
- หากล้มเหลว ให้ retry ตามนโยบายที่ตั้งไว้
- ใช้
event_keyและidempotency_keyช่วยกันป้องกันการสร้างซ้ำและส่งซ้ำ
ตัวอย่างนี้สะท้อนโครงสร้างที่ดีของระบบแจ้งเตือน คือแยกความรับผิดชอบชัดเจน ควบคุมแต่ละช่องทางได้ และพร้อมรับปัญหาที่เกิดขึ้นจริง
สรุป
การสร้างระบบแจ้งเตือนบนเว็บแบบมืออาชีพไม่ใช่แค่การส่งข้อความให้ผู้ใช้เห็น แต่คือการออกแบบทั้งกระบวนการให้เชื่อถือได้ ยืดหยุ่น และดูแลได้ในระยะยาว หลักสำคัญที่ควรยึดคือ
- ใช้
event_keyแบบ unique เพื่อกันการสร้าง event ซ้ำ - ใช้
idempotency_keyเพื่อกันการส่งซ้ำในแต่ละ channel - แยก queue และ worker ตามช่องทางเพื่อลดผลกระทบข้ามกัน
- ใช้ retry แบบ backoff และมีระบบ recovery สำหรับงานค้าง
- รองรับ preference และ quiet hours เพื่อลดความรำคาญของผู้ใช้
หากค่อย ๆ ทำตาม checklist เหล่านี้ทีละขั้น ระบบแจ้งเตือนของคุณจะไม่เพียงแค่ใช้งานได้ แต่ยังพร้อมขยาย รองรับงานจริง และรักษาคุณภาพประสบการณ์ของผู้ใช้ได้อย่างต่อเนื่อง