Systems Hub
Teacher নিজের course-এ
Extra Service যোগ করবে কীভাবে?
রহিম teacher — সে চায় তার course-এ "Live Session" আর "PDF Notes" add করতে।
করিম teacher — সে চায় শুধু "1-on-1 Call"।
এই flexible system-টা কীভাবে বানাব?
5-Table Design Race Condition Safe N+1 Solution Smart Merge formatTeacherServices()
AddOn System Design বুঝব আজকে
SLIDE 01

সমস্যাটা কী?

Simple করলে কেন কাজ করে না?
ভুল approach: Course table-এ services column array হিসেবে রাখো
// courses table-এ services: ['live-session', 'pdf-notes'] // সমস্যা: // 1. Price কোথায় রাখব? // 2. Service কতজন কিনেছে? // 3. Admin কি service অফার করতে পারবে? // 4. Teacher নিজে service বাদ দিতে পারবে? // 5. Race condition — ২ জন একসাথে // update করলে?
💥
হাচান admin হয়ে service add করল, একই সাথে রহিম teacher নিজের service update করল → data corrupt!
সঠিক approach: ৫টা আলাদা table — প্রতিটার আলাদা দায়িত্ব

৫ Table-এর কাজ

  • AddOnService — master list (admin তৈরি করে)
  • CourseDefaultService — course-এ default কোনগুলো
  • CourseAdminServiceOffering — admin force-add করেছে
  • Price — প্রতিটার price আলাদা
  • Selection — student কোনটা কিনল
SLIDE 02

৫-Table Design — সম্পূর্ণ Schema

প্রতিটা table একটাই কাজ করে
-- 1. Master service list (Admin manage করে) CREATE TABLE AddOnService ( id UUID PRIMARY KEY, name VARCHAR(100), -- "Live Session" description TEXT, isActive BOOLEAN DEFAULT true ); -- 2. Course-এর default services (Teacher set করে) CREATE TABLE CourseDefaultService ( courseId UUID REFERENCES Course(id), serviceId UUID REFERENCES AddOnService(id), isEnabled BOOLEAN DEFAULT true, PRIMARY KEY (courseId, serviceId) ); -- 3. Admin force-add (overrides teacher) CREATE TABLE CourseAdminServiceOffering ( courseId UUID REFERENCES Course(id), serviceId UUID REFERENCES AddOnService(id), addedBy UUID REFERENCES User(id), PRIMARY KEY (courseId, serviceId) ); -- 4. Prices (per course per service) CREATE TABLE ServicePrice ( courseId UUID, serviceId UUID, price DECIMAL(10,2), currency VARCHAR(3) DEFAULT 'BDT', PRIMARY KEY (courseId, serviceId) ); -- 5. Student selection CREATE TABLE StudentServiceSelection ( studentId UUID REFERENCES User(id), courseId UUID, serviceId UUID, purchasedAt TIMESTAMP DEFAULT NOW(), PRIMARY KEY (studentId, courseId, serviceId) );

কে কী করতে পারে?

Actor কাজ
Admin AddOnService তৈরি/বন্ধ
Admin যেকোনো course-এ force-add
Teacher রহিম নিজের course-এ enable/disable
Teacher রহিম Price set করা
Student করিম কিনে নেওয়া (Selection)
💡
রহিম "PDF Notes" disable করলেও Admin যদি force-add করে থাকে তাহলে সেটা দেখাবেই। Admin > Teacher।
SLIDE 03

Race Condition Safe — Upsert Pattern

২ জন একসাথে update করলে data নষ্ট হবে না
Race Condition: রহিম ও হাচান একসাথে "Live Session" enable করতে চাইল — দুজনেই check করল "নেই", দুজনেই INSERT → duplicate!
// ভুল — Race condition const exists = await prisma .courseDefaultService.findFirst({...}); if (!exists) { // রহিম ও হাচান দুজনেই এখানে আসল! await prisma.courseDefaultService.create({ data: { courseId, serviceId, isEnabled: true } }); // Duplicate key error! }
Upsert Pattern: আছে → update, নেই → create — একটাই atomic operation
// সঠিক — Upsert (atomic) await prisma.courseDefaultService.upsert({ where: { courseId_serviceId: { courseId, serviceId } }, create: { courseId, serviceId, isEnabled: true }, update: { isEnabled: true } // রহিম ও হাচান একসাথে করলেও // PostgreSQL একটাই চালাবে // দুটো duplicate হবে না });
🔒
PostgreSQL-এর ON CONFLICT DO UPDATE — database level-এ atomic। Application code-এ race condition নেই।
SLIDE 04

N+1 Problem → Smart Merge দিয়ে Fix

৫টা table থেকে একটা query-তে সব আনো
N+1: প্রতিটা service-এর জন্য আলাদা price query → ১০ service = ১০ DB query
একটা query-তে সব — services + prices + admin overrides
async function getTeacherServices( courseId: string, teacherId: string ) { // সব কিছু একবারে load const [defaults, adminAdded, prices] = await Promise.all([ // Teacher-এর default services prisma.courseDefaultService.findMany({ where: { courseId }, include: { service: true } }), // Admin force-added prisma.courseAdminServiceOffering.findMany({ where: { courseId }, include: { service: true } }), // সব prices prisma.servicePrice.findMany({ where: { courseId } }) ]); return formatTeacherServices( defaults, adminAdded, prices ); }
function formatTeacherServices( defaults, adminAdded, prices ) { // Price lookup map তৈরি (O(1) access) const priceMap = new Map( prices.map(p => [p.serviceId, p.price]) ); // Admin-added services set const adminSet = new Set( adminAdded.map(a => a.serviceId) ); // Smart merge const allServiceIds = new Set([ ...defaults.map(d => d.serviceId), ...adminAdded.map(a => a.serviceId) ]); return [...allServiceIds].map(serviceId => { const def = defaults.find( d => d.serviceId === serviceId ); const adm = adminAdded.find( a => a.serviceId === serviceId ); const service = def?.service || adm?.service; return { serviceId, name: service.name, isEnabled: adminSet.has(serviceId) ? true // Admin override — always on : (def?.isEnabled ?? false), isAdminForced: adminSet.has(serviceId), price: priceMap.get(serviceId) ?? 0, canDisable: !adminSet.has(serviceId), }; }); }
SLIDE 05

Student কী দেখবে? কীভাবে কিনবে?

করিম রহিমের course-এ "Live Session" কিনতে চায়

করিম course page-এ যায় → getStudentServices(courseId) call হয়

Available services দেখায়: "Live Session — ৫০০ টাকা", "PDF Notes — ২০০ টাকা"

করিম "Live Session" select করে → Payment করে

Payment success → StudentServiceSelection-এ insert করো (Upsert — duplicate নেই)

করিম এখন "Live Session"-এ access পাবে — তার selection-এ আছে

// Student কোন services কিনেছে check async function checkServiceAccess( studentId, courseId, serviceId ) { const selection = await prisma .studentServiceSelection.findUnique({ where: { studentId_courseId_serviceId: { studentId, courseId, serviceId } } }); return !!selection; }
SLIDE 06

যা শিখলাম — AddOn Service System

Complex feature, clean design

5-Table Design

  • প্রতিটা entity আলাদা table
  • Admin, Teacher, Student — আলাদা দায়িত্ব
  • Price flexible per course

Safe Code

  • Upsert → race condition নেই
  • Promise.all → N+1 নেই
  • Admin override → always wins

Smart Merge

  • formatTeacherServices() — একটা function
  • Map/Set → O(1) lookup
  • canDisable flag → UI জানে কী দেখাবে
🎯
রহিম teacher তার course-এ "Live Session" add করল। হাচান admin force করল "PDF Notes"। করিম student দুটোই দেখল — "PDF Notes" disable করতে পারল না (admin forced)। "Live Session" কিনল। সব কিছু সঠিক — race condition নেই, N+1 নেই।
সব একসাথে ধাপে ধাপে