সরাসরি পর্বে যান
পর্ব ৫৭ · ৬৬-এর মধ্যে · মডিউল ৯

Case study: notification system — push, SMS, email আর নির্ভরযোগ্য delivery

Case Study: Designing a Notification System — Push, SMS, Email & Reliable Delivery

Taught in Bangla, this lesson designs a multi-channel notification platform for push, SMS, email and in-app messages: requirements, estimation, API and data model, a reliable pipeline with outbox, idempotency keys, backoff retries, dead letter queues, provider failover and delivery receipts, separate priority lanes so OTPs never wait behind promotions, per-user limits, preferences, opt-out, quiet hours, templates and device-token lifecycle, with a simulator.

এই পর্বে যা শিখবেন

  • একটি বহু-channel notification প্ল্যাটফর্মের requirement, হিসাব, API আর data model ধাপে ধাপে দাঁড় করানো
  • OTP আর প্রচারের বার্তাকে আলাদা রেখে অগ্রাধিকার, rate limit আর per-user সীমা নকশা করা — simulator চালিয়ে
  • Provider বিকল হলে failover, retry আর duplicate SMS-এর trade-off সংখ্যায় মাপা
  • Preference, opt-out, quiet hours, বাংলা-ইংরেজি template আর device token-এর জীবনচক্র সামলানো
  • খরচ, অপব্যবহার, observability আর ৬০ সেকেন্ডের ইন্টারভিউ-সারাংশ — আর AI-এর দেওয়া নকশা যাচাই

১ · সমস্যা আর পরিধি — কে, কাকে, কী, কখন জানাবে

ধরা যাক “খবরি” (কাল্পনিক) একটি বড় অ্যাপ-কোম্পানির ভেতরের notification প্ল্যাটফর্ম। কোম্পানির login সেবা OTP পাঠায়, wallet সেবা জানায় “৳৫০০ এসেছে”, অর্ডার সেবা জানায় “পার্সেল পথে”, আর marketing দল ঈদের আগে ৫০ লাখ মানুষকে অফার পাঠাতে চায়। প্রতিটি দল আলাদা করে SMS কোম্পানির সাথে চুক্তি, push-এর কোড আর retry লিখলে একই ভুল দশবার হয়, আর কেউ জানে না একজন মানুষ আজ মোট কতগুলো বার্তা পেলেন। তাই একটি সাধারণ প্ল্যাটফর্ম: সবাই একটি API ডাকে, বাকিটা প্ল্যাটফর্মের দায়।

পুরোনো দিনের বড় ডাকঘরের কথা ভাবুন। রেজিস্ট্রি চিঠি, জরুরি টেলিগ্রাম আর দোকানের বিজ্ঞাপনের লিফলেট — সব এক দরজায় জমা হয়। কিন্তু বাছাইয়ের টেবিলে সেগুলো আলাদা ব্যাগে যায়: টেলিগ্রাম পরের গাড়িতেই, লিফলেট সুবিধামতো। কেউ “আমাকে লিফলেট পাঠাবেন না” বলে গেলে তাঁর নাম খাতায় ওঠে; রাতে কারও দরজায় লিফলেট ফেলা হয় না; আর রেজিস্ট্রি চিঠির প্রাপ্তিস্বীকার ফিরে আসে। Notification প্ল্যাটফর্ম ঠিক এই ডাকঘর।

পর্ব ৬-এর আট ধাপের কাঠামো ধরে এগোব: পরিধি, হিসাব, API, data model, ছবি, তিনটি deep dive, ভাঙন আর সারাংশ। প্রতিটি পুরোনো ধারণার জন্য এক বাক্য প্রয়োগ আর তার পর্বের link — নতুন করে শেখানো নয়।

Functional requirement

  • ভেতরের যেকোনো সেবা একজন ব্যবহারকারী (বা একটি তালিকা) আর একটি template দিয়ে বার্তা পাঠাতে বলবে; প্ল্যাটফর্ম চারটি channel-এ পৌঁছাবে — মোবাইল push (ফোনের platform push service, যেমন Apple-এর APNs বা Google-এর FCM দিয়ে), SMS (SMS gateway বা aggregator দিয়ে), email আর অ্যাপের ভেতরের in-app inbox.
  • দুই জাতের বার্তা: transactional (OTP, টাকা আসা-যাওয়া, টিকিট নিশ্চিত) আর promotional (অফার, প্রচার)। প্রথমটি দ্রুত আর নিশ্চিত, দ্বিতীয়টি ধীরে আর নিয়ম মেনে।
  • ব্যবহারকারীর preference: কোন ধরনের বার্তা কোন channel-এ, ভাষা (বাংলা বা ইংরেজি), quiet hours, আর অফার বন্ধ করা (opt-out).
  • নির্ধারিত সময়ে পাঠানো (campaign “সকাল ১০টায়, প্রত্যেকের নিজের সময়ে”), প্রতিটি বার্তার অবস্থা দেখা (queued, sent, delivered, failed) আর provider-এর প্রাপ্তিস্বীকার গ্রহণ।

Non-functional requirement

  • OTP দ্রুত: API-তে আসা থেকে provider-এর গ্রহণ পর্যন্ত p99 ৫ সেকেন্ডের মধ্যে (ধরা যাক)। Provider থেকে ফোনে পৌঁছানো অপারেটরের হাতে — সেটি মাপব, কিন্তু প্রতিশ্রুতি দেব না।
  • হারাবে না: API একবার 202 Accepted বললে বার্তাটি টেকসই জায়গায় আছে; transactional বার্তা নীরবে হারানো চলবে না। Duplicate যত কম সম্ভব।
  • বিচ্ছিন্নতা: একটি দলের বিশাল campaign বা ভুল loop অন্য দলের OTP ধীর করবে না।
  • Availability: API-র জন্য ৯৯.৯৫% (পর্ব ৫-এর nines-এর হিসাব)।
  • খরচ আর গোপনীয়তা: SMS-এর বিল নজরে; ফোন নম্বর আর OTP-র কোড log-এ যাবে না।

পরিধির বাইরে: নিজের SMS নেটওয়ার্ক বা অপারেটরের SMSC বানানো; কাকে কোন অফার দেখানো হবে তার segmentation বা ranking (আমরা শুধু তালিকা নিই); chat-এর বার্তা পৌঁছানো নিজে (সেটি পর্ব ৫৫-এর chat system; অ্যাপ বন্ধ থাকলে chat এই প্ল্যাটফর্ম দিয়েই push পাঠায়); email-এর deliverability-র ভেতরের খুঁটিনাটি (Computer Networks কোর্সের ইমেইল — SMTP, SPF, DKIM, DMARC পর্বে)।

ইন্টারভিউয়ার যে প্রশ্নগুলো শুনতে চান

প্রশ্নকেন জরুরিধরে নেওয়া উত্তর
কোন কোন channel? কোনটি সবচেয়ে জরুরি?SMS-এর খরচ আর গতি একেবারে আলাদাচারটিই; OTP-র জন্য SMS প্রধান
দিনে কতজন সক্রিয়, কতগুলো বার্তা?Worker, queue আর storage-এর মাপ১ কোটি সক্রিয়; অংশ ২-এর হিসাব
সবচেয়ে বড় campaign কত বড়, কত দ্রুত?Peak আসে campaign থেকে, গড় থেকে নয়৫০ লাখ push, ১০ মিনিটে
Duplicate বেশি খারাপ, না হারানো?Delivery semantics-এর পছন্দ ঠিক করেOTP আর লেনদেনে হারানো খারাপ; অফারে duplicate খারাপ
ব্যবহারকারী কোথায় থাকেন?Time zone আর quiet hoursবেশিরভাগ বাংলাদেশে, কিছু প্রবাসে
আইন বা নিয়ন্ত্রণের শর্ত?Opt-out, প্রচারের সময়, sender IDআইনি দলের সাথে মিলিয়ে নীতি config-এ

বাংলাদেশের প্রেক্ষাপটে: অনেকে সবসময় মোবাইল ডেটা চালু রাখেন না, অনেকে feature phone ব্যবহার করেন, আর ব্যাংক ও mobile financial service-এর লেনদেনের খবর বা OTP-তে SMS এখনো খুব প্রচলিত (নির্দিষ্ট পরিসংখ্যান দাবি করছি না)। তাই “সব push-এ, SMS বাদ” নকশা সস্তা, কিন্তু অসম্পূর্ণ।

২ · হিসাব — কতগুলো বার্তা, কত দ্রুত, কত টাকা

পর্ব ৩-এর back-of-the-envelope হিসাব-এর নিয়মে সব ইনপুট আগে লিখে নিই, সবই “ধরা যাক”: দিনে ১ কোটি সক্রিয় ব্যবহারকারী; প্রত্যেকে দিনে গড়ে ৬টি push, ৪টি in-app, ০.৫টি email আর ০.১টি SMS পান। সবচেয়ে ব্যস্ত ঘণ্টা গড়ের ৩ গুণ (peak factor); তার ওপর একটি campaign ১০ মিনিটে ৫০ লাখ push পাঠায়। প্রতিটি worker সেকেন্ডে কতগুলো পাঠাতে পারে, সেটিও অনুমান — বাস্তবে load test করে মাপতে হয়। SMS-এর প্রতি টুকরোর দাম ধরা হলো ০.৩০ টাকা — এটি কাল্পনিক সংখ্যা, কোনো প্রদানকারীর দর নয়; আসল দাম চুক্তি, sender-এর ধরন আর সময়ভেদে আলাদা।

// Back-of-the-envelope for a fictional notification platform. Every input is an assumption.
const DAU = 10e6;                                            // daily active users
const perUser = { push: 6, inapp: 4, email: 0.5, sms: 0.1 }; // notifications per active user per day
const peakFactor = 3;                                        // busiest hour vs daily average
const blast = 5e6, blastMin = 10;                            // one promo push campaign in 10 minutes
const perWorker = { push: 500, inapp: 2000, email: 100, sms: 50 }; // sends/s per worker (assumed)
const util = 0.7;                                            // keep workers at most 70% busy
const fmt = n => Math.round(n).toLocaleString("en-US");
let total = 0;
console.log("channel   per day       avg/s   peak/s  workers");
for (const ch of Object.keys(perUser)) {
  const day = DAU * perUser[ch]; total += day;
  const avg = day / 86400;
  const peak = avg * peakFactor + (ch === "push" ? blast / (blastMin * 60) : 0);
  const workers = Math.max(2, Math.ceil(peak / (perWorker[ch] * util))); // min 2 for redundancy
  console.log(ch.padEnd(7) + fmt(day).padStart(12) + fmt(avg).padStart(10) +
    fmt(peak).padStart(9) + String(workers).padStart(9));
}
console.log("all channels: " + fmt(total) + " per day, " + fmt(total / 86400) + "/s on average");
// SMS cost: Bangla text needs Unicode (UCS-2): 70 chars in one SMS, 67 per part when longer.
const sms = DAU * perUser.sms, otpShare = 0.8;              // 80% OTP (1 part), 20% alerts (2 parts)
const parts = sms * otpShare * 1 + sms * (1 - otpShare) * 2;
const price = 0.30;                                          // taka per SMS part: ASSUMED, not a quote
console.log("SMS parts/day " + fmt(parts) + ", cost/day " + fmt(parts * price) +
  " taka, cost/30 days " + fmt(parts * price * 30) + " taka");
// Status log: one row per notification, ~400 bytes, kept 90 days, 3 replicas.
const bytes = total * 400 * 90 * 3;
console.log("status log for 90 days x3 replicas: " + (bytes / 1e12).toFixed(1) + " TB");

আউটপুট:

channel   per day       avg/s   peak/s  workers
push     60,000,000       694   10,417       30
inapp    40,000,000       463    1,389        2
email     5,000,000        58      174        3
sms       1,000,000        12       35        2
all channels: 106,000,000 per day, 1,227/s on average
SMS parts/day 1,200,000, cost/day 360,000 taka, cost/30 days 10,800,000 taka
status log for 90 days x3 replicas: 11.4 TB

চারটি শিক্ষা পড়ে নিন। এক, সংখ্যায় push সবচেয়ে বড় (দিনে ৬ কোটি), কিন্তু তার peak ১০,৪১৭/s-এর ৮,৩৩৩/s এসেছে শুধু campaign থেকে — শুধু ব্যস্ত ঘণ্টার চাপ (গড়ের ৩ গুণ) দিয়ে মাপলে worker লাগত ৬টি, campaign ধরলে ৩০টি। দুই, SMS সংখ্যায় ছোট (দিনে ১০ লাখ, peak ৩৫/s), তবু দুটির কম worker নয় — একটি মরলে যেন OTP না থামে। তিন, খরচ উল্টো দিকে: ১২ লাখ SMS-টুকরোয় দিনে ৩,৬০,০০০ টাকা, ৩০ দিনে প্রায় ১ কোটি ৮ লাখ টাকা (ধরে নেওয়া দামে), অথচ platform push service সাধারণত প্রতি বার্তায় ফি নেয় না (লেখার সময়ে; নিজের সার্ভারের খরচ অবশ্যই আছে)। চার, প্রতিটি বার্তার অবস্থার একটি সারি ৯০ দিন আর তিন কপিতে প্রায় ১১.৪ TB — একটি ডেটাবেস সার্ভারের সাধ্যের বাইরে, তাই partition লাগবে (অংশ ৪)।

টুকরোর হিসাবটি খেয়াল করুন। GSM-এর ৭-bit অক্ষরে একটি SMS-এ ১৬০টি অক্ষর ধরে, কিন্তু বাংলা লেখা Unicode (UCS-2)-এ যায়, তখন এক টুকরোয় মাত্র ৭০টি; লম্বা হলে প্রতিটি SMS segment-এ ৬৭টি। ১২০ অক্ষরের বাংলা সতর্কবার্তা তাই দুই টুকরো, একই দৈর্ঘ্যের ইংরেজি এক টুকরো; ইংরেজি লেখায় একটি বাংলা অক্ষর বা “বাঁকা” উদ্ধৃতিচিহ্ন ঢুকলেও পুরোটা Unicode-এ যেতে পারে। Template লেখার সময়েই দৈর্ঘ্য যাচাই (অংশ ৮) সরাসরি টাকা বাঁচায়। SMS-এর জন্মকথা Computer Networks কোর্সের মোবাইল নেটওয়ার্ক পর্বে।

৩ · API — একটি অনুরোধ, একটি key, বাকিটা প্ল্যাটফর্মের দায়

প্রধান API একটিই, আর সেটি asynchronous: প্ল্যাটফর্ম বার্তাটি টেকসইভাবে জমা নিয়ে সঙ্গে সঙ্গে 202 Accepted বলে; পাঠানো হয় পরে। ডাকা সেবা SMS provider-এর ধীরতায় আটকায় না। প্রতিটি অনুরোধে একটি idempotency key বাধ্যতামূলক (পর্ব ৮-এর REST API নকশা): network-এ উত্তর হারিয়ে ডাকা সেবা আবার পাঠালে একই key দেখে প্ল্যাটফর্ম আগের notification-এর id ফেরত দেয়, দ্বিতীয় SMS বানায় না।

POST /v1/notifications HTTP/1.1
Authorization: Bearer <service token of auth-service>
Idempotency-Key: login-otp-u42-7781
Content-Type: application/json

{ "recipient": { "user_id": "u42" },
  "category":  "auth.otp",            // decides priority, caps, quiet hours
  "channels":  ["sms"],
  "template":  "otp.login",
  "vars":      { "code": "482913" },  // secret: never logged
  "ttl_seconds": 300 }                // useless after 5 minutes

HTTP/1.1 202 Accepted
{ "notification_id": "n-9001", "status": "accepted" }

আরও কয়েকটি endpoint, ছোট করে:

  • POST /v1/campaigns — template, audience-এর তালিকা বা segment-এর id, schedule: {"local_time": "10:00"}, সর্বোচ্চ হার; বড় campaign-এ দ্বিতীয় একজনের অনুমোদন লাগে।
  • GET/PUT /v1/users/{id}/preferences — কোন বিষয় কোন channel-এ, ভাষা, time zone, quiet hours, opt-out.
  • PUT /v1/users/{id}/devices — অ্যাপ চালু বা login হলে নিজের device token, platform আর ভাষা জানায়; logout-এ DELETE.
  • GET /v1/notifications/{id} — অবস্থা আর প্রতিটি চেষ্টার ইতিহাস; GET /v1/users/{id}/inbox?cursor=… — in-app তালিকা, cursor pagination-এ।
  • POST /hooks/{provider} — provider-দের delivery receipt আসার পথ; signature যাচাই ছাড়া কিছুই গৃহীত নয় (অংশ ৬)।

Key-এর নিয়ম পর্ব ৩৪-এর delivery semantics থেকে: key-এর পরিধি “কোন সেবা + key”, যাতে দুই দলের একই key সংঘাত না করে; একই key-তে ভিন্ন body এলে 422; আর key মনে রাখার সময়, অর্থাৎ dedup window, ২৪ ঘণ্টা — সবচেয়ে দেরিতে আসা retry-র চেয়ে লম্বা।

৪ · Data model — কোন তথ্য কোথায়, আর কেন

প্রতিটি তথ্যের পড়া-লেখার ধরন আলাদা, তাই store-ও আলাদা। নিয়মটি পর্ব ২০-এর SQL বনাম NoSQL আর পর্ব ২৩-এর partitioning থেকে: আগে access pattern, তারপর store আর partition key.

Entityমূল fieldAccess patternStore আর key
notification + deliveryid, tenant, user, category, template@version, প্রতিটি চেষ্টা: channel, provider, provider_msg_id, state, সময়খুব বেশি লেখা; id ধরে পড়া; ৯০ দিন রাখাWide-column বা key-value store; key = notification_id; TTL ৯০ দিন
provider_msg_id → notification_idprovider, তাদের id, আমাদের idReceipt এলে এক key-তে খোঁজাKey-value; key = provider + provider_msg_id; TTL ৭ দিন
inboxuser, সময়, notification_id, পড়া হয়েছে কি না“আমার সর্বশেষ ২০টি”Wide-column; partition = user_id, সময় ধরে উল্টো ক্রমে সাজানো
preference, device, templateবিষয় × channel, ভাষা, tz, quiet hours, opt-out-এর সময় ও উৎস; token, platform, last_seen; template-এর version, ভাষা, channelপ্রতিটি বার্তায় পড়া, কালেভদ্রে লেখাRelational (যেমন PostgreSQL) + cache (পর্ব ২৮-এর cache-aside)
idempotency keytenant + key → notification_id, body-র hashপ্রতিটি অনুরোধে একবার “আছে কি?”Redis-ধাঁচের store; atomic set-if-absent (না থাকলে তবেই লেখা) আর TTL ২৪ ঘণ্টা

Delivery-র অবস্থা একটি ছোট state machine: accepted → queued → sending → sent → delivered, পাশে failed, expired, suppressed আর deferred. “sent” মানে provider গ্রহণ করেছে, “delivered” মানে provider জানিয়েছে ফোনে পৌঁছেছে — দুটি আলাদা রাখা জরুরি, কারণ মাঝখানে অনেক কিছু হারাতে পারে। Receipt আগে-পরে আসতে পারে, তাই নিয়ম: অবস্থা কেবল সামনে এগোয়, পেছনে ফেরে না (দেরিতে আসা “sent” একটি “delivered”-কে নামিয়ে দেবে না)।

Preference প্রতিটি বার্তায় পড়া হয় — দিনে ১০ কোটিবার — কিন্তু বদলায় কালেভদ্রে, তাই cache-এ রাখা হয়। শর্ত: opt-out-এর পরে পুরোনো কপি যেন অফার পাঠাতে না থাকে — লেখার সময় cache মোছা আর ছোট TTL (পর্ব ২৯-এর invalidation).

৫ · High-level design — producer থেকে ফোন, আর receipt ফিরে আসা

Notification প্ল্যাটফর্ম — লেখার পথ আর প্রাপ্তিস্বীকারের পথ

Producers → API (idempotency) → router (preferences, caps, templates) → one queue per channel and priority → worker pools → providers → receipts back into the status store

Notification প্ল্যাটফর্মের architecture Auth · OTP outbox থেকে Payment · Order outbox থেকে Campaign batch, নির্ধারিত সময় Notification API auth · যাচাই · idempotency · rate limit Dedup store key · TTL 24h Router preference · cap · quiet hours · template Pref · Tmpl DB + cache push high push low SMS OTP SMS bulk email সব in-app inbox Channel workers — প্রতি লেনে আলাদা pool rate limit · retry + backoff · circuit breaker · DLQ APNs / FCM → ফোন SMS A প্রধান SMS B failover Email ESP → mailbox Receipt handler webhook · DLR · bounce · token বাতিল — signature যাচাই Status store · inbox · metric অবস্থা কেবল সামনে এগোয়
ছবি ৫৭.১ — নীল তীর: বার্তার পথ; কমলা ড্যাশ: provider থেকে ফিরে আসা খবর। লাল লেন দুটি transactional — তাদের worker pool আলাদা। Worker-রাও প্রতিটি চেষ্টার পরে status store-এ লেখে (ছবিতে দেখানো হয়নি)।

লেখার পথ: producer সেবা নিজের ডেটাবেসের transaction-এ ঘটনাটি outbox-এ লেখে, একটি relay সেটি API-তে পাঠায় (পর্ব ২৫-এর outbox pattern) — তাই “টাকা এল অথচ খবর গেল না” হয় না। API সেবার পরিচয় যাচাই করে (পর্ব ৫২-এর mTLS বা service token), key দেখে, প্রতিটি সেবার quota দেখে, তারপর একটি টেকসই ingest queue-তে লিখে 202 দেয়। Router preference আর template দেখে বার্তাটিকে এক বা একাধিক channel-এর লেনে পাঠায়; প্রতিটি লেন একটি আলাদা queue (পর্ব ৩১-এর message queue), যার পেছনে আলাদা worker pool. Worker provider-কে ডাকে, ফল লেখে।

পড়ার পথ হালকা: inbox খুললে user_id-এর partition থেকে সর্বশেষ বার্তা; অবস্থা দেখতে id ধরে খোঁজা। ফেরার পথ: provider-রা delivery receipt পাঠায় webhook-এ (পর্ব ৩২-এর webhook অংশ); সেগুলোও আগে queue-তে জমে, যাতে ঢল handler বসিয়ে না দেয়।

এবার একটি OTP-কে শুরু থেকে শেষ পর্যন্ত অনুসরণ করুন। Provider A ঠিক সেই মুহূর্তে অসুস্থ। “পরে” চাপুন:

  1. Auth সেবাkey: login-otp-u42-7781কোড তৈরি
  2. API202 · n-9001key নতুন
  3. Routersms-otp লেনtemplate otp.login
  4. SMS Atimeout 3 sউত্তর নেই
  5. SMS Bid b-55গৃহীত
  6. ReceiptDELIVEREDstatus store
notification n-9001 — u42-এর login OTP
সময়
অবস্থা
Provider
SMS-এর লেখা
A-র circuit breaker
  1. কোড তৈরি। u42 login-এ “কোড পাঠান” চাপলেন। Auth সেবা ৬ অঙ্কের কোড বানাল, নিজের ডেটাবেসে শুধু তার hash আর ৫ মিনিটের মেয়াদ রাখল, তারপর একটি key দিয়ে API ডাকল। এই key-ই পরের সব retry-তে থাকবে।
  2. API গ্রহণ করল। Dedup store-এ atomic set-if-absent — key নতুন, তাই notification n-9001 তৈরি। একই ফোনে ১০ মিনিটে ৩টির বেশি OTP নয়, এই সীমাও পার হলো (পর্ব ৪৯-এর rate limiting). বার্তা টেকসই queue-তে লেখার পরেই 202 — মাত্র ১৫ ms.
  3. Router. বিষয় auth.otp transactional, তাই quiet hours আর দৈনিক সীমা প্রযোজ্য নয়। ভাষা বাংলা, template otp.login-এর v3 বসল: ৫৩ অক্ষর — Unicode-এ এক টুকরো। বার্তা গেল SMS-OTP লেনে, যেখানে কোনো campaign-এর ভিড় নেই।
  4. A নীরব। Worker ২ ms-এর মধ্যে বার্তা তুলে A-কে ডাকল, আমাদের id n-9001 সাথে দিয়ে। ৩ সেকেন্ডের timeout পেরোল, উত্তর নেই। এটি সবচেয়ে কঠিন অবস্থা: SMS গেছে কি না, আমরা জানি না। একই সময়ে অন্য OTP-তেও A ব্যর্থ — breaker গুনছে।
  5. B দিয়ে পাঠানো। গত ২০টি চেষ্টার ১২টিই A-তে ব্যর্থ, তাই breaker open: এখন থেকে নতুন OTP সরাসরি B-তে (পর্ব ৪৮-এর circuit breaker). n-9001-ও প্রায় আধা সেকেন্ডের jitter-সহ অপেক্ষার পরে B-তে গেল, একই কোড নিয়ে। B গ্রহণ করল, id b-55; অবস্থা “sent”.
  6. Receipt এল। ১ সেকেন্ড পরে B-র webhook: b-55 DELIVERED. Handler signature যাচাই করল, b-55 থেকে n-9001 খুঁজে অবস্থা “delivered” করল। মোট সময় ৪.৬ সেকেন্ড। u42 কোড লিখে login করলেন।
  7. A-র দেরির খবর। ৪১ সেকেন্ডে A জেগে উঠে জানাল, সে-ও SMS-টি পাঠিয়েছিল। u42 দ্বিতীয় SMS পেলেন — কিন্তু একই কোড, আর কোড ততক্ষণে ব্যবহৃত; ক্ষতি সামান্য বিরক্তি। কোড বদলে পাঠালে দুটি ভিন্ন কোড বিভ্রান্তি তৈরি করত। অবস্থা “delivered”-ই থাকে, শুধু “duplicate SMS” metric এক বাড়ে। Breaker কিছুক্ষণ পরে half-open হয়ে A-কে আবার পরীক্ষা করবে।

৬ · Deep dive ক — নির্ভরযোগ্য delivery pipeline

পুরো pipeline at-least-once: প্রতিটি ধাপ নিশ্চিত হওয়ার পরেই পরের ধাপে ack দেয়, ব্যর্থ হলে আবার চেষ্টা করে। ফলে duplicate আসবে, আর প্রতিটি ধাপকে তা সইতে হবে — key দিয়ে API-তে, notification_id দিয়ে worker-এ আর status store-এ (পর্ব ৩৪)। চারটি জায়গায় নকশার সিদ্ধান্ত লাগে।

১. কোন ত্রুটিতে retry, কতবার, কতক্ষণ

Timeout, 5xx আর 429 (সাথে Retry-After) ক্ষণিকের — exponential backoff আর jitter-সহ আবার চেষ্টা। ভুল নম্বর, “এই token আর নেই” বা নিষিদ্ধ লেখা non-retryable — আবার চেষ্টা মানে শুধু টাকা নষ্ট। প্রতিটি বার্তার মেয়াদ (TTL) থাকে: ৫ মিনিট পরে OTP অর্থহীন, তাই retry নয়, expired. আর যে বার্তা প্রতিবার ব্যর্থ হবে (যেমন template-এ ভাঙা ভ্যারিয়েবল), তা কয়েকবার পরে DLQ-তে, ঠিক করার পরে redrive (পর্ব ৩১ আর পর্ব ৩৭-এর background job).

২. Provider failover আর circuit breaker — সংখ্যায়

দুটি SMS provider রাখা সাধারণ চর্চা, কিন্তু কীভাবে বদলাবেন? নিচের simulator-এ ১০ মিনিটে ২,০০০টি OTP; ১৮০ থেকে ৩৬০ সেকেন্ড পর্যন্ত A বিকল — প্রতিটি ডাক ৫ সেকেন্ডে timeout, আর তার ১০%-এ SMS আসলে চলে গিয়েছিল (অস্পষ্ট ফল)। বাকি সময়ে A-তে ২% আর B-তে ৩% ক্ষণিকের ত্রুটি। ৫% অনুরোধ producer নিজেই দুবার পাঠায় (একই key). প্রতিটি OTP-তে সর্বোচ্চ ৪টি চেষ্টা। তিনটি নীতি তুলনা করুন: শুধু A; প্রতিটি বার্তায় A আর B পালা করে; আর তার সাথে A-র জন্য একটি circuit breaker.

// 2,000 OTP SMS in 10 minutes. Provider A (primary) has an outage from 180 s to 360 s: calls time
// out after 5 s, and 10% of those timed-out SMS were actually sent (ambiguous). B is the backup.
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const g = rng(11), reqs = [];
for (let i = 0; i < 2000; i++) { const t = g() * 600; reqs.push({ key: "otp-" + i, t });
  if (g() < 0.05) reqs.push({ key: "otp-" + i, t: t + 1 + g() * 3 }); }   // producer retried: same key
reqs.sort((a, b) => a.t - b.t);
function run(policy) {
  const rand = rng(99), seen = new Set(), ttd = []; let blocked = 0, dup = 0, lost = 0, sick = 0, hist = [], openUntil = -1;
  const attempt = (p, t) => (p === "A" && t >= 180 && t < 360) ? (sick++, { dur: 5, ok: false, sent: rand() < 0.1 })
    : rand() < (p === "A" ? 0.02 : 0.03) ? { dur: 0.3, ok: false, sent: false }       // clear error
    : { dur: p === "A" ? 1.5 + rand() * 1.5 : 2 + rand() * 2, ok: true, sent: true };
  for (const r of reqs) {
    if (seen.has(r.key)) { blocked++; continue; } seen.add(r.key);  // idempotency key at the API
    let t = r.t, sends = 0, first = null; const startB = policy === "breaker" && t < openUntil;
    for (let k = 0; k < 4; k++) {                                   // at most 4 attempts
      const p = policy === "A only" ? "A" : ((k % 2 === 0) !== startB ? "A" : "B");
      const a = attempt(p, t);
      if (policy === "breaker" && p === "A" && k === 0) { hist = [...hist.slice(-9), a.ok];
        if (hist.filter(x => !x).length >= 5) { openUntil = t + 30; hist = []; } } // open for 30 s
      if (a.sent) { sends++; if (first === null) first = t + a.dur - r.t; }
      if (a.ok) break;
      t += a.dur + Math.pow(2, k) * (0.5 + rand() / 2);              // backoff 1,2,4 s with jitter
    }
    if (first === null) lost++; else ttd.push(first); if (sends > 1) dup++;
  }
  const s = ttd.sort((x, y) => x - y), q = p => s[Math.ceil(p * s.length) - 1].toFixed(1);
  console.log(policy.padEnd(12) + (100 * s.length / 2000).toFixed(1).padStart(7) + "%" +
    String(sick).padStart(7) + q(0.5).padStart(7) +
    q(0.99).padStart(7) + String(lost).padStart(6) + String(dup).padStart(6) + String(blocked).padStart(9));
}
console.log("policy        deliv. A-down    p50    p99  lost 2xSMS key-dups"); ["A only", "A<->B", "breaker"].forEach(run);

আউটপুট:

policy        deliv. A-down    p50    p99  lost 2xSMS key-dups
A only         83.7%   2205    2.4   25.3   327    46      101
A<->B         100.0%    605    2.6    9.9     0    59      101
breaker       100.0%     51    2.5    9.8     0     4      101

শুধু A-তে প্রায় ১৬% OTP (৩২৭টি) হারাল — ৪টি চেষ্টাও ৩০ সেকেন্ডের কম সময়ে শেষ, অথচ বিভ্রাট ৩ মিনিটের। A বিকল থাকার সময় তাকে ২,২০৫ বার ডাকা হলো — অসুস্থ সেবাকে আরও চাপ। A↔B পালা-য় সব পৌঁছাল, p99 ৯.৯ সেকেন্ড (প্রথমে A-র ৫ সেকেন্ডের timeout, তারপর B); কিন্তু ব্যবহারকারীর কাছে দুটি SMS গেল ৫৯ জনের — A-র অস্পষ্ট timeout-এর পরে B-ও পাঠিয়েছে। Breaker যোগ করলে কয়েকটি ব্যর্থতার পরেই নতুন OTP সরাসরি B-তে যায়: সব পৌঁছায়, বিকল A-কে ডাকা ৬০৫ থেকে ৫১-তে নামে, আর দুটি SMS মাত্র ৪ জনের। তিন নীতিতেই ১০১টি producer-এর দ্বিতীয় অনুরোধ API-র key দেখে বাদ পড়েছে — key না থাকলে সেগুলো ১০১টি বাড়তি SMS হতো। (মডেলটি সরল: breaker শুধু প্রথম চেষ্টার ফল গোনে, আর ৩০ সেকেন্ড পরে A-কে আবার পরীক্ষা করে।)

৩. Duplicate SMS — কোনটি সহনীয়, সিদ্ধান্ত লিখে রাখুন

পর্ব ৩৪-এ দেখেছেন, SMS বা সাধারণ email-এর মতো side effect আপনার transaction-এ বাঁধা যায় না; অনেক SMS provider আমাদের id নেয় কিন্তু সেই id দিয়ে duplicate আটকানোর নিশ্চয়তা দেয় কি না, তা প্রদানকারীভেদে আলাদা — চুক্তি আর documentation-এ যাচাই করুন। আর provider বদলালে কোনো key-ই কাজ করে না। তাই “exactly-once SMS” প্রতিশ্রুতি নয়; বরং প্রতিটি বিষয়ের জন্য সিদ্ধান্ত:

বিষয়কোনটি বেশি খারাপনীতি
OTPনা পৌঁছানো (login বন্ধ)At-least-once, failover; retry-তে একই কোড, ফলে duplicate নিরীহ; মেয়াদ পেরোলে থামা
টাকা আসা-যাওয়ার খবরনা পৌঁছানোAt-least-once; লেখায় লেনদেনের id রাখুন, যাতে দ্বিতীয় SMS-কে মানুষ “দ্বিতীয় লেনদেন” না ভাবেন
অফার, প্রচারদুবার পাঠানো (বিরক্তি, opt-out)অস্পষ্ট timeout-এর পরে অন্য provider-এ নয়; at-most-once-এর কাছাকাছি
“রাইড এসে গেছে”-ধরনের সময়-নির্ভরদেরিতে পৌঁছানোছোট TTL; push-এ collapse key, যাতে পুরোনোটিকে নতুনটি বদলে দেয় (chat-এর “পরিবার: ৩টি নতুন বার্তা”-র মতো জমানো push-ও এভাবে)

৪. Receipt — শেষ খবর, কিন্তু অসম্পূর্ণ

Receipt-এর webhook নিজেও at-least-once আর ক্রমহীন: একই DLR দুবার আসে, “delivered” আগে আর “sent” পরে আসে। তাই handler signature যাচাই করে (HMAC বা provider-এর নিয়মে), provider_msg_id দিয়ে খোঁজে আর শুধু সামনে এগোনো অবস্থা লেখে। কিছু receipt কখনো আসে না; নির্দিষ্ট সময় পরে অবস্থা “unknown”, অনুমানে “delivered” নয়। আর মাসশেষে provider-এর বিলের সাথে নিজের গণনা মেলানো (reconciliation). Push-এ provider শুধু গ্রহণের খবর দেয়; বাকিটা ভাবনার প্রশ্ন ৩-এ।

৭ · Deep dive খ — অগ্রাধিকার, ভিড় আর হারের নিয়ন্ত্রণ

সবচেয়ে বড় ঝুঁকি: ঈদের অফারের ১০ লাখ SMS আর কারও login-এর OTP একই লাইনে। Provider-রা মিলে সেকেন্ডে ১,০০০ SMS নেয় (ধরা যাক)। এক FIFO queue-তে সামনের ভিড় পেছনের সবাইকে আটকায় — head-of-line blocking. তিনটি নকশা: এক FIFO; একটি priority queue (একই worker, কিন্তু OTP আগে); আর bulkhead — OTP-র জন্য আলাদা queue আর সংরক্ষিত ১০০/s, বাকি ৯০০/s প্রচারের।

// OTP SMS mixed with a 1,000,000-message promo SMS blast. The SMS providers together accept
// 1,000 SMS/s (assumed). Compare one FIFO queue, a priority queue, and a bulkhead split.
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const rand = rng(57), C = 1000, otp = [], promo = [];
for (let t = 0; (t += -Math.log(1 - rand()) / 5) < 1200;) otp.push(t);        // ~5 OTP/s for 20 min
for (let i = 0; i < 1e6; i++) promo.push(i / 50000);                          // blast queued in 20 s
function serve(lanes, rate) {            // one server, strict priority by lane order, 1/rate s each
  const idx = lanes.map(() => 0), done = lanes.map(() => []), end = lanes.map(() => 0); let t = 0;
  while (lanes.some((l, k) => idx[k] < l.length)) {
    let k = lanes.findIndex((l, j) => idx[j] < l.length && l[idx[j]] <= t);
    if (k < 0) { t = Math.min(...lanes.map((l, j) => idx[j] < l.length ? l[idx[j]] : Infinity)); continue; }
    t += 1 / rate; done[k].push(t - lanes[k][idx[k]++]); end[k] = t;
  }
  return [done, end];
}
const pct = (a, p) => { const s = [...a].sort((x, y) => x - y); return s[Math.ceil(p * s.length) - 1]; };
const show = (name, w, promoEnd) => console.log(name.padEnd(22) + (pct(w, 0.5)).toFixed(3).padStart(9) +
  (pct(w, 0.99)).toFixed(3).padStart(10) + String(w.filter(x => x > 120).length).padStart(8) +
  String(Math.round(promoEnd)).padStart(9));
const fifo = [...otp.map(t => [t, 0]), ...promo.map(t => [t, 1])].sort((a, b) => a[0] - b[0]);
let t = 0; const fOtp = []; let fEnd = 0;
for (const [a, k] of fifo) { t = Math.max(t, a) + 1 / C; if (k === 0) fOtp.push(t - a); else fEnd = t; }
console.log("OTP count " + otp.length + ", promo 1000000, capacity " + C + " SMS/s");
console.log("mode                  OTP p50   OTP p99  >120 s  promo done (s)");
show("one FIFO queue", fOtp, fEnd);
const [[pO], pEnd] = serve([otp, promo], C);
show("priority queue", pO, pEnd[1]);
const [[bO]] = serve([otp], 100), [, [bEnd]] = serve([promo], 900);           // 100 + 900 = 1000
show("bulkhead 100 + 900", bO, bEnd);

আউটপুট:

OTP count 5975, promo 1000000, capacity 1000 SMS/s
mode                  OTP p50   OTP p99  >120 s  promo done (s)
one FIFO queue          409.702   967.375    4376     1000
priority queue            0.001     0.002       0     1005
bulkhead 100 + 900        0.010     0.018       0     1111

এক FIFO-তে OTP-র p50 প্রায় ৪১০ সেকেন্ড, p99 প্রায় ৯৬৭ সেকেন্ড; ৫,৯৭৫টির ৪,৩৭৬টি ২ মিনিটের বেশি দেরিতে — যেখানে কোডের মেয়াদ ৫ মিনিট, সেখানে বেশিরভাগ login কার্যত বন্ধ। ১০ লাখ বার্তা ১,০০০/s-এ শেষ হতে লাগে ১,০০০ সেকেন্ড, আর পেছনের OTP সেই পুরো সময় অপেক্ষা করে। Priority queue-তে OTP ১–২ ms-এ যায়, প্রচার শেষ হয় ১,০০৫ সেকেন্ডে (আগে চলে যাওয়া OTP-গুলো প্রায় ৫ সেকেন্ড জায়গা নিল)। Bulkhead-এ OTP-র p50 ১০ ms (১০০/s-এ প্রতিটির ১০ ms), প্রচার শেষ ১,১১১ সেকেন্ডে — সংরক্ষিত ১০০/s বেশিরভাগ সময় অলস থাকে, এটিই বীমার দাম।

তাহলে priority queue-ই সেরা? পুরোপুরি না। Strict priority-তে উঁচু লেন কখনো ফাঁকা না হলে নিচু লেন আটকে থাকে — starvation (Operating Systems কোর্সের Priority scheduling পর্বে aging-সহ)। আর একই worker আর connection pool ভাগ করলে প্রচারের worker-দের crash OTP-কেও ধরে। তাই বাস্তবে লেনভেদে আলাদা queue আর worker pool (bulkhead, পর্ব ৪৮), লেনের ভেতরে প্রয়োজনে অগ্রাধিকার। অনেক দেশে transactional আর promotional SMS-এর আলাদা পথ বা sender-এর ধরন থাকে (বাংলাদেশে “masking” আর “non-masking” sender-এর কথা প্রায়ই শোনা যায়; নিয়ম সময়ভেদে বদলায়, যাচাই করুন) — তাতে bulkhead provider পর্যন্ত পৌঁছায়।

হারের চারটি সীমা

  • Provider-এর সীমা: চুক্তির হার; সব worker মিলে একটি ভাগ করা token bucket (পর্ব ৪৯-এর distributed limiter); 429 এলে AIMD-এর মতো হার অর্ধেক, তারপর ধীরে বাড়ানো। Queue জমলে campaign-এর batch তৈরি থামে — backpressure (পর্ব ১৮-এর overload).
  • প্রতিটি দলের সীমা: প্রতিটি tenant একটি quota পায়, যাতে এক দলের ভুল loop বাকিদের জন্য noisy neighbor না হয়।
  • প্রতিটি ব্যবহারকারীর সীমা: দিনে সর্বোচ্চ ৩টি অ-জরুরি বার্তা — frequency cap (অংশ ৮)।
  • প্রতিটি ফোন নম্বরের OTP-সীমা: খরচ আর অপব্যবহার ঠেকাতে (অংশ ৯)।

নির্ধারিত সময় আর time zone

“সকাল ১০টায়” মানে কার সকাল? ঢাকার ১০টা UTC+3-এর দেশগুলোতে (মধ্যপ্রাচ্যের কয়েকটি দেশ) সকাল ৭টা। তাই campaign সময়-অঞ্চল ধরে ভাগ হয়, প্রতিটি ভাগ একটি delayed job (পর্ব ৩৭)। আর হার সীমিত রাখা লাখো মানুষের একসাথে অ্যাপ খোলার thundering herd-কেও কয়েক মিনিটে ছড়িয়ে দেয় (ভাবনার প্রশ্ন ৪)।

Campaign “ঈদ অফার” — channel: push, লেন: push-low, হার ৫,০০০/s
সময় (ঢাকা)
লক্ষ্য
বাদ (suppressed)
পিছিয়ে (deferred)
Provider গ্রহণ করল
Token অবৈধ
OTP লেন
  1. Campaign তৈরি। Marketing দল ২০ লাখ জনের তালিকা, template আর “প্রত্যেকের স্থানীয় সময়ে ১০টা” দিল; ১০ লাখের বেশি বলে দ্বিতীয় একজনের অনুমোদন লাগল। হার ৫,০০০/s — provider-এর সীমা আর backend-এর ক্ষমতা মিলিয়ে।
  2. ভাগ করা। ঠিক ১০টায় তালিকা ১,০০০ জনের ২,০০০টি batch-এ ভাগ হলো, প্রতিটি একটি job. সব একসাথে queue-তে ঢালা হয় না: push-low লেনের depth একটি সীমা ছাড়ালে নতুন batch তৈরি থামে।
  3. ছাঁকনি। প্রতিটি ব্যবহারকারীর preference দেখা হলো: ৩,০০,০০০ জন অফার বন্ধ করেছেন (opt-out), ১,০০,০০০ জন আজ ৩টি বার্তা পেয়ে গেছেন (cap) — মোট ৪,০০,০০০ বাদ। প্রবাসী ৪০,০০০ জনের স্থানীয় সময় এখনো quiet hours, তাই পিছিয়ে। বাকি ১৫,৬০,০০০।
  4. সীমিত হারে পাঠানো। Token bucket সেকেন্ডে ৫,০০০ ছাড়ে: ১৫,৬০,০০০ ÷ ৫,০০০ = ৩১২ সেকেন্ড, অর্থাৎ ৫ মিনিট ১২ সেকেন্ড। এর মধ্যে কেউ login করলে তাঁর OTP আলাদা লেনে, আলাদা worker-এ — একটুও দেরি নয়।
  5. ফল। ২% (৩১,২০০) token provider জানাল আর বৈধ নয় — অ্যাপ মুছে ফেলা হয়েছে; সেগুলো device টেবিল থেকে বাদ, retry নয়। বাকি ১৫,২৮,৮০০ provider গ্রহণ করল। মাঝে provider 429 দিলে হার কমিয়ে আবার ধীরে বাড়ানো হতো।
  6. প্রবাসীদের পালা। ঢাকার ১১টায় UTC+3-এর ব্যবহারকারীদের সকাল ৮টা; পিছিয়ে রাখা ৪০,০০০ বার্তা আবার cap আর opt-out মিলিয়ে পাঠানো হয় — মাঝে কেউ opt-out করলে তিনি আর পাবেন না। পরের এক দিনের opt-out-এর হার একটি জরুরি metric.

৮ · Deep dive গ — preference, template আর device token

Preference, opt-out আর quiet hours

Preference-এর মডেল একটি ছক: বিষয় (OTP, লেনদেন, অর্ডার, অফার, সামাজিক) × channel. কিছু বিষয় বন্ধ করা যায় না (OTP, নিরাপত্তার সতর্কতা); বাকিগুলো ব্যবহারকারীর হাতে। Opt-out দ্রুত কার্যকর হতে হবে, আর কখন, কোন পথে (অ্যাপের সেটিং, email-এর unsubscribe link, SMS-এর keyword) এল তার audit সারি থাকবে — খুঁটিনাটি আইন দেশভেদে আলাদা, তাই নিয়ম config-এ, আইনি দলের সাথে মিলিয়ে। Email-এ বারবার ফেরত আসা ঠিকানা (hard bounce) বা “spam” বলে চিহ্নিত করা মানুষ একটি suppression list-এ যান; সেখানে আবার পাঠালে প্রেরকের সুনাম নষ্ট হয় আর ভালো বার্তাও spam-এ যায় (বড় mailbox প্রদানকারীদের প্রেরক-নিয়ম Computer Networks কোর্সের ইমেইল পর্বের অংশ ৮-এ)।

Quiet hours (ধরা যাক রাত ১০টা থেকে সকাল ৮টা) ব্যবহারকারীর নিজের time zone-এ হিসাব করতে হয়, সার্ভারের ঘড়িতে নয়। নিচের simulator একটি দিনের ৩,০০০টি ঘটনা ৩০০ জনের ওপর চালায়, নিয়মগুলো এই ক্রমে: transactional সবসময় যায়; অফারে opt-out; একই template ৬ ঘণ্টার মধ্যে দ্বিতীয়বার হলে duplicate; quiet hours-এ পিছিয়ে; দিনে ৩টির বেশি অ-জরুরি নয়।

// One day of 3,000 notification events for 300 users. Rules, checked in this order:
// transactional (otp, payment) always go; promo opt-out; same template within 6 h = duplicate;
// quiet hours 22:00-08:00 in the USER's time zone = defer to 08:00; cap 3 non-transactional per day.
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const rand = rng(2026), users = [];
for (let u = 0; u < 300; u++) { const r = rand();
  users.push({ tz: r < 0.85 ? 6 : r < 0.95 ? 3 : 0, optOutPromo: rand() < 0.15, sent: 0, last: {} }); }
const types = [["otp", 0.15], ["payment", 0.15], ["reminder", 0.3], ["promo", 0.4]];
const ev = Array.from({ length: 3000 }, () => {
  let r = rand(), k = 0; while (r > types[k][1]) r -= types[k++][1];
  const type = types[k][0], tpl = type === "promo" ? "promo-" + Math.floor(rand() * 3) : type;
  return { t: rand() * 24, u: Math.floor(rand() * 300), type, tpl };     // t = hour of day in UTC
}).sort((a, b) => a.t - b.t);
const out = {}, add = (type, why) => { const k = type + " / " + why; out[k] = (out[k] || 0) + 1; };
for (const e of ev) {
  const u = users[e.u], local = (e.t + u.tz) % 24;
  if (e.type === "otp" || e.type === "payment") { add(e.type, "sent"); continue; }
  if (e.type === "promo" && u.optOutPromo) { add(e.type, "suppressed: opted out"); continue; }
  if (u.last[e.tpl] !== undefined && e.t - u.last[e.tpl] < 6) { add(e.type, "suppressed: duplicate"); continue; }
  if (local >= 22 || local < 8) { add(e.type, "deferred: quiet hours"); continue; }
  if (u.sent >= 3) { add(e.type, "suppressed: daily cap"); continue; }
  u.sent++; u.last[e.tpl] = e.t; add(e.type, "sent");
}
let tot = { sent: 0, suppressed: 0, deferred: 0 };
for (const k of Object.keys(out).sort()) { console.log(k.padEnd(34) + String(out[k]).padStart(5));
  tot[k.split(" / ")[1].split(":")[0]] += out[k]; }
console.log("total: sent " + tot.sent + ", suppressed " + tot.suppressed + ", deferred " + tot.deferred);
const quiet = h => h >= 22 || h < 8, nonTx = ev.filter(e => e.type === "promo" || e.type === "reminder");
const wrong = nonTx.filter(e => !quiet((e.t + 6) % 24) && quiet((e.t + users[e.u].tz) % 24)).length;
console.log("if quiet hours used server time (UTC+6): " + wrong + " events would reach users abroad at night");

আউটপুট:

otp / sent                          484
payment / sent                      476
promo / deferred: quiet hours       388
promo / sent                        400
promo / suppressed: daily cap        68
promo / suppressed: duplicate       134
promo / suppressed: opted out       185
reminder / deferred: quiet hours    331
reminder / sent                     291
reminder / suppressed: daily cap     52
reminder / suppressed: duplicate    191
total: sent 1651, suppressed 630, deferred 719
if quiet hours used server time (UTC+6): 54 events would reach users abroad at night

OTP (৪৮৪) আর লেনদেনের খবর (৪৭৬) সবই গেছে — কোনো নিয়ম তাদের ছোঁয় না। অফারের ১,১৭৫টির মধ্যে মাত্র ৪০০টি গেছে: ১৮৫টি opt-out, ১৩৪টি duplicate, ৬৮টি দৈনিক সীমায় বাদ, আর ৩৮৮টি রাতের জন্য পিছিয়ে। মোট ৩,০০০-এর ১,৬৫১টি পাঠানো, ৬৩০টি বাদ, ৭১৯টি পিছিয়ে। পিছিয়ে থাকারা সকালে আবার একই নিয়মে যাচাই হবে — তাই সকালে cap-এ আরও কিছু বাদ পড়তে পারে। শেষ লাইনটি সবচেয়ে শিক্ষণীয়: quiet hours সার্ভারের ঘড়িতে (UTC+6) হিসাব করলে ৫৪টি অ-জরুরি বার্তা প্রবাসীদের কাছে তাঁদের রাতে পৌঁছাত।

Template আর localization

Template আলাদা সেবায় থাকে, কোডে নয়: প্রতিটি template-এর id, version, ভাষা (bn, en) আর channel-ভেদে রূপ — push-এ শিরোনাম ও ছোট লেখা, SMS-এ ৭০ অক্ষরের মধ্যে, email-এ HTML আর সাধারণ লেখা দুটোই। Localization শুধু অনুবাদ নয়: টাকার চিহ্ন (৳৫০০), বাংলা অঙ্ক, তারিখের রূপ আর ভাষা না পেলে কোনটিতে ফিরবে (bn → en). প্রকাশের আগে স্বয়ংক্রিয় যাচাই: সব ভ্যারিয়েবল আছে কি না, SMS কয় টুকরো, link নিরাপদ কি না। ভ্যারিয়েবলের মান (যেমন প্রেরকের নাম) ব্যবহারকারীর লেখা হতে পারে, তাই HTML-এ escape আর SMS-এ link ছাঁটা — নইলে কেউ নিজের নামে প্রতারণার link বসিয়ে আপনার প্ল্যাটফর্ম দিয়েই ছড়াবে। প্রতিটি বার্তায় template-এর version লেখা থাকে।

Device token-এর জীবনচক্র

অ্যাপ চালু বা login হলে token নিয়ে PUT /devices-এ জানায়; token বদলালে আবার। একজনের কয়েকটি ফোন, তাই কয়েকটি token. Logout-এ token মুছতে হয়, নইলে পুরোনো ফোনে নতুন মালিকের খবর যেতে পারে। Provider token-কে অবৈধ বললে (APNs-এ 410 Unregistered, FCM-এ UNREGISTERED — লেখার সময়ের documentation অনুযায়ী) সঙ্গে সঙ্গে মোছা, আর অনেকদিন দেখা না যাওয়া token নিয়মিত ছাঁটাই।

ঘটনা payment.received — ব্যবহারকারী u42
ধাপ
তথ্য
Push-এর লেখা
SMS-এর লেখা
device টেবিল
  1. ঘটনা এল। Wallet সেবা টাকা জমার transaction-এর সাথেই outbox-এ ঘটনাটি লিখেছিল; relay সেটি key evt-7f3 দিয়ে পাঠাল। Key ঘটনার নিজের id থেকে, তাই relay দুবার পাঠালেও notification একটিই।
  2. Preference. “লেনদেন” বিষয়ে u42 push আর SMS দুটোই চান, email নয়; ভাষা বাংলা। Transactional, তাই রাত হলেও যাবে, দৈনিক সীমাতেও গোনা হবে না। Preference এল cache থেকে।
  3. Template. payment.received-এর v5 বাংলা রূপে বসল: টাকার অঙ্ক বাংলা অঙ্কে আর ৳ চিহ্নে, প্রেরকের নাম escape করে। SMS-এর রূপ ৫৭ অক্ষর — এক টুকরো। লেখায় লেনদেনের পরিমাণ আছে, তাই duplicate এলেও মানুষ বুঝবেন একই লেনদেন।
  4. Fan-out. u42-এর দুটি token — পুরোনো ফোনের t-old (৭০ দিন দেখা যায়নি) আর নতুন t-new — দুটিতেই push, আর একটি নম্বরে SMS. লগে নম্বর মুখোশে (+8801•••••45), পুরোটা নয়।
  5. উত্তর। t-new গৃহীত। t-old-এ provider বলল token আর বৈধ নয় — অ্যাপটি মুছে ফেলা হয়েছিল। এটি non-retryable: retry নয়, device টেবিলে t-old চিহ্নিত হলো invalid. SMS provider গ্রহণ করল।
  6. শেষ অবস্থা। Push “sent” (t-new), SMS-এর DLR এল “delivered”, আর inbox-এ একটি সারি — push না দেখলেও অ্যাপ খুললে খবরটি থাকবে। পরের বার শুধু t-new-এ push যাবে।

৯ · ভাঙন, trade-off আর বিস্তার

কোনটি মরলে কী হয়

যা বিকলফলপ্রতিকার
একটি SMS providerOTP আটকায়Breaker + দ্বিতীয় provider; অংশ ৬-এর simulator
সব SMS providerOTP একেবারে বন্ধGraceful degradation: অ্যাপে push বা in-app দিয়ে অনুমোদন, অন্য login পথ (পর্ব ৫২-এর MFA-র বিকল্প); অফার থামানো
Dedup storeKey যাচাই অসম্ভবOTP আর লেনদেনে “চালু রাখা” (duplicate-এর ঝুঁকি মেনে), অফারে থামিয়ে রাখা — সিদ্ধান্ত আগে থেকে লেখা
Preference DBকাকে কী পাঠাবেন অজানাCache-এর কপি দিয়ে চালানো; transactional-এর জন্য নিরাপদ default, অফার থামিয়ে রাখা — opt-out না জেনে অফার নয়
একটি template-এ bugসেই বিষয়ের সব বার্তা ব্যর্থDLQ, template-এর canary আর আগের version-এ ফেরা
Receipt-এর ঢলHandler আর status store-এ চাপWebhook আগে queue-তে, তারপর সীমিত হারে প্রক্রিয়া
পুরো regionসব বন্ধদ্বিতীয় region-এ OTP লেন আগে চালু (পর্ব ৫১-এর disaster recovery)

১০ গুণ বড় হলে

দিনে ১০০ কোটি বার্তা আর ৫ কোটির campaign হলে push worker ৩০ থেকে প্রায় ৩০০, status store-এ দিনে প্রায় ৪২০ GB নতুন লেখা (এক কপিতে, অংশ ২-এর ৪০০ byte ধরে) — partition আর TTL ছাড়া চলে না, queue-র partition বাড়ে (পর্ব ৩৩-এর log-ভিত্তিক streaming). কিন্তু প্রথম দেয়াল আমাদের সার্ভার নয়, provider-এর সীমা আর বিল: বেশি account আর হার, আর কম SMS — push আগে, না খোলা হলে তবেই SMS (OTP ছাড়া), অ-জরুরি বার্তা দিনে একবার সারাংশে। ব্যবহারকারীদের আলাদা cell-এ ভাগ করাও একটি পথ (পর্ব ৪৮)।

অপব্যবহার — শুধু প্রতিরক্ষা

“OTP পাঠান” বোতাম এমন একটি দরজা, যা চাপলেই আপনার টাকা খরচ হয়। SMS pumping-এ স্বয়ংক্রিয় অনুরোধে বিপুল OTP পাঠানো হয়, বিল ওঠে আপনার নামে। প্রতিরক্ষা কয়েক স্তরে: প্রতি নম্বর, device, IP আর নম্বর-সীমায় (prefix) হারের সীমা; সন্দেহজনক অনুরোধে বাড়তি যাচাই; আর সবচেয়ে কাজের সংকেত — “OTP চাওয়া” বনাম “সফল login”-এর অনুপাত, যা হঠাৎ পড়লে সতর্কবার্তা। দৈনিক SMS বাজেট ছাড়ালে নতুন অঞ্চলে পাঠানো থামানো (পর্ব ৪৯ আর ৫২-এর প্রয়োগ)।

কী মাপবেন আর কখন জাগাবেন

প্রতিটি channel আর লেনের আলাদা SLI (পর্ব ৫০-এর observability): API-র latency; প্রতিটি queue-র সবচেয়ে পুরোনো বার্তার বয়স (শুধু queue depth নয় — ১০ লাখের queue স্বাভাবিক, ১০ মিনিট পুরোনো OTP নয়); provider-ভেদে গ্রহণ আর delivered-এর হার; OTP-র সফল যাচাইয়ের অনুপাত; campaign-এর পরে opt-out; আর SMS-এর খরচ। SLO ধরা যাক: “৩০ দিনে ৯৯% OTP ৫ সেকেন্ডের মধ্যে provider-এ গৃহীত”; alert burn rate দিয়ে। Log-এ ফোন নম্বর আর কোড নয় — PII মুখোশে।

ইন্টারভিউর ৬০ সেকেন্ডের সারাংশ

“সেবারা outbox থেকে একটি asynchronous API ডাকে, সাথে idempotency key; API টেকসই queue-তে লিখে 202 দেয়। Router preference, opt-out, ব্যবহারকারীর time zone-এ quiet hours আর cap দেখে template বসায়, তারপর channel ও অগ্রাধিকারের আলাদা queue-তে পাঠায় — OTP-র worker pool আলাদা, campaign তাকে আটকাতে পারে না। Worker provider-এর সীমা মেনে পাঠায়: ক্ষণিকের ত্রুটিতে backoff-সহ retry, স্থায়ী ত্রুটিতে থামা, মেয়াদে expire, শেষে DLQ. SMS-এ দুটি provider আর breaker; retry-তে একই OTP কোড, তাই duplicate নিরীহ। Receipt-এ অবস্থা কেবল সামনে এগোয়। মাপি লেনভেদে সবচেয়ে পুরোনো বার্তার বয়স, delivered-এর হার, OTP-র সফল যাচাই আর SMS-এর খরচ।”

আরও গভীরে: ব্রাউজারের push-এর জন্য IETF-এর Web Push মান (RFC 8030, ২০১৬) আর সার্ভারের পরিচয়ের VAPID (RFC 8292, ২০১৭) পড়লে বোঝা যায় platform push service-এর নকশা কেমন — বার্তার TTL, urgency আর একটি বার্তাকে আরেকটি দিয়ে বদলানোর “topic”. Email-এ এক চাপে unsubscribe-এর মান RFC 8058 (২০১৭)। SMS aggregator-দের অনেকে এখনো SMPP (সংস্করণ ৩.৪ বহুল প্রচলিত) protocol-এ যুক্ত থাকে, যেখানে delivery receipt একটি আলাদা বার্তা হয়ে ফেরে। আর LinkedIn-এর প্রকাশিত engineering blog (২০১৭ সালের দিকে, “Air Traffic Controller” নামে) একজন সদস্যকে কতগুলো আর কোন বার্তা পাঠানো হবে তা কেন্দ্রীয়ভাবে ঠিক করার অভিজ্ঞতা বর্ণনা করে — প্রকাশিত বর্ণনা অনুযায়ী, খুঁটিনাটি সেখানে দেখুন।

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই case-এ দুর্বল আর শক্ত উত্তরের পার্থক্য):

  • দুর্বল: “Kafka-তে দিই, consumer FCM আর SMS API ডাকে।” — শক্ত: লেনভেদে আলাদা queue আর worker pool, কারণ সংখ্যায় দেখানো (১০ লাখ প্রচারের পেছনে OTP).
  • দুর্বল: “retry তিনবার।” — শক্ত: কোন ত্রুটিতে retry, backoff-jitter, TTL-এ থামা, DLQ, আর provider বদলালে duplicate-এর দাম।
  • দুর্বল: “exactly-once পাঠাব।” — শক্ত: at-least-once + idempotency key, OTP-তে একই কোড, অফারে at-most-once-এর কাছাকাছি।
  • দুর্বল: সময়ের কথা নেই। — শক্ত: ব্যবহারকারীর time zone-এ quiet hours, frequency cap, opt-out-এর audit.
  • দুর্বল: খরচ বাদ। — শক্ত: বাংলা SMS-এর ৭০ অক্ষরের টুকরো, push আগে, বাজেট আর pumping-এর সতর্কতা।

১০ · AI-এর যুগে — notification-এর নকশা যাচাই

AI-কে একটি notification system-এর নকশা চাইলে প্রায়ই একটি সুন্দর ছবি আসে: Kafka, Redis, কয়েকটি microservice, FCM আর একটি SMS API. দেখতে পূর্ণ, কিন্তু এই case-এর কঠিন অংশগুলো — অগ্রাধিকারের বিচ্ছিন্নতা, provider বদলের duplicate, time zone আর SMS-এর খরচ — প্রায়ই অনুপস্থিত। সাধারণ ভুলগুলো:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
সব channel আর বিষয়ের জন্য একটি topic, একটি consumer groupCampaign-এর পেছনে OTP আটকায়অংশ ৭-এর simulator নিজের সংখ্যায় চালান; “OTP-র p99 campaign চলাকালীন কত?” জিজ্ঞেস করুন
“Exactly-once delivery নিশ্চিত”বাইরের provider-এ অসম্ভবঅস্পষ্ট timeout-এর পরে কী হয়, লিখিত উত্তর চান
Provider-এর হার, payload-এর সীমা বা দাম বানিয়ে বলাএগুলো চুক্তি আর সংস্করণভেদে বদলায়অফিসিয়াল documentation আর চুক্তি দেখুন; উৎস ছাড়া সংখ্যা বাদ
SMS-এর খরচের হিসাবে বাংলার ৭০ অক্ষর ভুলে যাওয়া, বা গুণে ভুলবিল দ্বিগুণ হতে পারেঅংশ ২-এর estimator-এ নিজের ইনপুট দিয়ে অঙ্ক আবার করুন
Quiet hours সার্ভারের ঘড়িতে; token মোছার কথা নেইরাতে বিরক্তি; মৃত token-এ অপচয়প্রবাসী ব্যবহারকারী আর অ্যাপ মোছা ফোন ধরে failure-mode পর্যালোচনা
ছোট দলের জন্য দশটি microserviceOver-engineeringআগে একটি সেবা + আলাদা queue আর worker pool; ভাগ পরে, প্রয়োজনে

AI-কে সংখ্যা, শর্ত আর ব্যর্থতার প্রশ্নে বেঁধে দিন:

একটি notification প্ল্যাটফর্মের নকশা পর্যালোচনা করো। শর্ত: দিনে ১ কোটি সক্রিয় ব্যবহারকারী; push, SMS, email, in-app; OTP-র p99 ৫ সেকেন্ডের মধ্যে provider-এর কাছে পৌঁছাতে হবে, এমনকি ১০ লাখ প্রচারের SMS চলাকালীনও; দুটি SMS provider; ব্যবহারকারীর কিছু অংশ প্রবাসে। ১. OTP আর campaign কীভাবে আলাদা রাখবে — queue, worker pool আর provider-এর স্তরে আলাদা করে বলো। ২. Provider A-তে timeout হলে B-তে পাঠালে duplicate-এর ঝুঁকি কোথায়, আর OTP, লেনদেনের খবর ও অফারে আলাদা কী নীতি নেবে? ৩. প্রতিটি সংখ্যার (হার, সীমা, দাম) পাশে লেখো সেটি অনুমান না documentation থেকে; documentation হলে কোন পাতা। ৪. Quiet hours, frequency cap, opt-out আর মৃত device token কীভাবে সামলাবে? ৫. পাঁচটি failure mode আর প্রতিটির জন্য কোন metric-এ alert দেবে, তালিকা দাও। যা জানো না, সেখানে “জানি না” লেখো।

AI-এর নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

ইন্টারভিউয়ার মূল নকশার পরে যেসব প্রশ্নে চাপ দিতে পারেন। নিজে ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১“প্রতিটি OTP ঠিক একবার পৌঁছাবে” — এই প্রতিশ্রুতি দিতে পারবেন? না পারলে কী প্রতিশ্রুতি দেবেন?

না, আর কারণটি প্রকৌশলের নয়, যুক্তির। আমাদের worker provider-কে ডাকল, ৩ সেকেন্ড উত্তর নেই। দুটি সম্ভাবনা: অনুরোধ পৌঁছায়নি, অথবা পৌঁছে SMS চলে গেছে কিন্তু উত্তর হারিয়েছে। Worker-এর হাতে থাকা তথ্যে এ দুটি আলাদা করার উপায় নেই। এটিই distributed system-এর পুরোনো two generals problem-এর রূপ — অনির্ভরযোগ্য পথে কোনো সংখ্যক প্রাপ্তিস্বীকারই দুই পক্ষকে নিশ্চিত করতে পারে না (সমস্যাটি ১৯৭০-এর দশকের গবেষণায় এসেছে আর Jim Gray-এর লেখায় এই নামে পরিচিত হয়)।

তাহলে বেছে নিতে হয়: আবার পাঠালে কখনো দুটি SMS (at-least-once), না পাঠালে কখনো একটিও না (at-most-once). OTP-তে না পৌঁছানো অনেক খারাপ — মানুষ login করতে পারেন না — তাই at-least-once. এরপর duplicate-কে নিরীহ করার কৌশল: retry-তে নতুন কোড নয়, একই কোড। একই provider-এ ফেরত গেলে আমাদের notification_id পাঠান — provider যদি তা দিয়ে duplicate আটকায় (চুক্তিতে যাচাই করুন), ভালো। ভেতরের দিকে, API-তে idempotency key আর status store-এ notification_id দিয়ে আমাদের নিজেদের সৃষ্ট duplicate (producer-এর retry, queue-র redelivery) পুরোপুরি বন্ধ করা যায় — অংশ ৬-এর simulator-এ ১০১টি এমন অনুরোধ বাদ পড়েছিল।

অতএব সৎ প্রতিশ্রুতি হলো: “একটি গৃহীত অনুরোধ নীরবে হারাবে না; প্ল্যাটফর্মের ভেতরে duplicate তৈরি হবে না; বাইরের provider-এর অস্পষ্ট ফলে কখনো দ্বিতীয় SMS যেতে পারে, যা একই কোড বহন করে; আর মেয়াদ পেরোলে চেষ্টা থামবে।” সাথে metric: duplicate SMS-এর হার আর provider-ভেদে অস্পষ্ট timeout. ইন্টারভিউতে “exactly-once delivery” না বলে “effectively-once processing, at-least-once delivery” বলাটাই পরিপক্বতার চিহ্ন। আর অফারের মতো বিষয়ে, যেখানে duplicate বেশি ক্ষতিকর, অস্পষ্ট timeout-এর পরে অন্য provider-এ না পাঠিয়ে চুপ থাকাও একটি সৎ ও বৈধ সিদ্ধান্ত।

মূল উপলব্ধি: নিয়ন্ত্রণের বাইরের side effect-এ নিশ্চয়তা কেনা যায় না; কেনা যায় কোন ভুলটি ঘটবে তার পছন্দ, আর সেই ভুলকে নিরীহ করার নকশা।

প্র ০২OTP-র জন্য SMS কেন? SMS-এর OTP কতটা নিরাপদ, আর বিকল্প থাকলে প্ল্যাটফর্ম কী দেবে?

SMS-এর সবচেয়ে বড় গুণ পৌঁছানোর পরিধি। প্রায় প্রতিটি মোবাইল ফোন, এমনকি সাধারণ feature phone-ও, ইন্টারনেট ছাড়াই SMS পায়; অ্যাপ install করার দরকার নেই। SMS নিজে GSM-এর যুগে এসেছিল — প্রথম SMS পাঠানোর কথা সাধারণত ১৯৯২ সালের ডিসেম্বর বলে উল্লেখ করা হয় — আর তিন দশক পরেও ব্যাংক আর mobile financial service-এ লেনদেনের খবর ও OTP-র প্রধান পথগুলোর একটি (বাংলাদেশের জন্য কোনো নির্দিষ্ট হার দাবি করছি না)।

কিন্তু নিরাপত্তার দিকে SMS দুর্বল। SIM বদল (প্রতারক কারও নামে নতুন SIM তুলে নেন), ফোনে ক্ষতিকর অ্যাপ, অপারেটরের নেটওয়ার্কের পুরোনো signaling-এর দুর্বলতা আর সবচেয়ে বেশি — প্রতারণার ফোনে মানুষকে কোডটি বলিয়ে নেওয়া। এজন্য যুক্তরাষ্ট্রের NIST তাদের digital identity নির্দেশিকায় (SP 800-63B, ২০১৭) SMS-এর মতো পথকে “restricted” authenticator বলে চিহ্নিত করে — অর্থাৎ ব্যবহার চলে, কিন্তু ঝুঁকি জানিয়ে আর বিকল্প রেখে। পর্ব ৫২-এর নিরাপত্তা নকশায় TOTP অ্যাপ আর passkey শক্ত বিকল্প হিসেবে এসেছে।

প্ল্যাটফর্মের ভূমিকা তাই দুটি। এক, SMS-এর OTP যত নিরাপদ করা যায়: লেখায় “কাউকে জানাবেন না” আর কোন কাজের কোড তা স্পষ্ট, ছোট মেয়াদ, প্রতি নম্বরে সীমা, log-এ কোড নয়। দুই, একাধিক পথ দেওয়া, যাতে auth সেবা বেছে নিতে পারে: অ্যাপ ফোনে login থাকলে push-এ “আপনি কি login করছেন? হ্যাঁ/না”, না থাকলে SMS, আর সেটিও ব্যর্থ হলে অন্য পথ। কোনটি কখন, সেই সিদ্ধান্ত auth সেবার; প্ল্যাটফর্ম শুধু নির্ভরযোগ্য বাহন। খরচের দিকটিও ভুলবেন না: প্রতিটি OTP-র SMS টাকা খরচ করে, আর অ্যাপের push প্রায় বিনামূল্যে — তাই যেখানে নিরাপদ, সেখানে push আগে আর SMS পরের বিকল্প।

মূল উপলব্ধি: SMS OTP পৌঁছানোয় চমৎকার, নিরাপত্তায় মাঝারি; নকশায় দুটোই স্বীকার করুন আর শক্ত বিকল্পের দরজা খোলা রাখুন।

প্র ০৩Push-এর provider “গৃহীত” বলল। ব্যবহারকারী কি সত্যিই দেখলেন? “delivered”-এর হার আপনি কীভাবে মাপবেন?

Provider-এর “গৃহীত” মানে শুধু বার্তাটি তার হাতে পৌঁছেছে। এরপর ফোন বন্ধ থাকতে পারে, ইন্টারনেট না থাকতে পারে, ব্যবহারকারী notification বন্ধ করে রাখতে পারেন, আর Android-এ ব্যাটারি বাঁচানোর নিয়ম বার্তা পিছিয়ে দিতে পারে। Android ৬.০ (২০১৫)-তে আসা Doze মোড ফোন অলস থাকলে নেটওয়ার্কের কাজ জমিয়ে রাখে; FCM-এর “high priority” বার্তা সেটি ভাঙতে পারে, তবে অপব্যবহার করলে প্ল্যাটফর্ম নিজেই অগ্রাধিকার কমিয়ে দিতে পারে (লেখার সময়ের documentation অনুযায়ী; নিয়ম বদলায়)। কিছু ফোন-নির্মাতা নিজস্বভাবে background-এর অ্যাপ আরও কড়াভাবে বন্ধ করেন — এটি এত পরিচিত সমস্যা যে স্বেচ্ছাসেবীরা নির্মাতাভেদে আচরণের তালিকা রাখেন।

তাই “delivered” মাপতে অ্যাপকেও অংশ নিতে হয়। Push-এর ভেতরে notification_id থাকে; অ্যাপ বার্তা পেয়ে বা দেখিয়ে একটি ছোট “received” ঘটনা পাঠায়, আর মানুষ চাপলে “opened”. এগুলো একসাথে ব্যাচে, ইন্টারনেট ফিরলে পাঠানো যায়। তখন funnel দাঁড়ায়: অনুরোধ → provider-এর গ্রহণ → অ্যাপে পৌঁছানো → খোলা। প্রতিটি ধাপের হার platform, অ্যাপের version আর ফোন-নির্মাতা ধরে ভাগ করলে সমস্যা চোখে পড়ে — যেমন একটি নতুন version-এ “received” হঠাৎ নেমে যাওয়া মানে token নিবন্ধনে bug.

SLO লেখার সময় সৎ থাকুন: আমাদের নিয়ন্ত্রণে থাকা অংশের (provider-এর গ্রহণ পর্যন্ত) জন্য কড়া লক্ষ্য, আর ফোনে পৌঁছানোর জন্য পর্যবেক্ষণ ও প্রবণতা, প্রতিশ্রুতি নয়। জরুরি তথ্য (লেনদেন) কখনো শুধু push-এর ভরসায় রাখবেন না — in-app inbox-এ সবসময় একটি কপি, আর প্রয়োজনে SMS. আর মৃত token-এর ব্যর্থতা হারের হিসাবে আলাদা রাখুন, নইলে পুরোনো install-এর জমা আবর্জনা delivered-এর হারকে কৃত্রিমভাবে নামিয়ে দেবে।

মূল উপলব্ধি: প্রতিটি “delivered” শব্দের পাশে জিজ্ঞেস করুন “কার কাছে delivered?” — উত্তরটি metric-এর নামেই লিখে রাখুন।

প্র ০৪৫০ লাখ জনকে এক মিনিটে push পাঠাতে পারলে সেটি কি সাফল্য? Notification প্ল্যাটফর্মের বাইরে কী ভাঙতে পারে?

প্ল্যাটফর্মের দিক থেকে সাফল্য, পুরো সিস্টেমের দিক থেকে প্রায়ই বিপদ। Push পৌঁছালে মানুষ চাপেন, আর প্রত্যেকে অ্যাপ খুলে home screen, অফারের পাতা আর ছবি চান। ৫০ লাখের ৫%-ও যদি প্রথম দুই মিনিটে খোলেন, তা ২,৫০,০০০ জন — সেকেন্ডে দুই হাজারের বেশি নতুন session, প্রতিটিতে কয়েকটি API call. সাধারণ দিনের হিসাবে বানানো backend তখন thundering herd-এর মুখে, ঠিক যেমন পর্ব ৬-এ ফল প্রকাশের মুহূর্তে দেখেছেন। নিজেরাই নিজেদের ওপর একটি ঢেউ পাঠালাম।

প্রতিকার কয়েকটি। প্রথমত, campaign-এর হার শুধু provider দেখে নয়, backend-এর ক্ষমতা দেখেও ঠিক করা: ৫,০০০/s-এ ৫০ লাখ পাঠাতে প্রায় ১৭ মিনিট, ঢেউটিও সেই সময়ে ছড়ায়। দ্বিতীয়ত, অফারের পাতা আর ছবি আগে থেকে CDN-এ রাখা (পর্ব ১৭-এর CDN) আর API-র উত্তর cache করা, যাতে খোলার ঢেউ origin পর্যন্ত না যায়। তৃতীয়ত, autoscaling আগে থেকে বাড়িয়ে রাখা — scheduled scaling — কারণ campaign-এর সময় আমরা জানি। চতুর্থত, backend চাপের সংকেত দিলে campaign নিজে থেমে যাওয়া বা ধীর হওয়া — পর্ব ১৮-এর backpressure, এবার দুই সেবার মধ্যে।

একই চিন্তা অন্য দিকেও: receipt-এর ঢল, email-এর bounce-এর ঢল বা “unsubscribe” পাতার ভিড়। প্রতিটি বড় পাঠানোর একটি প্রতিধ্বনি আছে। ভালো নকশাকারী campaign-কে একটি load test-এর মতো দেখেন: আগে ছোট একটি অংশে (১%) পাঠিয়ে মাপেন, তারপর বাকিটা। তাই campaign-এর ক্যালেন্ডার শুধু marketing-এর নয়, on-call দলেরও জানা থাকা উচিত — বড় পাঠানোর সময় কেউ dashboard-এর সামনে থাকবেন।

মূল উপলব্ধি: একটি সেবার throughput বাড়ালে চাপ অদৃশ্য হয় না, পরের সেবায় সরে যায়; হার নির্ধারণ পুরো শিকল দেখে করুন।

প্র ০৫একটি দলের ভুলে লাখো মানুষের কাছে ভুল বার্তা চলে গেলে? বহু দলের প্ল্যাটফর্মে মানুষের ভুল কীভাবে ঠেকাবেন?

সবচেয়ে বিখ্যাত উদাহরণটি একটি সরকারি সতর্কবার্তা ব্যবস্থার। ২০১৮ সালের ১৩ জানুয়ারি যুক্তরাষ্ট্রের Hawaii-তে জরুরি ব্যবস্থাপনা সংস্থা থেকে ভুলবশত “ক্ষেপণাস্ত্র আসছে, আশ্রয় নিন” সতর্কবার্তা লাখো ফোনে চলে যায়; সংশোধনী বার্তা পাঠাতে প্রায় ৩৮ মিনিট লেগেছিল বলে ব্যাপকভাবে প্রতিবেদিত। পরের তদন্তে উঠে আসে বিভ্রান্তিকর মেনু, পরীক্ষা আর আসল বার্তার কাছাকাছি বিকল্প, আর আগে থেকে তৈরি “ভুল ছিল” বার্তার অভাব। একটি বাণিজ্যিক প্ল্যাটফর্মে ক্ষতি কম হলেও ধরন একই: ভুল তালিকায় অফার, পরীক্ষার template আসল মানুষের কাছে, বা bug-এর loop-এ একজনকে শতবার।

প্রতিরক্ষা কয়েক স্তরে। মানুষের স্তরে: বড় campaign-এ দ্বিতীয় একজনের অনুমোদন, অনুমোদনের পাতায় “কতজন, কোন template, প্রথম ৫ জনের আসল preview”; পরীক্ষার পরিবেশ আসল ঠিকানায় পাঠাতেই পারে না। প্ল্যাটফর্মের স্তরে: প্রতিটি দলের quota; প্রতিটি ব্যবহারকারীর দৈনিক cap, যা loop-এর bug-কেও থামায়; একটি জরুরি থামানোর সুইচ (feature flag-এর মতো) যা যেকোনো campaign বা পুরো promotional লেন সঙ্গে সঙ্গে বন্ধ করে; আর canary — প্রথমে ১% পাঠিয়ে opt-out ও অভিযোগের হার দেখা (পর্ব ৫৩-এর canary-র চিন্তা)। পরের স্তরে: আগে থেকে অনুমোদিত সংশোধনী template আর লক্ষ্য-তালিকা ধরে রাখা, যাতে ভুল হলে একই মানুষদের কয়েক মিনিটে “দুঃখিত, বার্তাটি ভুল ছিল” পাঠানো যায়।

এর সাথে audit log: কে, কখন, কোন তালিকায়, কোন template পাঠালেন — ঘটনার পরে দোষ খোঁজার জন্য নয়, প্রক্রিয়া শোধরানোর জন্য (blameless postmortem, পর্ব ৫০)।

মূল উপলব্ধি: যে ব্যবস্থা এক চাপে লাখো মানুষের কাছে পৌঁছায়, তার নকশায় “ভুল চাপ” একটি প্রথম শ্রেণির ব্যর্থতা — থামানো আর সংশোধনের পথ আগে থেকে বানান।

অনুশীলন

হিসাব, নকশা বদল, ত্রুটি খোঁজা আর ফল অনুমান — চারটিই নিজে করে তারপর উত্তর মেলান।

  1. SMS-এর হিসাব: ধরা যাক দিনে ৫০ লাখ সক্রিয় ব্যবহারকারী, প্রত্যেকে দিনে গড়ে ০.২টি SMS. ৯০% বার্তা এক টুকরোর, ১০% বাংলায় ১২০ অক্ষরের সতর্কবার্তা। প্রতি টুকরো ০.৩০ টাকা (কাল্পনিক)। (ক) দিনে কত টুকরো, কত টাকা, ৩০ দিনে কত? (খ) একটি sale-এর দিনে দিনের ৩০% SMS এক ঘণ্টায় এলে peak কত/s? দুটি provider প্রতিটি ৫০/s নিলে কী বলবেন? (গ) সতর্কবার্তাগুলো ইংরেজিতে লিখলে কত বাঁচে?

    (ক) SMS = ৫০,০০,০০০ × ০.২ = ১০,০০,০০০/দিন। ১২০ অক্ষরের বাংলা লেখা ৭০-এর বেশি, তাই ৬৭ অক্ষরের টুকরোয় ⌈১২০ ÷ ৬৭⌉ = ২ টুকরো। টুকরো = ৯,০০,০০০ × ১ + ১,০০,০০০ × ২ = ১১,০০,০০০। খরচ = ১১,০০,০০০ × ০.৩০ = ৩,৩০,০০০ টাকা/দিন; ৩০ দিনে ৯৯,০০,০০০ টাকা।

    (খ) ১০,০০,০০০-এর ৩০% = ৩,০০,০০০ এক ঘণ্টায় → ৩,০০,০০০ ÷ ৩,৬০০ ≈ ৮৩.৩/s. দুই provider মিলে ১০০/s, অর্থাৎ utilization প্রায় ৮৩% — খুব কাছাকাছি; একটি provider বিকল হলে বাকি ৫০/s-এ queue দ্রুত বাড়বে আর OTP দেরি হবে (পর্ব ৪-এর utilization আর latency). তাছাড়া অনেক provider টুকরো ধরে হার গোনে (চুক্তিতে দেখুন), তাতে চাপ আরও বেশি। সমাধান: বেশি হারের চুক্তি বা তৃতীয় provider, আর sale-এর ঘণ্টায় promotional SMS আলাদা পথে বা পিছিয়ে।

    (গ) ১২০ অক্ষরের ইংরেজি (GSM-7) এক টুকরো, তাই সতর্কবার্তায় ১,০০,০০০ টুকরো কমে: দিনে ৩০,০০০ টাকা, ৩০ দিনে ৯,০০,০০০ টাকা। কিন্তু সব ব্যবহারকারী ইংরেজি স্বচ্ছন্দে পড়েন না; ভালো সমাধান প্রায়ই বাংলা লেখা ছেঁটে ৭০ অক্ষরের মধ্যে আনা — তাতে ভাষাও থাকে, টাকাও বাঁচে।

  2. Requirement বদল: এখন ব্যবহারকারীরা চান, অ-জরুরি বার্তা (সামাজিক, অফার, অর্ডারের ছোট আপডেট) আলাদা করে না এসে দিনে একবার, তাঁদের বেছে নেওয়া সময়ে, একটি সংক্ষিপ্ত সারাংশে (digest) আসুক। নকশায় কী কী বদলাবেন?
    • Preference: প্রতিটি বিষয়ের জন্য “সঙ্গে সঙ্গে” বা “digest”, আর digest-এর স্থানীয় সময় (যেমন রাত ৮টা) ও time zone.
    • Router-এ নতুন পথ: digest-এর বার্তা channel-এর queue-তে না গিয়ে একটি digest store-এ জমে — partition = user_id, ভেতরে সময় ধরে; একই বিষয়ের ঘটনা জমিয়ে গোনা (“৫ জন আপনার পোস্টে মন্তব্য করেছেন”)।
    • Scheduler: প্রতিটি time zone আর সময়ের জন্য delayed job (পর্ব ৩৭); নির্দিষ্ট সময়ে ব্যবহারকারীদের ভাগে ভাগে digest তৈরি, তারপর সাধারণ push-low বা email লেনে — হার সীমিত, যাতে রাত ৮টায় একসাথে লাখো বার্তা না যায়।
    • Idempotency: digest-এর key = user + তারিখ, যাতে job দুবার চললেও একটি digest; পাঠানোর পরে জমা ঘটনাগুলো “ব্যবহৃত” চিহ্নিত।
    • Template: তালিকা-ধরনের নতুন template, আর SMS-এ digest নয় (খরচ); email বা push-এ।
    • প্রান্তের বিষয়: digest-এর আগে কেউ অ্যাপ খুলে সব দেখে ফেললে digest বাদ; মেয়াদ পেরোনো অফার digest থেকে বাদ; জরুরি বিষয় কখনো digest-এ নয়।
  3. ত্রুটিপূর্ণ নকশা খুঁজুন: একটি দলের প্রস্তাব — “Payment সেবা টাকা জমার DB transaction-এর ভেতরেই SMS provider-এর API ডাকবে; সব বার্তা একটি queue-তে; ব্যর্থ হলে সঙ্গে সঙ্গে ১০ বার retry; অবস্থা শুধু log-এ; quiet hours সার্ভারের ঘড়িতে; device token কখনো মোছা হয় না।” অন্তত পাঁচটি সমস্যা আর প্রতিকার বলুন।
    1. Transaction-এর ভেতরে বাইরের call: provider ধীর হলে DB lock আর connection আটকে থাকে, payment-ই ধীর; আর SMS যাওয়ার পরে transaction rollback হলে “টাকা এসেছে” অথচ আসেনি। প্রতিকার: outbox (পর্ব ২৫) আর asynchronous API.
    2. একটি queue: campaign-এর পেছনে OTP আর লেনদেনের খবর আটকায় (অংশ ৭-এর FIFO: p50 প্রায় ৪১০ সেকেন্ড)। প্রতিকার: লেনভেদে queue আর worker pool.
    3. সঙ্গে সঙ্গে ১০ বার retry: backoff আর jitter নেই — অসুস্থ provider-কে retry storm দিয়ে আরও বসিয়ে দেওয়া; স্থায়ী ত্রুটিতেও ১০ বার খরচ; TTL নেই। প্রতিকার: ত্রুটির শ্রেণিভাগ, backoff-jitter, retry budget, TTL, DLQ, failover আর breaker.
    4. অবস্থা শুধু log-এ: receipt মেলানো, “আমার OTP আসেনি” অভিযোগের উত্তর, delivered-এর হার বা বিলের reconciliation — কিছুই সম্ভব নয়। প্রতিকার: status store আর state machine.
    5. Quiet hours সার্ভারের ঘড়িতে: প্রবাসীরা রাতে বার্তা পান (অংশ ৮-এর simulator-এ ৫৪টি)। প্রতিকার: ব্যবহারকারীর time zone.
    6. Token কখনো মোছা হয় না: মৃত token-এ অপচয়, delivery-র হিসাব বিকৃত, আর logout-এর পরে পুরোনো ফোনে খবর যাওয়ার গোপনীয়তা-ঝুঁকি। প্রতিকার: unregistered-এ মোছা, logout-এ মোছা, পুরোনো token ছাঁটাই।
  4. ফল অনুমান করুন: অংশ ৭-এর priority simulator-এ bulkhead-এর ভাগ 100 + 900 থেকে বদলে 20 + 980 করলে (OTP আসে সেকেন্ডে গড়ে প্রায় ৫টি) OTP-র p50, p99 আর প্রচার শেষের সময় কেমন হবে? আর 6 + 994 বা 5 + 995 করলে? আগে অনুমান করুন, তারপর simulator-এর শেষের আগের লাইনে 100 আর 900 বদলে চালান।

    চালালে পাওয়া যায়:

    bulkhead 20 + 980         0.050     0.137       0     1020
    bulkhead 6 + 994          0.424     2.843       0     1006
    bulkhead 5 + 995          7.930    17.637       0     1005

    ২০/s: প্রতিটি OTP-র সেবার সময় ১/২০ = ৫০ ms, তাই p50 ঠিক ০.০৫০ সেকেন্ড; utilization প্রায় ৫ ÷ ২০ = ২৫%, মাঝে মাঝে কয়েকটি একসাথে এলে সামান্য অপেক্ষা, তাই p99 ০.১৩৭ সেকেন্ড। প্রচার একটু দ্রুত শেষ: ১০,০০,০০০ ÷ ৯৮০ ≈ ১,০২০ সেকেন্ড। ৬/s: utilization প্রায় ৮৩% — p99 ২.৮৪ সেকেন্ডে লাফ। ৫/s: আগমনের হার (৫,৯৭৫ ÷ ১,২০০ ≈ ৪.৯৮/s) প্রায় ক্ষমতার সমান, utilization প্রায় ১০০% — p50 ৭.৯ আর p99 ১৭.৬ সেকেন্ড। ক্ষমতা মাত্র প্রায় ১৭% কমালে (৬ থেকে ৫) p99 ৬ গুণের বেশি বাড়ল: utilization ১-এর কাছে গেলে latency বিস্ফোরিত হয় (পর্ব ৪-এর queueing). শিক্ষা: সংরক্ষিত লেনকে গড় আগমনের ঠিক সমান নয়, কয়েক গুণ বেশি দিন — আর OTP-র নিজের peak (sale-এর ঘণ্টা) মাথায় রাখুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি লেনদেনের SMS-এর পথ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Payment সেবা transaction-এর সাথেই ঘটনাটি outbox-এ লেখে
  2. API সেবার পরিচয় আর idempotency key যাচাই করে টেকসই queue-তে লেখে
  3. Router preference দেখে template বসায়
  4. বার্তা channel আর অগ্রাধিকারের নিজস্ব queue-তে যায়
  5. Worker সীমা মেনে provider-কে পাঠায়, প্রয়োজনে retry বা failover
  6. Provider-এর delivery receipt status store-এ অবস্থা হালনাগাদ করে

একটি SMS queue-তে আগে থেকে ১০ লাখ প্রচারের বার্তা জমা, আর provider-রা মিলে সেকেন্ডে ১,০০০ SMS নেয়। ঠিক তখন একটি OTP একই FIFO queue-র শেষে ঢুকলে মোটামুটি কতক্ষণ অপেক্ষা করবে?

  • প্রায় ১ সেকেন্ড
  • প্রায় ১০০ সেকেন্ড
  • প্রায় ১,০০০ সেকেন্ড (১৭ মিনিটের মতো)
  • প্রায় শূন্য, কারণ OTP ছোট বার্তা

Provider A-কে একটি OTP পাঠানোর পরে ৫ সেকেন্ডে উত্তর আসেনি, তাই worker সেটি provider B-তে পাঠাল। এই নকশার প্রধান ঝুঁকি কী, আর তা নিরীহ রাখার সবচেয়ে ভালো উপায়?

  • OTP একেবারে হারাবে; তাই B-তে পাঠানো উচিত নয়
  • ব্যবহারকারী দুটি SMS পেতে পারেন; retry-তে একই কোড পাঠালে সেটি নিরীহ
  • কোনো ঝুঁকি নেই, কারণ API-তে idempotency key আছে
  • ঝুঁকি duplicate; তাই B-তে নতুন কোড পাঠানো উচিত

একটি বাংলা SMS-এর দৈর্ঘ্য ৭৫ অক্ষর। Unicode (UCS-2) SMS-এ এক টুকরোয় ৭০ অক্ষর, আর জোড়া দেওয়া লম্বা বার্তায় প্রতি টুকরোয় ৬৭ অক্ষর। বার্তাটি কয় টুকরোয় যাবে?

  • ১ টুকরো
  • ২ টুকরো
  • ৩ টুকরো
  • ১ টুকরো, কারণ SMS-এ ১৬০ অক্ষর ধরে

একটি প্ল্যাটফর্মের সার্ভার ঢাকায় (UTC+6), কিছু ব্যবহারকারী মধ্যপ্রাচ্যে (UTC+3). রাত ১০টা থেকে সকাল ৮টা quiet hours. ঢাকার সকাল ১০টায় একটি অফার পাঠানোর সময় quiet hours কোন ঘড়িতে যাচাই করা উচিত?

  • সার্ভারের ঘড়িতে, কারণ সব হিসাব এক জায়গায় সহজ
  • UTC-তে, কারণ সেটি সবার জন্য এক
  • Campaign-এর মালিক দলের ঘড়িতে
  • প্রতিটি ব্যবহারকারীর নিজের time zone-এ

একটি push পাঠানোর উত্তরে platform push service জানাল device token-টি আর নিবন্ধিত নয় (যেমন অ্যাপ মুছে ফেলা হয়েছে)। Worker-এর সঠিক কাজ কোনটি?

  • Retry না করে token-টি invalid চিহ্নিত করা বা মুছে ফেলা
  • Exponential backoff দিয়ে আবার চেষ্টা করা
  • বার্তাটি DLQ-তে রেখে পরে redrive করা
  • ব্যবহারকারীর সব notification বন্ধ করা

Payment সেবা চায়, টাকা জমার DB transaction commit হলেই নিশ্চিতভাবে “টাকা এসেছে” notification যাবে, আর rollback হলে যাবে না। কোন কৌশলটি সবচেয়ে উপযুক্ত?

  • Transaction-এর ভেতরেই SMS provider-এর API ডাকা
  • Commit-এর পরে আলাদাভাবে notification API ডাকা
  • একই transaction-এ outbox টেবিলে ঘটনা লেখা, তারপর relay দিয়ে পাঠানো
  • রাতে একবার সব লেনদেন মিলিয়ে notification পাঠানো

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:

মনে রাখুন

  1. একটি asynchronous API: producer-এর outbox থেকে, প্রতিটি অনুরোধে idempotency key, টেকসই queue-তে লেখার পরে 202; পাঠানো, retry আর receipt প্ল্যাটফর্মের দায়।
  2. লেনভেদে বিচ্ছিন্নতা: channel আর অগ্রাধিকার অনুযায়ী আলাদা queue আর worker pool; এক FIFO-তে ১০ লাখ প্রচারের পেছনে OTP-র p50 প্রায় ৪১০ সেকেন্ড, bulkhead-এ ১০ ms.
  3. নির্ভরযোগ্যতা: ত্রুটি চিনে retry, backoff-jitter, TTL-এ থামা, DLQ; দুটি provider আর breaker; বাইরের provider-এ exactly-once নেই, তাই OTP-তে একই কোড আর বিষয়ভেদে duplicate-নীতি।
  4. মানুষের নিয়ম: preference, opt-out-এর audit, suppression list, frequency cap আর ব্যবহারকারীর time zone-এ quiet hours; transactional এসবের বাইরে; মৃত token সঙ্গে সঙ্গে মোছা।
  5. খরচ, অপব্যবহার, পরিমাপ: বাংলা SMS-এ ৭০ অক্ষরের টুকরো, push আগে, প্রতি নম্বরে OTP-সীমা আর বাজেট; প্রতিটি লেনের সবচেয়ে পুরোনো বার্তার বয়স, provider-ভেদে delivered-এর হার আর OTP-র সফল যাচাই মাপুন।

পরের পর্বে: একটি ride-sharing অ্যাপে লাখো চালক কয়েক সেকেন্ড পরপর অবস্থান পাঠান — পর্ব ৫৮-এ location update-এর স্রোত, geohash আর যাত্রীর সাথে চালক মেলানো। “আপনার রাইড এসে গেছে” push-টি যাবে এই পর্বের প্ল্যাটফর্ম দিয়েই।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান