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

Case study: ticket booking আর payment — concurrency, idempotency আর ledger

Case Study: Ticket Booking & Payments — Concurrency, Idempotency and Ledgers

Taught in Bangla, this lesson designs a ticket booking and payment system: on-sale peak and hold-expiry estimates, seat inventory with three ways to prevent double booking compared in a simulator and live SQL, a virtual waiting room with admission tokens and bot defences, payment idempotency keys, a state machine with an unknown state, reconciliation, a hold-charge-confirm saga and a double-entry ledger.

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

  • একটি টিকিট প্ল্যাটফর্মের requirement, on-sale-এর চূড়ার হিসাব আর hold-এর মেয়াদের প্রভাব নিজে গোনা
  • সিটের অবস্থা, hold আর double booking ঠেকানোর তিন কৌশলের তুলনা — simulator আর লাইভ SQL-এ
  • Waiting room, admission token, bot ঠেকানোর স্তর আর “পুরোনো কিন্তু নিরাপদ” seat map
  • Payment-এর idempotency, state machine, অজানা ফল, reconciliation আর hold → charge → confirm saga
  • Double-entry ledger, ভাঙনের তালিকা, ১০ গুণ ভিড়ের পরিকল্পনা আর ইন্টারভিউর ৬০ সেকেন্ডের সারাংশ

১ · সমস্যা আর পরিধি — একটি টিকিট প্ল্যাটফর্মের requirement

ঈদের আগে বাসের টিকিটের ভিড় যিনি দেখেছেন, তিনি সমস্যাটা জানেন: কাউন্টার খোলার আগেই লম্বা লাইন, সবাই একই দিনের একই রুটের টিকিট চান, আর সিট সীমিত। এবার কাউন্টারটি অনলাইনে নিন। এই পর্বের বিষয় “সিটঘর” — একটি টিকিট প্ল্যাটফর্ম (কাল্পনিক): আন্তঃজেলা বাসের সিট আর কনসার্ট বা খেলার টিকিট বিক্রি হয়, ক্রেতা নিজে সিট বাছেন, আর টাকা দেন বাইরের payment gateway বা mobile financial service (MFS) দিয়ে। কোনো বাস্তব রেল, বাস বা টিকিট-সেবার নকশা এখানে বর্ণনা করা হয়নি; সব সংখ্যা “ধরা যাক”।

এগোব পর্ব ০৬-এর আট ধাপের কাঠামো ধরে: পরিধি, হিসাব, API ও data model, ছবি, তারপর চারটি গভীর অংশ, ভাঙন আর সারাংশ। আগের মডিউলে শেখা প্রতিটি কৌশল এখানে এক বাক্যে প্রয়োগ হবে, সাথে তার পর্বের link.

যা করবে (functional)

ট্রিপ বা অনুষ্ঠান খোঁজা; seat map দেখা; এক অর্ডারে সর্বোচ্চ ৪টি সিট কিছুক্ষণ ধরে রাখা (hold); টাকা দেওয়া; e-ticket আর SMS; বাতিল ও ফেরত; আসনহীন গ্যালারি-টিকিটও।

ক্রেতার কাজ

যেমন হবে (non-functional)

একটি সিট কখনো দুজনকে নয়; টাকা দুবার কাটা বা হারানো নয়; on-sale-এর বহু গুণ ভিড়েও সাইট খোলা; seat map দ্রুত; কয়েক সেকেন্ডে নিশ্চিতকরণ; প্রতিটি টাকার audit.

মান আর নিশ্চয়তা

বাইরে রাখছি (out of scope)

Dynamic pricing, খোঁজার ranking, অপারেটরের onboarding, seat map আঁকা, জালিয়াতি ধরার মডেল, করের রিপোর্ট আর gateway-এর ভেতর।

স্পষ্ট সীমানা

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

  • সিট বাছাই, না শুধু সংখ্যা? নির্দিষ্ট সিট মানে প্রতিটি সিট একটি সারি; গ্যালারি মানে একটি counter (অংশ ৫)।
  • ভিড় কখন? বিক্রির সময় জানা থাকলে আগে থেকে সার্ভার বাড়ানো, cache গরম করা আর load test সম্ভব।
  • Hold কতক্ষণ, এক account কতগুলো? ব্যবসার সিদ্ধান্ত, কিন্তু হিসাব বদলায় (অংশ ২)।
  • টাকা কোন পথে, আর ফেরতের নীতি কী? Gateway কি idempotency key, webhook আর দিনশেষের রিপোর্ট দেয়; বাতিলে মাশুল কত — ledger-এর entry এখান থেকে আসে।

শুরুতেই একটি সিদ্ধান্ত পুরো নকশা গড়ে দেয়: সিস্টেমটি আসলে দুই জগৎ। দেখার জগৎ — ট্রিপের তালিকা, seat map, দাম — পড়া হয় বিপুল পরিমাণে, আর এক-দুই সেকেন্ড পুরোনো তথ্যেও ক্ষতি নেই। কেনার জগৎ — hold, অর্ডার, টাকা — অনুরোধ অনেক কম, কিন্তু প্রতিটি সিট আর প্রতিটি টাকার জন্য একটিই সত্য চাই।

ডেটাকতটা পুরোনো চলেভুল হলে ক্ষতি
ট্রিপের তালিকা, সময়, দামকয়েক মিনিটসামান্য — কেনার সময় দাম আবার যাচাই
Seat map-এ কোন সিট খালিএক-দুই সেকেন্ডকম — hold ব্যর্থ হলে map আবার আনা
সিটের hold আর bookingশূন্য — প্রতি সিটে একটিই সত্যবড় — একই সিটে দুজন যাত্রী
Payment আর ledgerশূন্য, আর পুরো ইতিহাস রাখতে হবেসবচেয়ে বড় — টাকা দুবার কাটা বা হারানো

রেলস্টেশনের টিকিট কাউন্টারের দেয়ালের বোর্ডে লেখা “ঢাকা–রাজশাহী: ১২টি সিট বাকি” — কয়েক মিনিট পুরোনো হলেও চলে। কিন্তু ভেতরের খাতা একটিই, আর কেরানি সিট নম্বরে টিক দেন। বোর্ড দেখে লাইনে দাঁড়ানো যায়; সিট মেলে খাতার টিকে। এই নকশায় বোর্ড হলো cache, খাতা হলো seat DB.

২ · হিসাব — on-sale-এর চূড়া, waiting room-এর হার আর hold-এর মেয়াদ

নকশা ঠিক হয় বিক্রি শুরুর প্রথম কয়েক মিনিটে, গড় দিনে নয় (পর্ব ০৩-এর peak-এর নিয়ম)। ধরা যাক ঈদের আগের এক সকালে ৮টায় এক দিনের সব ট্রিপের বিক্রি খোলে:

অনুমান (ধরা যাক)মান
বিক্রির সিট S১,২০,০০০ (৩,০০০ ট্রিপ × ৪০ সিট)
প্রথম ১০ মিনিটে আগ্রহী ক্রেতা U৬,০০,০০০; অর্ধেক আসেন প্রথম ৬০ সেকেন্ডে
Waiting room ছাড়া একজন পাঠানপ্রতি ২ সেকেন্ডে একটি অনুরোধ (refresh আর retry)
Waiting room-এর পাতা খোঁজ নেয়প্রতি ২০ সেকেন্ডে একবার, edge থেকে উত্তর
ভেতরে ঢোকানোর হার Aসেকেন্ডে ৪০০ জন; ভেতরে গড়ে ১৮০ সেকেন্ড, প্রতি ১০ সেকেন্ডে একটি অনুরোধ
ঢোকা মানুষের কতজন কেনেন p; অর্ডারে সিট k৭০%; গড়ে ১.৮টি
ডেটাবেসে লেখাhold-চেষ্টা ১.৩; ক্রেতাপ্রতি আরও ৯টি (অর্ডার, payment-এর ২টি অবস্থা, ledger-এর ৩ লাইন, confirm-এ গড়ে ২টি সিট, outbox); না-কেনা মানুষের hold ছাড়া ১টি

Hold-এর মেয়াদ কী করে, বুঝতে Little-এর সূত্র লাগবে (পর্ব ০৪): যে সিটগুলো কেউ ধরে রেখে চলে যান, সেগুলো মেয়াদ শেষ না হওয়া পর্যন্ত আটকে থাকে। আটকে থাকা সংখ্যা = আটকানোর হার × আটকে থাকার সময়, যতক্ষণ না পুরো inventory ফুরায়। নিচের calculator প্রতিটি সংখ্যা নিজে গোনে:

// On-sale estimator for a fictional ticket platform. Every input is an assumption.
const S = 120000;          // seats on sale (3,000 trips x 40 seats)
const U = 600000;          // hopeful buyers in the first 10 minutes
const firstMin = 0.5;      // share of U that arrives in the first 60 s
const A = 400;             // waiting room admits this many users per second
const p = 0.7, k = 1.8;    // share of admitted users who pay; seats per order
const writesPayer = 9, writesLeaver = 1, holdTries = 1.3;   // DB writes per user
const fmt = (x) => Math.round(x).toLocaleString("en-US");

const active = U * firstMin;
console.log("peak arrivals        :", fmt(active / 60), "users/s");
console.log("no waiting room      :", fmt(active / 2), "req/s to the booking API (1 req per 2 s)");
console.log("waiting room polls   :", fmt(U / 20), "req/s at the edge (1 poll per 20 s)");
console.log("admitted in the flow :", fmt(A * 180), "users -> API", fmt(A * 180 * 0.1), "req/s");
const writes = A * (holdTries + p * writesPayer + (1 - p) * writesLeaver);
console.log("DB write QPS         :", fmt(writes), "writes/s");
const sold = A * p * k, ghostRate = A * (1 - p) * k;      // seats per second
console.log("seats held per second:", fmt(sold), "by payers +", fmt(ghostRate), "by leavers");
console.log("users needed to sell out:", fmt(S / (p * k)), "of", fmt(U));
const tFull = S / (sold + ghostRate);
console.log("\nTTL   ghost seats  share  first 'sold out' at  ghosts return from");
for (const ttlMin of [1, 2, 5, 10]) {
  const T = ttlMin * 60;
  const ghost = T >= tFull ? ghostRate * tFull : ghostRate * T;
  const tOut = T >= tFull ? tFull : (S - ghostRate * T) / sold;
  console.log(String(ttlMin).padStart(2) + " min" + fmt(ghost).padStart(12) +
    ((100 * ghost / S).toFixed(1) + "%").padStart(8) +
    (tOut.toFixed(1) + " s").padStart(19) + (T + " s").padStart(18));
}

আউটপুট:

peak arrivals        : 5,000 users/s
no waiting room      : 150,000 req/s to the booking API (1 req per 2 s)
waiting room polls   : 30,000 req/s at the edge (1 poll per 20 s)
admitted in the flow : 72,000 users -> API 7,200 req/s
DB write QPS         : 3,160 writes/s
seats held per second: 504 by payers + 216 by leavers
users needed to sell out: 95,238 of 600,000

TTL   ghost seats  share  first 'sold out' at  ghosts return from
 1 min      12,960   10.8%            212.4 s              60 s
 2 min      25,920   21.6%            186.7 s             120 s
 5 min      36,000   30.0%            166.7 s             300 s
10 min      36,000   30.0%            166.7 s             600 s

দরজা না থাকলে সেকেন্ডে 150,000 অনুরোধ সরাসরি booking API আর ডেটাবেসে যেত। Waiting room থাকলে বাইরের ভিড় হয় edge-এ সেকেন্ডে 30,000টি হালকা খোঁজ, আর ভেতরে ৭২,০০০ জনের 7,200 req/s ও ডেটাবেসে 3,160টি লেখা — একটি ভাগ করা relational ডেটাবেসের সামর্থ্যে রাখা যায় এমন সংখ্যা।

সবাই পাবেন না: সব সিট বেচতে লাগে মাত্র ৯৫,২৩৮ জন; বাকি প্রায় ৫ লাখ মানুষকে যত আগে সম্ভব “বিক্রি শেষ” জানানোই সৎ আর সস্তা নকশা।

Hold-এর মেয়াদ: প্রতি সেকেন্ডে ২১৬টি সিট এমন মানুষ ধরেন, যাঁরা কিনবেন না। মেয়াদ ৫ বা ১০ মিনিট হলে বিক্রির ১৬৬.৭ সেকেন্ডেই পর্দায় “সব শেষ”, অথচ ৩৬,০০০টি সিট (৩০%) আসলে “ভূতের হাতে” — ফিরবে ৩০০ বা ৬০০ সেকেন্ডে, তখন লাইনের মানুষ হয়তো চলে গেছেন। মেয়াদ ১ মিনিট হলে আটকানো সিট ১০.৮%-এ নামে, কিন্তু MFS-এ PIN আর OTP দিতে যাঁর বেশি সময় লাগে, তিনি সিট হারাবেন — সমাধান অংশ ৭-এ।

Storage চাপ নয়: ধরা যাক বছরে ২ কোটি অর্ডার, অর্ডার-payment-ledger-outbox মিলিয়ে প্রতিটি প্রায় ২ KB — বছরে প্রায় ৪০ GB, পাঁচ বছরে প্রায় ২০০ GB. সমস্যাটা আকারের নয়, কয়েক মিনিটের ঢেউয়ের। আর ৫০,০০০ আসনের একটি স্টেডিয়ামের seat map-ও ছোট: প্রতি আসনে ২ bit (খালি, hold, বিক্রি) ধরলে প্রায় ১২,৫০০ byte — cache আর CDN-এ সহজে আঁটে।

৩ · API আর data model — প্রতিটি লেখায় key, প্রতিটি সিটে একটি সারি

API ছোট রাখুন, আর প্রতিটি লেখার অনুরোধে idempotency key বাধ্যতামূলক করুন — নিয়মটি পর্ব ০৮-এর নিরাপদ retry-র; দুর্বল মোবাইল নেটওয়ার্কে retry এখানে নিয়ম, ব্যতিক্রম নয়।

Endpointকাজনিশ্চয়তা
GET /v1/shows/{id}/seatmapকোন সিট খালিcache থেকে, ETag-সহ; এক-দুই সেকেন্ড পুরোনো চলে
POST /v1/holdsকয়েকটি সিট একসাথে ধরাসব, নয়তো একটিও না; key; সফল হলে মেয়াদসহ hold_id, না হলে 409
POST /v1/ordershold থেকে অর্ডার, দাম ঠিক করাkey; অবস্থা PENDING_PAYMENT
POST /v1/orders/{id}/paymentsgateway-তে পরিশোধ শুরুkey; 202 আর gateway-এর পাতায় যাওয়ার ঠিকানা
POST /v1/webhooks/paymentsgateway-এর খবরস্বাক্ষর যাচাই; gateway-এর event id দিয়ে duplicate বাদ
GET /v1/orders/{id}অবস্থা দেখাসরাসরি ডেটাবেস থেকে, cache নয়
GET /queue/statuswaiting room-এ নিজের জায়গাedge-এ, token দেখে; ডেটাবেস ছোঁয় না
POST /v1/holds
Authorization: Bearer <session>
Queue-Token: qt_8f2a...
Idempotency-Key: 5d1c7e02
{"show_id": 7, "seats": ["A1", "A2"]}

201 Created
{"hold_id": "h1", "expires_at": "08:12:30", "amount_paisa": 170000}

409 Conflict
{"error": "seat_unavailable", "seats": ["A2"], "seatmap_version": 813}

টাকা লেখা হয় পয়সায়, পূর্ণসংখ্যা হিসেবে — minor unit; floating-point-এ ০.১ + ০.২ ঠিক ০.৩ হয় না, আর লাখো লেনদেনে সেই ভুল জমে। 409-এর উত্তরে নতুন seat map-এর version থাকে, যাতে app সাথে সাথে map বদলাতে পারে।

Data model — কোন ডেটা কোথায়, কোন key-তে ভাগ

টেবিলমূল কলামকোথায়, কেন
seats(show_id, seat) primary key, status, hold_id, held_until, versionBooking DB; প্রতিটি সিট একটি সারি, তাই সংঘাত সারির স্তরে
ga_stock(show_id, tier, bucket), remaining CHECK >= 0Booking DB; আসনহীন টিকিটের counter, কয়েক ভাগে (অংশ ৫)
holds, ordershold_id, order_id, user_id, show_id, amount_paisa, statusBooking DB, একই show-এর shard-এ — confirm আর অর্ডার এক local transaction-এ
paymentspayment_id, order_id, state, gateway_key, gateway_refPayment DB; state machine (অংশ ৭)
ledger_entriestxn_id, line, account, amount_paisa — শুধু যোগ হয়Payment DB; payment-এর অবস্থা আর লাইন একসাথে commit (অংশ ৮)
idempotency_keys, inbox, outboxkey বা event_id UNIQUE, জমা উত্তরপ্রতিটি DB-তে; পর্ব ২৫ আর ৩৪-এর কৌশল

সিট, অর্ডার আর টাকা relational ডেটাবেসে (PostgreSQL বা MySQL-এর মতো) — দরকার শর্তসহ লেখা, CHECK, UNIQUE আর ACID transaction (পর্ব ২০)। ভাগের key — partition key — show_id: একটি show-এর সব সিট, hold আর অর্ডার এক shard-এ, তাই confirm-এর পুরো কাজ এক local transaction (পর্ব ২৩)। “আমার টিকিট” তালিকা user_id দিয়ে খোঁজা হয় — তার জন্য আলাদা read model, event থেকে বানানো (পর্ব ৩৫-এর CQRS); ট্রিপ খোঁজা search index-এ (পর্ব ২৭), আর e-ticket-এর PDF থাকে object storage-এ (পর্ব ২৬)।

৪ · High-level design — দরজা থেকে খাতা পর্যন্ত

পুরো ছবি নিচে। ওপর থেকে নিচে একটি অনুরোধের পথ; ডানে বাইরের gateway; নিচে খবর পাঠানোর পথ।

সিটঘরের নকশা — waiting room, দুটি সেবা, দুটি ডেটাবেস আর বাইরের gateway

Users → CDN + waiting room → booking API → booking service (cache + partitioned booking DB) and payment service (DB + ledger + outbox) ↔ gateway → relay → notifications

সিটঘরের high-level design যাত্রী ও দর্শক CDN + virtual waiting room Booking API ×N Booking service Payment service Gateway / MFS Seat- map Booking DB Payment DB Relay → broker Reconcile Notification service app · web স্থির পাতা · সারি · স্বাক্ষর-করা token token · rate limit · key যাচাই বাইরের সেবা hold · order · confirm state machine · saga payments · ledger outbox — একসাথে commit দুই DB-র outbox থেকে event gateway রিপোর্ট SMS · push · email (পর্ব ৫৭) কঠিন তীর: অনুরোধ · ভাঙা তীর: বাইরের call বা event লাল সীমানা: সত্যের উৎস · cache শুধু ইঙ্গিত cache shard: show_id seats · orders charge ↑ webhook ↓
ছবি ৬০.১ — কঠিন তীর অনুরোধ, ভাঙা তীর বাইরের call বা event. লাল সীমানার দুটি বাক্স সত্যের উৎস: সিটের সারিসহ Booking DB (এই পর্বে “seat DB”) আর ledger-সহ Payment DB. Cache শুধু দেখানোর জন্য।

পড়ার পথ: app → CDN (স্থির আসনবিন্যাস) → booking API → seat-map cache; miss হলে replica থেকে, একসাথে আসা miss মিলিয়ে একটিতে (request coalescing, পর্ব ২৯)। লেখার পথ: booking service-এর শর্তসহ লেখা seat DB-র primary-তে → payment service → gateway; খবর ফেরে webhook-এ, নিশ্চিতকরণ যায় outbox → relay → broker → notification service হয়ে।

এবার রুমানার একটি কেনা, শুরু থেকে শেষ — সব ঠিকঠাক চললে:

রুমানার অর্ডার আর সিস্টেমের অবস্থা
সময়
রুমানার পর্দা
সিট A1 · A2
অর্ডার O1
Payment P1
Ledger · outbox
  1. 07:46 — সারিতে। রুমানা CDN থেকে waiting room-এর স্থির পাতা আর স্বাক্ষর-করা token পেলেন; ৮টার lottery-তে নম্বর #৪১,৩৭৭ (অংশ ৬)। কোনো ডেটাবেস ছোঁয়া হয়নি।
  2. 08:01:58 — ভেতরে। ভর্তির নম্বর তাঁর নম্বর পেরোল; edge দিল ১৫ মিনিটের admission token. Booking API শুধু স্বাক্ষর আর মেয়াদ যাচাই করে, ডেটাবেস ছাড়াই।
  3. 08:02:20 — seat map. Cache থেকে map, version ৮১২, প্রায় এক সেকেন্ড পুরোনো। A1 আর A2 খালি দেখাচ্ছে — এটি প্রতিশ্রুতি নয়, শুধু ইঙ্গিত।
  4. 08:02:30 — hold. POST /holds, idempotency key-সহ। Booking service show ৭-এর shard-এ শর্তসহ UPDATE চালাল — দুটি সিটই খালি ছিল, তাই দুটিই held h1, মেয়াদ 08:12:30.
  5. 08:04:10 — অর্ডার আর পরিশোধ। O1 PENDING_PAYMENT, ৳১,৭০০; P1 গেল gateway-তে, key O1-p1. রুমানা MFS অ্যাপে PIN দিলেন — সেই অংশ gateway-এর।
  6. 08:05:00 — webhook. Gateway স্বাক্ষর-করা খবর পাঠাল: সফল। Payment service এক local transaction-এ P1-কে SUCCEEDED করল, ledger-এ ৩টি লাইন লিখল (যোগফল শূন্য), আর outbox-এ PaymentSucceeded.
  7. 08:05:01 — confirm. Booking service event পেয়ে এক local transaction-এ: সিট দুটি booked (শর্ত: এখনো h1-এর hold), O1 CONFIRMED, আর outbox-এ BookingConfirmed. পর্দায় “নিশ্চিত”।
  8. 08:05:03 — খবর। Relay event broker-এ দিল; notification service SMS আর push পাঠাল (পর্ব ৫৭), cache নতুন version পেল। প্রতিটি ধাপ আলাদা commit, তবু retry-যোগ্য আর idempotent.

কেন দুটি সেবা, পাঁচটি নয়? Booking আর payment আলাদা, কারণ টাকার অংশের নিরাপত্তা, audit আর দল আলাদা, আর gateway-এর ধীরতা যেন সিট বিক্রি না থামায় (পর্ব ৪৭)। আরও ভাঙলে প্রতিটি নতুন সীমানায় saga আর outbox বাড়ে; শুরুতে modular monolith-ও সৎ উত্তর।

৫ · Deep dive ১ — inventory আর concurrency: একটি সিট, একজন মালিক

প্রতিটি সিটের জীবন একটি ছোট state machine. অবস্থা তিনটি, আর প্রতিটি বদল একটি শর্তসহ লেখা:

একটি সিটের অবস্থা

available → held (with a deadline) → booked; held → available on release or expiry

  1. availableকেউ ধরেননিবা hold-এর মেয়াদ শেষ
  2. heldhold_id · held_untilছাড়লে বা মেয়াদ গেলে আবার available
  3. bookedorder_idবাতিল হলে নীতি মেনে ফেরত
ছবি ৬০.২ — Hold হলো পর্ব ২৫-এর semantic lock, সাথে একটি মেয়াদ (TTL): কেউ চলে গেলে বা crash করলে সিট নিজে থেকেই ফেরে।

বিপদ একটিই: দুজন একই মুহূর্তে একই খালি সিট দেখেন, আর দুজনেই লেখেন — overselling বা double booking. পর্ব ২৪-এর অ্যাপ-স্তরের প্যাটার্ন থেকে তিনটি পথ এখানে প্রয়োগ হয়:

কৌশলকীভাবেভিড়ে আচরণকখন মানায়
Pessimistic — SELECT … FOR UPDATEপড়ার সময়েই সারিতে lock, যাচাই, লেখা, commitঅন্যরা লাইনে অপেক্ষা করেন; lock-এর সময়ই throughput-এর সীমা; কয়েক সিটে একই ক্রমে lock না নিলে deadlockযাচাই এক বাক্যে আঁটে না (যেমন account-এর মোট সীমা)
Optimistic — version কলামlock ছাড়া পড়া; WHERE version = v দিয়ে লেখাসংঘাতে ০ সারি, আবার পড়ে চেষ্টা; version কোন স্তরে, তার ওপর সব নির্ভরপড়া আর লেখার মাঝে সিদ্ধান্তের কাজ আছে
শর্তসহ atomic updateএক বাক্যে: SET status = 'held' … WHERE status = 'available'এক round trip, lock শুধু বাক্যের সময়; ০ সারি মানে “অন্য কেউ আগে নিয়েছেন”সিট hold-এর মতো সরল নিয়ম — এখানে প্রথম পছন্দ

কোনটি কত ভালো, একটি simulator দেখায়। ১০০টি সিট, ৩০০ জন ক্রেতা প্রথম ২০০ ms-এর মধ্যে আসেন; প্রত্যেকে seat map দেখে একটি খালি সিট বাছেন, তারপর ৫–৩০ ms পরে লেখেন। লেখা ব্যর্থ হলে আবার দেখে অন্য সিট, সর্বোচ্চ তিনবার। প্রতিটি কৌশলে আগমন আর দেরি একই seeded সংখ্যায়:

// Double booking race: 300 buyers, 100 seats, the same seeded arrivals for every strategy.
function mulberry32(a) {
  return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const SEATS = 100, BUYERS = 300, MAX_TRIES = 3;
function run(mode) {
  const r = mulberry32(42), owners = Array.from({ length: SEATS }, () => []);
  const ver = new Array(SEATS).fill(0); let showVer = 0, fails = 0, retries = 0, gaveUp = 0;
  const ev = [];                                  // arrivals spread over the first 200 ms
  for (let b = 0; b < BUYERS; b++) ev.push({ t: Math.floor(r() * 200), b, kind: "read", tries: 1 });
  while (ev.length) {
    ev.sort((x, y) => x.t - y.t || x.b - y.b); const e = ev.shift();
    if (e.kind === "read") {                      // look at the seat map, pick a free seat
      const free = owners.map((o, s) => o.length ? -1 : s).filter((s) => s >= 0);
      if (!free.length) continue;                 // sold out: the buyer leaves
      const s = free[Math.floor(r() * free.length)];
      const v = mode === "version per show" ? showVer : ver[s];
      ev.push({ ...e, kind: "write", t: e.t + 5 + Math.floor(r() * 25), s, v });
      continue;
    }
    const ok = mode === "naive read-then-write" ? true
      : mode === "conditional UPDATE" ? owners[e.s].length === 0
      : mode === "version per seat" ? ver[e.s] === e.v : showVer === e.v;
    if (ok) { owners[e.s].push(e.b); ver[e.s]++; showVer++; continue; }
    fails++;                                      // 0 rows updated: re-read and try again
    if (e.tries < MAX_TRIES) { retries++; ev.push({ ...e, kind: "read", t: e.t + 1, tries: e.tries + 1 }); }
    else gaveUp++;
  }
  const tickets = owners.reduce((n, o) => n + o.length, 0), sold = owners.filter((o) => o.length).length;
  console.log(mode.padEnd(22) + String(tickets).padStart(8) + String(tickets - sold).padStart(10) +
    String(fails).padStart(8) + String(retries).padStart(9) + String(gaveUp).padStart(9) + String(SEATS - sold).padStart(8));
}
console.log("strategy               tickets  oversold   fails  retries  gave up  unsold");
for (const m of ["naive read-then-write", "conditional UPDATE", "version per seat", "version per show"]) run(m);

আউটপুট:

strategy               tickets  oversold   fails  retries  gave up  unsold
naive read-then-write      193        93       0        0        0       0
conditional UPDATE         100         0     109      100        9       0
version per seat           100         0     109      100        9       0
version per show            32         0     838      570      268      68

Naive পথে কোনো লেখা ব্যর্থ হয় না — সেটিই সমস্যা: ১০০টি সিটে 193টি টিকিট, 93টি সিট দুবার বা তারও বেশি বিক্রি। পড়া আর লেখার মাঝের ৫–৩০ ms-এ অন্যরাও একই সিট খালি দেখেছেন।

শর্তসহ UPDATE আর প্রতি-সিট version-এর ফল হুবহু এক: ঠিক ১০০টি টিকিট, 109টি ব্যর্থ লেখা, 100টি retry; ৯ জন তিনবার হেরে ফিরেছেন, কিন্তু কোনো সিট পড়ে নেই। সিটের একটিই অবস্থা থাকলে “version বদলেছে” আর “সিট আর খালি নেই” একই কথা; পার্থক্য শুধু round trip-এ।

পুরো show-এর একটি version হলে যেকোনো বিক্রি সবার version পুরোনো করে, তাই আলাদা সিটের ক্রেতারাও সংঘাতে — 838টি ব্যর্থ লেখা, 268 জন ফিরে গেছেন, আর ক্রেতা থাকা সত্ত্বেও 68টি সিট অবিক্রীত। এটি একটি hot row. নিয়ম: সংঘাতের একক হোক ব্যবসার আসল একক — সিট।

কয়েকটি সিট একসাথে, আর মেয়াদ

রুমানা A1 আর A2 একসাথে চান; একটি পেলে চলবে না — পুরো শর্ত একটি বাক্যে, অথবা এক transaction-এ দুটি লেখা আর বদলানো সারি ২ না হলে rollback. Pessimistic পথে সিটগুলো সবসময় একই ক্রমে lock করুন, নইলে deadlock (DBMS কোর্সের deadlock পর্ব)।

মেয়াদ শেষে সিট কে ফেরত দেবে? শর্তেই মেয়াদ রাখুন: “খালি” মানে available, অথবা held কিন্তু held_until পেরিয়ে গেছে — Redis-এর lazy expiration-এর মতো। একটি sweeper job (পর্ব ৩৭-এর delayed job) পরিষ্কার করে আর cache ও waiting room-কে খবর দেয়, কিন্তু সঠিকতা তার ওপর নির্ভর করে না। নিচের লাইভ SQL-এ সময় মিনিটে (৬০০ = সকাল ১০টা); নমুনা ডেটাবেসে show ৭-এর A1–A4 খালি, আর আসনহীন show ৯-এ ৩টি টিকিট বাকি:

-- Time is minutes since midnight: 600 = 10:00. A hold lasts 10 minutes.
-- 10:00 Rumana holds A1 and A2. A seat is free if available OR its hold expired.
UPDATE seats SET status = 'held', hold_id = 'h1', held_until = 610
WHERE show_id = 7 AND seat IN ('A1', 'A2')
  AND (status = 'available' OR (status = 'held' AND held_until < 600));
-- 10:03 Karim asks for A2 and A3 together: all or nothing, so 0 rows.
UPDATE seats SET status = 'held', hold_id = 'h2', held_until = 613
WHERE show_id = 7 AND seat IN ('A2', 'A3')
  AND (SELECT COUNT(*) FROM seats WHERE show_id = 7 AND seat IN ('A2', 'A3')
       AND (status = 'available' OR (status = 'held' AND held_until < 603))) = 2;
-- 10:03 Karim takes A3 and A4 instead.
UPDATE seats SET status = 'held', hold_id = 'h2', held_until = 613
WHERE show_id = 7 AND seat IN ('A3', 'A4') AND status = 'available';
-- 10:08 Rumana's payment succeeds: confirm only her own, unexpired hold.
UPDATE seats SET status = 'booked' WHERE hold_id = 'h1' AND status = 'held' AND held_until >= 608;
-- 10:15 Karim's payment arrives after his hold expired: 0 rows, so refund him.
UPDATE seats SET status = 'booked' WHERE hold_id = 'h2' AND status = 'held' AND held_until >= 615;
-- 10:16 Sumi takes A4: the expired hold is reclaimed by the same WHERE.
UPDATE seats SET status = 'held', hold_id = 'h3', held_until = 626
WHERE show_id = 7 AND seat = 'A4'
  AND (status = 'available' OR (status = 'held' AND held_until < 616));
SELECT seat, status, hold_id, held_until FROM seats WHERE show_id = 7 ORDER BY seat;
-- General admission: a counter, decremented atomically with a guard.
UPDATE ga_stock SET remaining = remaining - 2 WHERE show_id = 9 AND remaining >= 2;
UPDATE ga_stock SET remaining = remaining - 2 WHERE show_id = 9 AND remaining >= 2;
SELECT show_id, remaining FROM ga_stock;

ছয়টি লেখার ফল ক্রমে: ২টি সারি বদলেছে (রুমানার hold), ০টি (করিমের A2 + A3 — A2 রুমানার, তাই সব-নয়তো-কিছুই-না শর্তে একটিও নয়), ২টি (করিম A3 + A4), ২টি (রুমানার confirm, মেয়াদের ভেতরে), ০টি (করিমের payment ১০:১৫-এ, hold শেষ ১০:১৩-এ — তাঁর টাকা ফেরত যাবে), আর ১টি (সুমি A4 পেলেন; মেয়াদোত্তীর্ণ hold একই WHERE-এ ফিরেছে, sweeper ছাড়াই)। SELECT: A1 আর A2 booked h1, A3 এখনো held h2 613 (মেয়াদ গেছে, কিন্তু কেউ চায়নি বলে সারি বদলায়নি), A4 held h3 626. Counter-এ প্রথম কমানোয় ১টি সারি, দ্বিতীয়তে ০টি; শেষে 9 | 1. SQLite ডেমোটি শর্তের যুক্তি দেখায়, সমান্তরাল চাপ বা shard নয়।

Hot row, hot shard আর ভাগের কৌশল

প্রতিটি সিট আলাদা সারি বলে সংঘাত ছড়িয়ে যায়, শুধু জনপ্রিয় সিটে ভিড় জমে। কিন্তু general admission বা আসনহীন টিকিটে সবাই একটিই counter সারি কমান। পর্ব ২৪-এর ব্যস্ত সারির হিসাব: প্রতিটি লেখা lock ধরে h সেকেন্ড, তাই সারিটিতে সর্বোচ্চ ১ ÷ h লেখা প্রতি সেকেন্ডে। h = ৪ ms হলে ২৫০; প্রথম মিনিটে সেকেন্ডে ৩,০০০ টিকিটের চাহিদা থাকলে অন্তত ১২টি ভাগ লাগে। কৌশলগুলো:

  • Counter ভাগ করা: একটি গ্যালারির ১০,০০০ টিকিট ১২টি bucket-এ, প্রতিটি অনুরোধ একটি bucket থেকে কমায়, খালি হলে পাশেরটিতে — sharded counter; মোট সীমা কখনো ভাঙে না, কারণ প্রতিটি bucket-এর নিজের CHECK >= 0. এটি Patrick O’Neil-এর ১৯৮৬ সালের escrow পদ্ধতির ধারণা, পর্ব ৪৬-এর bounded counter-এর আত্মীয়।
  • একজন লেখক: একটি গ্যালারি বা সেকশনের সব অনুরোধ একটি queue-তে, একটি worker ক্রমানুসারে সিদ্ধান্ত নেয় আর এক transaction-এ কয়েকশো hold লেখে — single-writer নীতি (পর্ব ৪৩-এর lock ছাড়াই)।

Partition key show_id হাজারো ট্রিপকে হাজারো shard-এ ছড়ায়, কিন্তু ৫০,০০০ আসনের একটি কনসার্ট পুরোটাই এক shard-এ — একটি hot spot, যার আলোচনা পর্ব ২৩-এর hot spot আর skew অংশে। প্রতিকার: এমন show আলাদা শক্তিশালী shard বা cell-এ, আর অর্ডার এক সেকশনে সীমিত করে key (show_id, section) (ভাবনার প্রশ্ন ১)।

Distributed lock সঠিকতার ভিত্তি নয়। Redis-এ SET … NX PX দিয়ে lock, তারপর শর্তহীন লেখা — lock-এর মেয়াদ থেমে থাকা process-এর হাতে ফুরালে বা failover-এ হারালে দুজন মালিক। পর্ব ৪৩-এর fencing token-এর শিক্ষা: শেষ লেখাটিই শর্তসহ হোক; তখন lock শুধু optimization (ভাবনার প্রশ্ন ৩)।

৬ · Deep dive ২ — on-sale-এর ঢেউ: waiting room, bot আর seat map

দরজা না থাকলে সেকেন্ডে দেড় লাখ অনুরোধ, থাকলে ভেতরে হাজার সাতেক (অংশ ২)। Virtual waiting room আসলে edge-এ বসানো admission control — পর্ব ১৮-এর দরজায় গোনা, মানুষের জন্য সাজানো। ফেরিঘাটের সিরিয়ালের মতো: সবাই একসাথে উঠতে চাইলে কেউ ওঠেন না; টোকেন নিয়ে অপেক্ষা করলে ফেরি ভরা থাকে, ঘাটে ধাক্কাধাক্কি নেই।

যন্ত্রটি কীভাবে চলে

  1. স্থির পাতা, edge থেকে: সারির পাতা আর খোঁজের উত্তর CDN থেকে (পর্ব ১৭); origin-এ ভার নেই।
  2. খোলার আগে এলে lottery, পরে এলে সারি: নির্দিষ্ট সময়ের আগে আসা সবার নম্বর খোলার মুহূর্তে এলোমেলো; পরে আসা মানুষ আসার ক্রমে পেছনে — মিলিসেকেন্ডের দৌড় সুবিধা পায় না।
  3. স্বাক্ষর-করা token: নম্বর, session আর মেয়াদ একটি token-এ, server-এর গোপন key দিয়ে HMAC-স্বাক্ষর (পর্ব ১৫)। ডেটাবেস ছাড়াই যাচাই; session-এ বাঁধা, তাই বদলানো বা বেচা যায় না।
  4. ভর্তির নম্বর X এগোয় সেকেন্ডে A করে: নম্বর ≤ X হলে ১৫ মিনিটের admitted token. A ঠিক করে একটি controller, ডেটাবেসের p99 আর error দেখে — পর্ব ৪৯-এর adaptive সীমা.
  5. সিট ফুরালে থামা: ভর্তি বন্ধ, অপেক্ষারতদের সৎ খবর; hold ফিরলে আবার খোলে।
সারির অবস্থা (বিক্রি খোলে 08:00:00)
সময়
ভর্তির নম্বর X
হার A
রুমানা (07:46-এ এসেছেন)
করিম (07:59:58-এ)
সুমি (08:00:40-এ)
  1. খোলার আগে। ধরা যাক (অংশ ২-এর চেয়ে হালকা একটি কাল্পনিক ভিড়) 07:45 থেকে 08:00-এর মধ্যে আসা ১,২০,০০০ জন একটি দলে — রুমানা 07:46-এ, করিম 07:59:58-এ; দলে আগে আসার বাড়তি সুবিধা নেই।
  2. 08:00:00 — lottery. দলটি এলোমেলো করে নম্বর: করিম #৮,২১৪, রুমানা #৪১,৩৭৭. ভর্তি শুরু সেকেন্ডে ৪০০ জন করে।
  3. 08:00:21 — করিম ভেতরে। X = ৪০০ × ২১ = ৮,৪০০ ≥ ৮,২১৪. Edge তাঁকে ১৫ মিনিটের admitted token দিল।
  4. 08:00:40 — সুমি এলেন। খোলার পরে, তাই দলের পেছনে: #১,৩৫,৬০২. X = ১৬,০০০; অনুমান (১,৩৫,৬০২ − ১৬,০০০) ÷ ৪০০ ≈ ২৯৯ সেকেন্ড — “প্রায় ৫ মিনিট”।
  5. 08:01:00 — চাপ কমানো। ডেটাবেসের p99 ২৫০ ms, লক্ষ্য ১৫০ ms-এর ওপরে; controller A নামাল ৩০০-তে। X = ২৪,০০০; সুমির নতুন অনুমান (১,৩৫,৬০২ − ২৪,০০০) ÷ ৩০০ ≈ ৩৭২ সেকেন্ড, প্রায় ৬.২ মিনিট।
  6. 08:01:58 — রুমানা ভেতরে। X = ২৪,০০০ + ৩০০ × ৫৮ = ৪১,৪০০ ≥ ৪১,৩৭৭ (এক সেকেন্ড আগে ছিল ৪১,১০০)। এখান থেকে অংশ ৪-এর stepper-এর পথ।
  7. 08:02:50 — সিট শূন্য। ধরা যাক এই মুহূর্তে সব সিট বিক্রি বা hold-এ। ভর্তি থামল X = ২৪,০০০ + ৩০০ × ১১০ = ৫৭,০০০-এ; সুমিসহ পেছনের সবাই সৎ খবর পেলেন, আর খোঁজের ব্যবধান বাড়ল ৬০ সেকেন্ডে।

Bot আর অপব্যবহার — শুধু সুরক্ষার দিক থেকে

জনপ্রিয় টিকিটে script দিয়ে গণ-কেনা আর পরে বেশি দামে বিক্রির চেষ্টা থাকেই। Bot mitigation একটি যন্ত্র নয়, কয়েকটি স্তর:

স্তরকী করেসতর্কতা
Account-এর সীমাঅর্ডারে সর্বোচ্চ ৪টি সিট, এক সময়ে একটিই সক্রিয় hold, এক show-এ সীমিত অর্ডারসবচেয়ে কার্যকর আর সস্তা; ভূতের hold-ও কমায়
Account যাচাইফোন নম্বরে OTP (পর্ব ৫৭) — এক মানুষ, মোটামুটি এক accountগোপনীয়তা আর প্রবেশযোগ্যতার কথা মাথায় রেখে
Rate limitaccount, device আর IP-প্রতি token bucket (পর্ব ৪৯)অনেক মোবাইল নেটওয়ার্কে বহু মানুষ একই public IP ভাগ করেন — IP-র সীমা ঢিলা রাখুন
সন্দেহে যাচাই আর নজরদারিঅস্বাভাবিক আচরণে মানুষ-যাচাইয়ের প্রশ্ন; অনেক hold কিন্তু payment নেই — চিহ্নিতসবাইকে নয়; সিদ্ধান্তে মানুষের পর্যালোচনা

Seat map-এর cache — পুরোনো, কিন্তু নিরাপদ

প্রতিটি hold-এর আগে seat map কয়েকবার দেখা হয়। Map আসে cache-aside পথে (পর্ব ২৮), এক-দুই সেকেন্ডের TTL বা বদলের event-এ নতুন version; একসাথে হাজার miss হলে coalescing (পর্ব ২৯-এর stampede-রোধ)। স্থির আসনবিন্যাস CDN-এ দীর্ঘ মেয়াদে, শুধু ছোট availability-অংশ ঘনঘন বদলায়।

পুরোনো-কিন্তু-নিরাপদের নিয়ম

Cache দুই দিকে ভুল করতে পারে, কোনোটিই বিক্রিতে ভুল ঘটায় না। “খালি” দেখাল অথচ hold — শর্তসহ লেখা ০ সারি, 409, app map আবার আনে; ক্ষতি একটি ক্লিক। “বিক্রি” দেখাল অথচ খালি — কয়েক সেকেন্ডে ঠিক হয়। সিদ্ধান্ত হয় শুধু seat DB-র শর্তসহ লেখায়; “মাত্র ১২টি বাকি” তাই দেখানোর জন্য, প্রতিশ্রুতি নয়।

ভিড় সামলাতে পর্ব ১৮-এর graceful degradation-ও কাজে লাগে: on-sale-এর সময় সুপারিশ, রিভিউ আর বিশদ খোঁজ বন্ধ রেখে সব সামর্থ্য কেনার পথে দেওয়া।

৭ · Deep dive ৩ — payment: idempotency, অজানা ফল আর reconciliation

সিট আমাদের ডেটাবেসে, টাকা অন্যের সিস্টেমে; মাঝে কোনো যৌথ transaction নেই, আর MFS-এ PIN দেওয়ার মাঝপথে সংযোগও যেতে পারে। নকশা তিনটি নিয়মে দাঁড়ায়।

নিয়ম ১ — প্রতিটি কড়ায় একটি key

কোথায়KeyDuplicate এলে
App → POST /orders, /paymentsapp-এর বানানো UUID, প্রতিটি অভিপ্রায়ে একটিজমা উত্তর হুবহু (পর্ব ০৮)
আমাদের সার্ভার → gatewayঅর্ডার আর চেষ্টা থেকে বানানো, যেমন O1-p1gateway দ্বিতীয়বার কাটে না, আগের ফল দেয় — যদি gateway key নেয় (অনেক payment API-র প্রকাশিত ডকুমেন্টেশনে আছে; নিজেরটির চুক্তি যাচাই করুন)
Gateway → আমাদের webhookgateway-এর event id; স্বাক্ষর যাচাই আগেinbox-এ আগেই আছে, তাই বাদ (পর্ব ৩৪)
Ledger-এ লেখালেনদেনের id, যেমন P1-captureUNIQUE-এ আটকায় (অংশ ৮)
RefundO1-refund-1একবারই ফেরত

Gateway key না নিলে? তাহলে retry-র আগে অবস্থা জিজ্ঞেস করুন (“অর্ডার O1-এর চার্জ হয়েছে?”), আর নিজের দিকে “পাঠানো হচ্ছে” লিখে রাখুন — পর্ব ৩৪-এর নিয়ন্ত্রণের বাইরের side effect. টাকার পথে key ছাড়া অন্ধ retry চলে না।

নিয়ম ২ — Timeout মানে UNKNOWN, FAILED নয়

Payment-এর অবস্থা একটি state machine; প্রতিটি বদল একটি শর্তসহ UPDATE, যেমন SET state = 'SUCCEEDED' WHERE id = 'P1' AND state IN ('PENDING', 'UNKNOWN') — দেরিতে আসা দ্বিতীয় webhook ০ সারি পায়, অবস্থা পেছনে যায় না।

থেকেঘটনাযেখানে যায়
CREATEDgateway-তে অনুরোধ গেল (key-সহ)PENDING
PENDINGসফলতার উত্তর বা webhookSUCCEEDED — সাথে ledger-এর লাইন, একই transaction-এ
PENDINGgateway স্পষ্ট “না” বললFAILED — গ্রাহক নতুন চেষ্টা (নতুন key) দিতে পারেন
PENDINGউত্তর এল না (timeout)UNKNOWN — কখনো FAILED নয়
UNKNOWNএকই key-তে খোঁজ, webhook বা reconciliationSUCCEEDED বা FAILED
SUCCEEDEDঅর্ডার বাতিল বা মেয়াদোত্তীর্ণREFUND_PENDING → REFUNDED, উল্টো ledger লাইনসহ

UNKNOWN চলাকালীন সিটের কী হবে? Payment শুরু হলে hold-এর মেয়াদ একবার, একটি সীমা পর্যন্ত বাড়ে (ধরা যাক ৫ মিনিট); তারপরও অজানা থাকলে সিট ছাড়া, আর পরে সফলতার খবর এলে টাকা ফেরত — নিচের stepper. বিকল্প নীতি: সিট তখনো খালি থাকলে আবার hold করে confirm. এখানে payment-ই pivot: confirm একটি local, শর্তসহ, retry-যোগ্য লেখা — যতক্ষণ hold বৈধ। Saga-র তত্ত্ব পর্ব ২৫-এর saga অংশে।

অর্ডার O1 · সিট A1, A2 · ৳১,৭০০
সময়
সিট A1 · A2
অর্ডার O1
Payment P1
Ledger
রুমানার পর্দা বা SMS
  1. T1 — hold. A1 আর A2 শর্তসহ লেখায় h1-এর, মেয়াদ 10:10; O1 PENDING_PAYMENT. উল্টানো সস্তা।
  2. T2 — charge শুরু। P1 PENDING, gateway-তে key O1-p1. Payment চলছে বলে hold একবার বেড়ে 10:15 — এর বেশি নয়।
  3. Timeout. ৩০ সেকেন্ডে উত্তর নেই: P1 UNKNOWN, FAILED নয় — টাকা হয়তো কেটেছে। নতুন key-তে চার্জ নয়; পর্দার সতর্কবার্তা রুমানাকেও আবার চাপতে নিষেধ করে।
  4. খোঁজ, backoff-সহ। একই key-তে অবস্থা জানতে চাওয়া হলো ৫, ১০, ২০ সেকেন্ড… ব্যবধানে, পর্ব ৪৮-এর exponential backoff-এ; gateway প্রতিবারই জানাল, এখনো প্রক্রিয়াধীন।
  5. C1 — সীমা পেরোল, সিট ছাড়া। 10:15-এ P1 এখনো অজানা। Compensation: WHERE hold_id = 'h1' AND status = 'held' দিয়ে সিট ছাড়া, O1 EXPIRED, আর একই transaction-এ outbox-এ খবর।
  6. দেরিতে সফলতা। 10:21:40-এ স্বাক্ষর-করা webhook: P1 সফল। Payment service P1-কে SUCCEEDED করল আর booking-এর ৩টি ledger লাইন লিখল। কিন্তু O1 EXPIRED, আর A1 ইতিমধ্যে সুমির hold-এ — তাই C2: refund, key O1-refund-1, P1 REFUND_PENDING.
  7. ফেরত নিশ্চিত। Ledger-এ আরও ৩টি উল্টো লাইন, মোট ৬, যোগফল শূন্য; P1 REFUNDED, রুমানার কাছে SMS. সিট দুবার বিক্রি হয়নি, টাকা হারায়নি — তবু অভিজ্ঞতা খারাপ, তাই এই পথ কতবার ঘটে, মাপুন।

এবার বড় পরিসরে: ২,০০০টি payment, স্বাভাবিক সময়ে ৩% অনুরোধে timeout, আর অর্ডার ১০০১–১২০০-এর সময় gateway ধীর, ৫০% timeout. Timeout-এর ৬০% ক্ষেত্রে টাকা আসলে কেটেছে, শুধু উত্তর হারিয়েছে; ২% ক্রেতা দুবার চাপেন; উত্তর না এলে সর্বোচ্চ তিনবার চেষ্টা। দুই নকশা — key ছাড়া (timeout মানে ব্যর্থ) আর key-সহ (timeout মানে UNKNOWN) — একই seeded দুর্ঘটনায়, তারপর gateway-এর রিপোর্টের সাথে মিলিয়ে:

// 2,000 payments through a gateway with seeded timeouts, a slow period and double taps.
function mulberry32(a) {
  return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const N = 2000, TRIES = 3, DOUBLE_TAP = 0.02, APPLIED_IF_TIMEOUT = 0.6;
const timeoutP = (id) => (id > 1000 && id <= 1200 ? 0.5 : 0.03);  // orders 1001-1200: gateway is slow
const W = [8, 7, 9, 8, 9, 8, 5, 6];
const show = (m, row) => console.log(m.padEnd(11) + row.map((v, i) => String(v).padStart(W[i])).join(""));
function run(useKey) {
  const r = mulberry32(7), charges = new Map(), ours = new Map();
  for (let id = 1; id <= N; id++) {
    let paid = false;
    const calls = r() < DOUBLE_TAP ? 2 : 1;       // a double tap sends a second, parallel request
    for (let c = 0; c < calls; c++)
      for (let t = 1; t <= TRIES; t++) {          // retry only while the reply is missing
        const timeout = r() < timeoutP(id), applied = !timeout || r() < APPLIED_IF_TIMEOUT;
        if (applied && !(useKey && charges.has(id))) charges.set(id, (charges.get(id) || 0) + 1);
        if (!timeout) { paid = true; break; }     // with a key, the reply carries the first result
      }
    ours.set(id, paid ? "PAID" : useKey ? "UNKNOWN" : "FAILED");   // no key: timeout = failure
  }
  const c = { total: 0, double: 0, chgFail: 0, unknown: 0, bad: 0, refunds: 0, late: 0, ok: 0 };
  for (const [id, st] of ours) {                  // reconciliation with the gateway's report
    const n = charges.get(id) || 0; c.total += n;
    if ((st === "PAID") !== (n === 1)) c.bad++;   // mismatch before reconciliation
    if (n > 1) c.double++; if (st === "FAILED" && n > 0) c.chgFail++;
    if (st === "UNKNOWN") { c.unknown++; ours.set(id, n > 0 ? "PAID" : "FAILED"); if (n > 0) c.late++; }
    const keep = ours.get(id) === "PAID" ? 1 : 0;  // keep one charge for a paid order, refund the rest
    c.refunds += n - Math.min(n, keep); if (Math.min(n, keep) === keep) c.ok++;
  }
  show(useKey ? "with key" : "without key", Object.values(c));
}
show("mode", ["charges", "double", "chg+fail", "unknown", "mismatch", "refunds", "late", "ok"]);
run(false); run(true);

আউটপুট:

mode        charges double chg+fail unknown mismatch refunds late    ok
without key    2181    164       17       0      157     199    0  2000
with key       1999      0        0      18       17       0   17  2000

Key ছাড়া: ২,০০০টি অর্ডারে 2181টি চার্জ; 164টি অর্ডারে দুবার বা বেশি — প্রতিটি retry আর দ্বিতীয় চাপ নতুন চার্জ, তার প্রায় অর্ধেক ধীর সময়ের মাত্র ২০০টি অর্ডারে। আরও 17টিকে আমরা “ব্যর্থ” ধরে সিট ছেড়েছি, অথচ টাকা কেটেছে। মেলানোর আগে 157টি অর্ডারে দুই খাতা আলাদা। Reconciliation সব ঠিক করে (ok ২,০০০) — কিন্তু 199টি refund-এর দামে: মাশুল, অপেক্ষা আর ক্ষুব্ধ গ্রাহক।

Key-সহ: double charge শূন্য। ১৮টি অর্ডারে তিন চেষ্টাতেই উত্তর আসেনি — UNKNOWN, FAILED নয়। রিপোর্ট দেখাল 17টিতে টাকা কেটেছে (late confirm), একটিতে কাটেনি; মোট চার্জ 1999, refund শূন্য। Reconciliation দুই নকশাতেই শেষ পাহারাদার, কিন্তু key থাকলে তার কাজ ছোট।

নিয়ম ৩ — দিনশেষে মিলিয়ে দেখা

Webhook হারায়, bug থাকে, gateway-ও ভুল করে। তাই প্রতিদিন (ভিড়ের দিনে ঘণ্টায় ঘণ্টায়) gateway-এর settlement report নিজের payment আর ledger-এর সাথে মেলানো — reconciliation, মুদি দোকানের খাতা আর ক্যাশবাক্স মেলানোর মতো:

আমাদের খাতায়Gateway-এর রিপোর্টেকাজ
SUCCEEDEDআছে, একই অঙ্কমিলেছে — কিছু নয়
UNKNOWN বা FAILEDআছেhold বৈধ থাকলে confirm, নইলে refund
SUCCEEDEDনেইপরের রিপোর্ট পর্যন্ত অপেক্ষা, তারপর তদন্ত — টিকিট আটকে রাখার নীতি আগে ঠিক করা
একটি অর্ডারদুটি চার্জএকটি refund
অঙ্ক বা fee আলাদা—তদন্ত আর মানুষের হাতে; alert

আর নিশ্চিতকরণের খবর কখনো transaction-এর ভেতরে SMS পাঠিয়ে নয়: সিট booked, অর্ডার CONFIRMED আর outbox-এর BookingConfirmed একসাথে commit, তারপর relay পাঠায় — পর্ব ২৫-এর outbox. SMS, push আর email-এর নির্ভরযোগ্য পথ, retry আর duplicate-রোধ পর্ব ৫৭-এর notification system-এ।

৮ · Deep dive ৪ — ledger: প্রতিটি টাকার দুই দিক

“গতকাল কত টাকা এল, কার কাছে কত পাওনা, কত ফেরত গেল?” — এই প্রশ্নের উত্তর একটি balance কলাম দিতে পারে না, কারণ কলামটি শুধু শেষ অবস্থা জানে। মুদি দোকানি খাতায় প্রতিটি বাকি আর শোধ আলাদা লাইনে লেখেন, কখনো কেটে মোছেন না; মাসশেষের হিসাব লাইনগুলো যোগ করে। Ledger ঠিক তাই, আর double-entry তাতে একটি কঠোর নিয়ম যোগ করে: প্রতিটি লেনদেন অন্তত দুটি হিসাবে লেখা হয়, একদিকে debit, অন্যদিকে সমান credit — লাইনগুলোর যোগফল শূন্য। ধারণাটির event sourcing-এর সাথে সম্পর্ক পর্ব ৩৫-এর ledger অংশে; এখানে প্রয়োগ।

লেনদেনDebit (+)Credit (−)
Booking ৳১,৭০০, কমিশন ১০%gateway_clearing ১,৭০০ (gateway আমাদের দেবে)operator_payable ১,৫৩০ (অপারেটরকে দেব) · platform_revenue ১৭০
Gateway-এর fee ৳৩০gateway_fees ৩০ (খরচ)gateway_clearing ৩০
পুরো refundoperator_payable ১,৫৩০ · platform_revenue ১৭০gateway_clearing ১,৭০০
অপারেটরকে পরিশোধoperator_payablebank

নিয়মগুলো: এক, প্রতিটি লেনদেনের লাইনের যোগফল শূন্য, নইলে লেখাই হয় না। দুই, entry শুধু যোগ হয় — append-only; ভুল শোধরায় উল্টো entry — পূর্ণ audit trail. তিন, balance গোনা হয় লাইন যোগ করে; আলাদা balance সারি রাখলে তা একই transaction-এ। চার, লেনদেনের id UNIQUE — retry-তে দ্বিতীয়বার লেখা আটকায়। পাঁচ, টাকা পয়সায়, পূর্ণসংখ্যায়, আর কমিশনের ভাগে গোলাকারের লেখা নিয়ম। নিচের ledger একটি booking, তার fee, একটি দ্বিতীয় booking (retry-সহ), প্রথমটির refund আর একটি ভুল payout লেখার চেষ্টা করে:

// Double-entry ledger: append-only lines in paisa (integers), every transaction sums to zero.
const ledger = [], posted = new Set();
const tk = (taka) => Math.round(taka * 100);             // money as integer paisa, never floats
function post(txn, lines) {
  const sum = lines.reduce((s, [, amt]) => s + amt, 0);
  if (posted.has(txn)) return console.log(txn.padEnd(13) + "duplicate txn id, ignored");
  if (sum !== 0) return console.log(txn.padEnd(13) + "REJECTED: lines sum to " + sum + " paisa");
  posted.add(txn);
  lines.forEach(([acct, amt], i) => ledger.push(Object.freeze({ txn, line: i + 1, acct, amt })));
  console.log(txn.padEnd(13) + "posted " + lines.length + " lines");
}
// Debit = +, credit = -. Booking of 1,700 Tk; the platform keeps a 10% commission.
post("T1 book O1", [["gateway_clearing", tk(1700)], ["operator_payable", -tk(1530)], ["platform_revenue", -tk(170)]]);
post("T2 fee O1", [["gateway_fees", tk(30)], ["gateway_clearing", -tk(30)]]);
post("T3 book O2", [["gateway_clearing", tk(850)], ["operator_payable", -tk(765)], ["platform_revenue", -tk(85)]]);
post("T3 book O2", [["gateway_clearing", tk(850)], ["operator_payable", -tk(765)], ["platform_revenue", -tk(85)]]);
post("T4 refund O1", [["operator_payable", tk(1530)], ["platform_revenue", tk(170)], ["gateway_clearing", -tk(1700)]]);
post("T5 payout", [["operator_payable", tk(765)], ["bank", -tk(750)]]);       // a typo: 750 instead of 765
function report(title) {
  const bal = {}, byTxn = {};                              // balances are derived, never stored alone
  for (const e of ledger) { bal[e.acct] = (bal[e.acct] || 0) + e.amt; byTxn[e.txn] = (byTxn[e.txn] || 0) + e.amt; }
  console.log("\n" + title);
  for (const [a, v] of Object.entries(bal)) console.log("  " + a.padEnd(18) + (v / 100).toFixed(2).padStart(10) + " Tk");
  console.log("  sum of all lines  " + String(Object.values(bal).reduce((s, v) => s + v, 0)).padStart(10) + " paisa");
  const bad = Object.entries(byTxn).filter(([, s]) => s !== 0);
  console.log("  unbalanced txns: " + (bad.length ? bad.map(([t, s]) => t + " (" + s + ")").join(", ") : "none"));
}
report("trial balance");
ledger.push({ txn: "T3 book O2", line: 4, acct: "platform_revenue", amt: -tk(85) });  // a bug bypasses post()
report("after a buggy direct write");

আউটপুট:

T1 book O1   posted 3 lines
T2 fee O1    posted 2 lines
T3 book O2   posted 3 lines
T3 book O2   duplicate txn id, ignored
T4 refund O1 posted 3 lines
T5 payout    REJECTED: lines sum to 1500 paisa

trial balance
  gateway_clearing      820.00 Tk
  operator_payable     -765.00 Tk
  platform_revenue      -85.00 Tk
  gateway_fees           30.00 Tk
  sum of all lines           0 paisa
  unbalanced txns: none

after a buggy direct write
  gateway_clearing      820.00 Tk
  operator_payable     -765.00 Tk
  platform_revenue     -170.00 Tk
  gateway_fees           30.00 Tk
  sum of all lines       -8500 paisa
  unbalanced txns: T3 book O2 (-8500)

T3 দুবার এসেছে (retry), দ্বিতীয়টি id দেখে বাদ। T5-এ টাইপের ভুল — ৭৬৫-এর জায়গায় ৭৫০ — লাইনের যোগফল ১,৫০০ পয়সা, তাই লেখার আগেই প্রত্যাখ্যাত। Trial balance-এ চারটি হিসাব: gateway_clearing 820.00 — O2-র ৮৫০ থেকে O1-এর fee ৩০ বাদ, কারণ O1 পুরো ফেরত গেছে কিন্তু fee ফেরত আসেনি (refund-এ fee ফেরে কি না, gateway-এর চুক্তিভেদে আলাদা); operator_payable -765.00 — অপারেটরকে O2-র টাকা দিতে হবে (ঋণাত্মক মানে credit-এর দিকে, অর্থাৎ পাওনাদার); platform_revenue -85.00 — O2-র কমিশন; gateway_fees 30.00 — খরচ। যোগফল 0. এরপর একটি bug post() এড়িয়ে সরাসরি একটি লাইন লিখল: মোট হলো -8500 পয়সা, আর লেনদেন-ধরে যোগ করলে দোষী ধরা পড়ে — T3 book O2 (-8500). বাস্তবে এই যাচাই চলে প্রতি রাতে, আর শূন্য না হলেই alert.

CREATE TABLE ledger_entries (
  txn_id       TEXT    NOT NULL,          -- e.g. 'P1-capture', idempotent
  line         INTEGER NOT NULL,
  account      TEXT    NOT NULL,
  amount_paisa BIGINT  NOT NULL CHECK (amount_paisa <> 0),
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (txn_id, line)
);
-- App role: INSERT and SELECT only. No UPDATE, no DELETE.
-- Nightly check: SELECT txn_id FROM ledger_entries
--   GROUP BY txn_id HAVING SUM(amount_paisa) <> 0;

লাইনের যোগফল-শূন্য নিয়ম এক বাক্যে ডেটাবেস পুরোপুরি চাপিয়ে দিতে পারে না (একটি লেনদেনের লাইনগুলো আলাদা সারি), তাই লেখা হয় একটি ফাংশন দিয়ে, যা আগে যোগফল যাচাই করে সব লাইন এক transaction-এ লেখে; আর রাতের যাচাই ধরে যা ফাংশন এড়িয়ে ঢুকেছে। একটি সমস্যা লুকিয়ে আছে: platform_revenue প্রায় প্রতিটি booking-এ বদলায় — একটি hot account. তার balance সারি প্রতিবার হালনাগাদ করলে সেটি অংশ ৫-এর মতোই hot row. প্রতিকার ভাবনার প্রশ্ন ৫-এ।

৯ · ভাঙন, trade-off আর বিস্তার — কী মরলে কী হয়

যা ভাঙেফলপ্রতিরক্ষা
Waiting room বা edgeসবাই সরাসরি API-তেadmitted token বাধ্যতামূলক, না থাকলে দ্রুত 503 আর Retry-After; দ্বিতীয় CDN
একটি seat DB shard-এর primaryসেই shows-এর বিক্রি কয়েক সেকেন্ড থামেsynchronous replica-য় failover, RPO প্রায় শূন্য (পর্ব ২২, ৫১); hold ডেটাবেসে, তাই হারায় না; ওই shard-এর ভর্তি সাময়িক থামানো
Seat-map cacheসব পড়া ডেটাবেসেcoalescing, replica, ভর্তির হার কমানো — সঠিকতা অক্ষত
Message broker বা relayনিশ্চিতকরণ দেরিতেoutbox-এ জমে থাকে, হারায় না; app অবস্থা সরাসরি দেখায়
Gateway ধীর বা বন্ধUNKNOWN বাড়েcircuit breaker (পর্ব ৪৮), বিকল্প পরিশোধ-পথ, ভর্তির হার কমানো
Webhook হারানোসফল payment-এর খবর নেইএকই key-তে status খোঁজ, আর দিনশেষের reconciliation

১০ গুণ ভিড়ে কোথায় আটকাবে

  • একটি বিশাল show-এর shard: সেকশনভিত্তিক ভাগ, single-writer, আর “সেরা খালি সিট” নিজে বেছে দেওয়া (ভাবনার প্রশ্ন ১)।
  • আসনহীন counter: আরও bucket; প্রতিটি bucket-এর সীমা ১ ÷ h.
  • Gateway-এর নিজের সীমা: প্রদানকারীর চুক্তি থেকে, অনুমানে নয়; কয়েকটি gateway-তে ভাগ, ভর্তির হার তাতেও বাঁধা।
  • SMS আর waiting room-এর খোঁজ: লাখো নিশ্চিতকরণে queue আর অগ্রাধিকার (পর্ব ৫৭); poll-এর ব্যবধান বাড়ানো বা SSE (পর্ব ১০)।

নিরাপত্তা, নজরদারি আর খরচ

নিরাপত্তা (পর্ব ৫২): webhook-এর স্বাক্ষর আর সময়-ছাপ যাচাই; কার্ডের তথ্য নিজেরা না রেখে gateway-এর পাতা ব্যবহার; refund-এর ক্ষমতা অল্প কয়েকজনের, বড় অঙ্কে দুজনের অনুমোদন (least privilege); payment চেষ্টার rate limit; ledger-এ app-এর UPDATE-DELETE-এর অধিকারই নেই।

নজরদারি (পর্ব ৫০-এর alert) — যেগুলোতে সতর্কসংকেত বাজবে:

খরচ: বিক্রির সময় জানা, তাই আগে সার্ভার বাড়ানো, পরে কমানো (পর্ব ১৩); ভিড়ের বড় অংশ edge-এর সস্তা পাতায়; ডেটাবেসের আকার ঠিক হয় ভর্তির হারে, মোট ভিড়ে নয়।

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

“সিস্টেমটি দুই ভাগ: দেখার পথ cache আর CDN থেকে, একটু পুরোনো চলে; কেনার পথ কঠোর। ৬ লাখ মানুষ, ১.২ লাখ সিট — তাই edge-এ waiting room, lottery আর স্বাক্ষর-করা token, আর ডেটাবেসের p99 দেখে সেকেন্ডে কয়েকশো জন ভেতরে। প্রতিটি সিট একটি সারি, show_id-তে shard; hold হলো শর্তসহ UPDATE আর মেয়াদ, তাই double booking অসম্ভব। Payment-এ প্রতিটি কড়ায় idempotency key, timeout মানে UNKNOWN, একই key-তে খোঁজ, ব্যর্থ হলে সিট ছাড়া বা refund — saga, খবর outbox দিয়ে। টাকা double-entry ledger-এ, প্রতি রাতে trial balance আর reconciliation. বড় ঝুঁকি বিশাল show-এর hot shard আর gateway-এর ধীরতা — তার পরিকল্পনা আছে।”

আরও গভীরে: Patrick O’Neil-এর “The Escrow Transactional Method” (ACM TODS, ১৯৮৬) দেখায়, মজুতের মতো সংখ্যার ক্ষেত্রে পুরো সারিতে lock না নিয়ে “এতটুকু কমানোর অধিকার” আগেই সংরক্ষণ করে বহু লেনদেন একসাথে চালানো যায় — আসনহীন টিকিটের bucket-এর তাত্ত্বিক ভিত্তি। Pat Helland-এর “Life beyond Distributed Transactions” (CIDR ২০০৭) আর “Immutability Changes Everything” (CIDR ২০১৫) সীমানা পেরোনো কাজকে idempotent বার্তা আর পরিবর্তনহীন লেখায় ভাবতে বলে। Virtual waiting room আজ কয়েকটি CDN-সেবার প্রকাশিত পণ্য; খুঁটিনাটি সংস্করণভেদে আলাদা।

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

  • Double booking: দুর্বল — “Redis lock দেব”; শক্ত — “seat DB-তে শর্তসহ UPDATE, ০ সারি মানে আগেই নেওয়া; lock থাকলেও তা শুধু গতি বাড়ায়”।
  • ভিড়: দুর্বল — “autoscaling করব”; শক্ত — “ডেটাবেস autoscale হয় না; edge-এ waiting room, ভর্তির হার ডেটাবেসের সামর্থ্য থেকে হিসাব করা, বিক্রি শেষ হলে আগেই জানানো”।
  • Payment timeout: দুর্বল — “আবার চেষ্টা করব”; শক্ত — “UNKNOWN, একই key-তে খোঁজ, hold-এর সীমিত বাড়তি সময়, তারপর compensation আর দিনশেষে মিলিয়ে দেখা”।
  • টাকার হিসাব: দুর্বল — “balance কলাম আপডেট”; শক্ত — “double-entry, append-only, পয়সায় পূর্ণসংখ্যা, প্রতি লেনদেনে যোগফল শূন্য”।
  • Hold-এর মেয়াদের হিসাব: ভূতের hold = না-কেনা মানুষের hold-এর হার × মেয়াদ; সেকেন্ডে ২১৬টি আর ২ মিনিটে ২৫,৯২০টি সিট।

১০ · AI-এর যুগে — টিকিট আর payment-এর নকশা যাচাই

“একটি টিকিট বুকিং সিস্টেম ডিজাইন করুন” — এমন অনুরোধে AI সাধারণত দেয় microservices, Kafka, Redis lock আর Elasticsearch-এর তালিকা; কঠিন প্রশ্নগুলো — কোথায় শর্তসহ লেখা, timeout-এ কী, টাকার খাতা কেমন — প্রায়ই বাদ। চেনা ভুল:

AI-এর ভুলকেমন দেখায়কীভাবে যাচাই করবেন
Distributed lock-ই সঠিকতা“Redis SETNX দিয়ে সিট lock, তারপর booking insert”Lock-এর মেয়াদ ফুরালে কী হয়? শেষ লেখায় শর্ত বা UNIQUE আছে কি না (অংশ ৫)
Timeout = ব্যর্থ“payment fail হলে retry” — নতুন request, নতুন keyঅংশ ৭-এর simulator চালান: double charge গুনুন
টাকা float-এ, balance কলামেbalance FLOAT, refund মানে payment সারি মোছাপয়সায় পূর্ণসংখ্যা, append-only ledger, যোগফল শূন্য (অংশ ৮)
হিসাবের ভুল“১০ লাখ user, তাই ১০ লাখ QPS”; hold-এর মেয়াদের প্রভাব নেইনিজে হিসাব করুন: চূড়া, ভর্তির হার, ভূতের hold (অংশ ২)
“ACID দিয়ে strong consistency, তাই CAP-এর C”দুই consistency গুলিয়ে ফেলাACID-এর C নিয়ম রক্ষা, CAP-এর C সর্বশেষ মান (পর্ব ২৪, ৪০)
বানানো সীমা আর দাম“gateway সেকেন্ডে ১০,০০০ লেনদেন নেয়, fee ১%”প্রদানকারীর চুক্তি আর অফিশিয়াল ডকুমেন্টেশন
অতিরিক্ত নকশা“প্রতিটি সিটের জন্য আলাদা microservice”, gateway-এর সাথে 2PCসরলতর বিকল্প কী? Gateway “prepare” বোঝে না (পর্ব ২৫)

AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে খসড়া করুন, তারপর এভাবে যাচাই করান:

তুমি একজন অভিজ্ঞ backend আর payments reviewer. নিচে একটি টিকিট প্ল্যাটফর্মের (কাল্পনিক) নকশা: সিট বাছাই, hold-এর মেয়াদ, waiting room, বাইরের payment gateway আর MFS, ledger. [এখানে আপনার নকশা, টেবিল আর হিসাব পেস্ট করুন] ১. দুজন একই সিট একই মুহূর্তে চাইলে কোন লেখাটি double booking আটকায়, দেখাও; শর্ত ছাড়া lock বা cache-এর ওপর ভরসা থাকলে চিহ্নিত করো। ২. Payment-এর অবস্থা আর বদল টেবিলে লেখো; timeout-এ কী হয়, কোথায় idempotency key লাগে, বলো। ৩. Hold-এর মেয়াদে payment অজানা থাকলে সিট আর টাকার কী হবে, compensation-সহ দেখাও। ৪. আমার হিসাব (চূড়া, ভর্তির হার, ডেটাবেসের লেখা, ভূতের hold) সূত্রসহ আবার করো। ৫. প্রতিটি লেনদেনের ledger লাইন লেখো, যোগফল শূন্য কি না যাচাই করো। ৬. পণ্যের সীমা, fee বা ডিফল্টের উৎস দাও, নিশ্চিত না হলে বলো; নতুন প্রযুক্তির আগে সরলতর বিকল্প বলো।

AI-এর নকশা মানার আগে — চেকলিস্ট

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

ইন্টারভিউয়ার যেসব follow-up-এ চাপ দিতে পারেন — নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১একটি ৫০,০০০ আসনের স্টেডিয়ামের কনসার্ট — পুরো show একটি shard-এ, আর প্রথম মিনিটে লাখো মানুষ সামনের সারির একই কয়েকশো সিট চান। কীভাবে scale করবেন?

এখানে দুটি আলাদা সমস্যা জড়িয়ে আছে। এক, shard-এর লেখার সামর্থ্য। show_id partition key হাজারো বাস-ট্রিপের জন্য চমৎকার, কিন্তু একটি বিশাল show-এর সব লেখা একটি primary-তে যায় — সেই যন্ত্রের সামর্থ্যই সীমা। দুই, সংঘাতের ঘনত্ব। সবাই একই seat map দেখে একই “সেরা” সিট বাছেন, তাই প্রতি-সিট সারি থাকলেও লেখার বড় অংশ ০ সারি পায় — অংশ ৫-এর simulator-এর ব্যর্থ লেখাগুলোর মতো, কিন্তু আরও অনেক বেশি; প্রতিটি ব্যর্থতা একটি বাড়তি round trip আর একটি হতাশ ক্রেতা।

প্রতিকার কয়েক স্তরে:

  • ভর্তির হার এই shard-এর জন্য আলাদা: waiting room প্রতি show-এর সামর্থ্য জানে; বিশাল show-এর সারি আলাদা আর ধীর।
  • সেকশনে ভাগ: এক অর্ডার এক সেকশনে সীমিত করে key (show_id, section) — প্রতিটি সেকশন আলাদা ভাগে, আলাদা যন্ত্রে।
  • Best-available বরাদ্দ: ক্রেতা বলেন “সেকশন B, ২টি সিট”, সার্ভার নিজে পাশাপাশি সেরা খালি সিট দেয়। প্রতিটি সেকশনের একটি worker queue থেকে অনুরোধ নিয়ে ক্রমানুসারে বরাদ্দ করে আর এক transaction-এ কয়েকশো hold লেখে — single-writer, সংঘাত শূন্য।
  • আলাদা cell: জানা বড় অনুষ্ঠান আগে থেকে নিজস্ব ডেটাবেস আর সেবার cell-এ (পর্ব ৪৮-এর bulkhead-এর ধারণা), যাতে অন্য বিক্রি আক্রান্ত না হয়।
  • আগে থেকে প্রস্তুতি: সময় জানা, তাই cache গরম করা, load test আর change freeze.

Trade-off স্পষ্ট: নিজে সিট বাছার স্বাধীনতা কমে, throughput বাড়ে। অনেক টিকিট-সেবা জনপ্রিয় বিক্রিতে তাই বাছাই সীমিত করে বা বরাদ্দ নিজে করে — এটি প্রযুক্তির নয়, পণ্যের সিদ্ধান্ত, আর ইন্টারভিউতে সেটি বলাই শক্ত উত্তর।

মূল উপলব্ধি: hot shard-এর সমাধান শুধু আরও বড় যন্ত্র নয় — সংঘাতের একক ছোট করা, সিদ্ধান্ত এক জায়গায় ক্রমানুসারে নেওয়া, আর দরকারে পণ্যের নিয়ম বদলানো।

প্র ০২Hold-এর মেয়াদ কত হবে — ১০ মিনিট, না ২ মিনিট? আর payment চলার সময় মেয়াদ শেষ হলে কী করবেন?

অংশ ২-এর হিসাব দুই দিকের দাম দেখায়। মেয়াদ লম্বা হলে যাঁরা কিনবেন না, তাঁদের hold অনেকক্ষণ সিট আটকে রাখে: সেকেন্ডে ২১৬টি হারে ৫ বা ১০ মিনিটে ৩৬,০০০টি সিট, অর্থাৎ ৩০%, পর্দায় “বিক্রি শেষ” দেখায়, অথচ পরে ফেরে — তখন লাইনের মানুষ হয়তো চলে গেছেন, আর ফিরে আসা সিট bot-রা ধরে ফেলে। মেয়াদ ছোট হলে প্রকৃত ক্রেতা মাঝপথে সিট হারান — MFS-এ অ্যাপ বদলানো, PIN, কখনো OTP, দুর্বল সংযোগে কয়েক মিনিট লাগতেই পারে।

তাই একটি সংখ্যা নয়, দুই ধাপের নীতি:

  • বাছাইয়ের মেয়াদ ছোট: সিট বেছে অর্ডার করা পর্যন্ত, ধরা যাক ৫ মিনিট; পর্দায় countdown, আর “ফিরে যান” চাপলে সঙ্গে সঙ্গে ছাড়া।
  • Payment শুরু হলে একবার বাড়তি সময়: একটি সীমা পর্যন্ত, যা মাপা হয় প্রকৃত ক্রেতাদের payment-সময়ের p95 বা p99 থেকে — অনুমানে নয়।
  • সীমার পরে নীতি লেখা থাকে: সিট ছাড়া, আর দেরিতে সফল হলে স্বয়ংক্রিয় refund (অংশ ৭-এর stepper); অথবা সিট তখনো খালি থাকলে আবার hold করে confirm.
  • অপব্যবহার রোধ: এক account-এ একটিই সক্রিয় hold — নইলে কেউ বারবার hold করে সিট আটকে রাখতে পারেন।

রেলস্টেশনের কাউন্টারেও এমন নিয়ম পরিচিত: ফর্ম পূরণের সময় সিট আটকে রাখা যায় না, টাকা গোনার সময় যায়। Hold-এর মেয়াদ তাই প্রকৌশল আর ব্যবসার যৌথ সিদ্ধান্ত। চালুর পরে মাপুন: মেয়াদোত্তীর্ণ hold-এর অনুপাত, payment-এর সময়ের বণ্টন, আর “সিট হারিয়েছি” অভিযোগ।

মূল উপলব্ধি: মেয়াদ একটি টানাপোড়েন — ভূতের hold বনাম প্রকৃত ক্রেতার ক্ষতি; উত্তর আসে মাপা বণ্টন আর দুই ধাপের নীতি থেকে, একটি জাদুর সংখ্যা থেকে নয়।

প্র ০৩Redis-এ SET NX দিয়ে সিট lock করলে সমস্যা কী? কখন এটি গ্রহণযোগ্য?

Redis-এর SET key value NX PX ttl একটি সুন্দর, দ্রুত “আমি আগে” চিহ্ন। সমস্যা হয় যখন একে সঠিকতার একমাত্র পাহারাদার বানানো হয়, আর ডেটাবেসের লেখা শর্তহীন থাকে।

  • মেয়াদ ফুরানো থেমে থাকা process-এর হাতে: process A lock নিল, তারপর garbage collection বা network-এর কারণে কয়েক সেকেন্ড থেমে রইল; মেয়াদ শেষ, B একই lock নিল; A জেগে উঠে জানে না যে lock আর তার নয় — দুজনেই ডেটাবেসে লেখে। পর্ব ৪৩-এর fencing token এজন্যই।
  • Failover-এ হারানো: Redis-এর replication সাধারণত asynchronous; primary lock লিখে replica-য় পৌঁছানোর আগে মরলে নতুন primary-তে lock নেই।
  • Redlock বিতর্ক: ২০১৬ সালে Martin Kleppmann যুক্তি দেন যে বহু-node Redlock-ও ঘড়ি আর বিরতির অনুমানে নির্ভর করে, তাই সঠিকতার জন্য যথেষ্ট নয়; Redis-এর স্রষ্টা Salvatore Sanfilippo পাল্টা যুক্তি দেন। বিতর্কের ব্যবহারিক শিক্ষা দুই পক্ষই মানেন: সঠিকতা চাইলে সুরক্ষিত resource-কেই শেষ যাচাই করতে হবে।

আর শেষ যাচাই — WHERE status = 'available' বা UNIQUE — থাকলেই সঠিকতা আসে; তখন Redis lock শুধু optimization: ডেটাবেসে পৌঁছানোর আগেই অনেক নিশ্চিত-ব্যর্থ লেখা ছেঁকে ফেলা। সেটি কাজের, কিন্তু একটি বাড়তি ব্যবস্থা, বাড়তি ব্যর্থতার ধরন আর দুই জায়গার অমিল সামলানোর খরচ নিয়ে আসে।

কখন গ্রহণযোগ্য: (ক) ছাঁকনি হিসেবে, শর্তসহ লেখার সামনে; (খ) আসনহীন counter-এর দ্রুত আনুমানিক গোনা, ডেটাবেসের সাথে নিয়মিত মিলিয়ে; (গ) যদি Redis নিজেই সত্যের উৎস হয় — টেকসই লেখা, replica-তে নিশ্চিতকরণসহ — তখন সে আর cache নয়, একটি ডেটাবেস, আর তার নিশ্চয়তা সেভাবেই যাচাই করতে হয়।

ইন্টারভিউতে তাই আগে শর্তসহ লেখার কথা বলুন, তারপর দরকার হলে Redis-কে সামনের ছাঁকনি হিসেবে আনুন।

মূল উপলব্ধি: lock একটি পরামর্শ; সঠিকতা আসে সত্যের উৎসে শর্তসহ লেখা থেকে। “কে জিতল” সিদ্ধান্তটি যেখানে ডেটা থাকে, সেখানেই নিন।

প্র ০৪Waiting room কি ন্যায্য? আগে-এলে-আগে, lottery, না অন্য কিছু — আর bot-রা কীভাবে ফাঁকি দিতে চায়?

“আগে এলে আগে” শুনতে ন্যায্য, কিন্তু অনলাইনে “আগে” মানে প্রায়ই মিলিসেকেন্ডের দৌড়: কার সংযোগ দ্রুত, কে server-এর কাছে, কার script খোলার মুহূর্তে হাজারবার চাপে। তাতে সাধারণ মানুষ, বিশেষ করে দুর্বল মোবাইল নেটওয়ার্কের মানুষ, পিছিয়ে পড়েন। তাই অনেক waiting room একটি মিশ্র নিয়ম নেয়: খোলার আগের একটি সময়-জানালায় যাঁরা আসেন, তাঁদের মধ্যে lottery; তারপর আসার ক্রমে। এতে আগেভাগে এসে ঘণ্টার পর ঘণ্টা reload করার প্রয়োজনও থাকে না।

ন্যায্যতা শুধু ক্রম নয়:

  • মাথাপিছু সীমা: অর্ডারে সর্বোচ্চ সিট, এক account-এ এক hold; সবচেয়ে সস্তা আর কার্যকর।
  • এক মানুষ, এক জায়গা: token session-এ বাঁধা, তাই অনেক tab খুললে সুবিধা নেই; অনেক account-এর বিরুদ্ধে ফোন-যাচাই আর আচরণ দেখা।
  • স্বচ্ছতা: নিজের নম্বর আর আনুমানিক সময় দেখানো; সিট ফুরালে আগেভাগে জানানো — অংশ ২-এ দেখেছেন, প্রায় ৫ লাখ মানুষ কোনো সিট পাবেন না, তাঁদের অপেক্ষায় রাখা নিষ্ঠুর আর ব্যয়বহুল।
  • আগে নিবন্ধন: খুব বড় অনুষ্ঠানে আগে নিবন্ধন আর যাচাই, তারপর এলোমেলো সময়ে প্রবেশের সুযোগ — সার্ভারের চাপও ছড়ায়।

বড় অনুষ্ঠানের টিকিট-বিক্রিতে ভিড়ে সাইট ভেঙে পড়া বা bot-এর দখলের খবর বিভিন্ন দেশে বারবার সংবাদে এসেছে; প্রতিক্রিয়ায় কোথাও কোথাও পুনর্বিক্রি নিয়ে আইনও হয়েছে। খুঁটিনাটি দেশ আর ঘটনাভেদে আলাদা — মূল শিক্ষা হলো, ভিড় আর ন্যায্যতা একসাথে নকশা করতে হয়। প্রকৌশলীর কাজ নিয়মটি প্রয়োগযোগ্য করা: token যাচাই, সীমা, rate limit আর নজরদারি, সবই সুরক্ষার দিক থেকে।

মূল উপলব্ধি: ন্যায্যতা একটি পণ্যের সিদ্ধান্ত; সিস্টেমের কাজ সেটি চাপিয়ে দেওয়া — এমনভাবে যে দ্রুততম script নয়, নিয়মই জেতে।

প্র ০৫Ledger-এ balance কেন গোনা হয়, রাখা হয় না? লাখো লেনদেনে SUM কি ধীর হবে না — আর hot account-এর কী হবে?

শুধু একটি balance কলাম থাকলে তিনটি প্রশ্নের উত্তর নেই: টাকাটা কোথা থেকে এল, কবে বদলাল, আর কে বদলাল। একটি bug একবার ভুল UPDATE করলে আগের মান চিরতরে হারায়। Entry-গুলো থাকলে balance যেকোনো সময় আবার গোনা যায়, যেকোনো দিনের অবস্থা ফিরিয়ে দেখা যায়, আর trial balance দিয়ে ভুল ধরা যায়। হিসাববিজ্ঞানে নীতিটি বহু শতাব্দী পুরোনো — Luca Pacioli ১৪৯৪ সালের বইয়ে double-entry পদ্ধতির লিখিত বর্ণনা দেন — আর Pat Helland-এর একটি লেখার শিরোনামই “Accountants Don’t Use Erasers”.

তবে প্রতিবার সব লাইন SUM করা সত্যিই ধীর। বাস্তব নকশা তাই মিশ্র:

  • Snapshot আর তারপরের লাইন: প্রতি রাতে প্রতিটি হিসাবের balance-এর একটি snapshot, আর দেখানোর সময় snapshot + তার পরের লাইন — পর্ব ৩৫-এর snapshot-এর মতো।
  • একই transaction-এ balance সারি: গ্রাহকের wallet-এর মতো হিসাবে, যেখানে “ঋণাত্মক নয়” যাচাই সঙ্গে সঙ্গে দরকার, entry লেখার সাথেই শর্তসহ UPDATE: SET balance = balance - x WHERE balance >= x. Entry-ই সত্য, সারিটি তার দ্রুত প্রতিফলন।
  • Hot account ভাগ: platform_revenue-এর মতো হিসাব প্রায় প্রতিটি booking-এ বদলায়। প্রতিবার তার balance সারি বদলালে lock-এর লাইন — h = ৫ ms হলে সর্বোচ্চ সেকেন্ডে ২০০টি booking. সমাধান: এমন অভ্যন্তরীণ হিসাবে running balance না রেখে শুধু entry লেখা, আর রিপোর্টে যোগ করা; অথবা হিসাবটিকে ১৬টি উপ-হিসাবে ভাগ করে প্রতিটি লেনদেন একটিতে লেখা — অংশ ৫-এর bucket-এর মতো।

আরও তিনটি অভ্যাস: লেনদেনের id UNIQUE, তাই retry-তে দ্বিতীয় লাইন ঢোকে না; entry-তে মুদ্রা আর পয়সায় অঙ্ক; আর কমিশন ভাগে গোলাকারের নির্দিষ্ট নিয়ম, যাতে ভাগের যোগ মূল অঙ্কের সমান থাকে।

মূল উপলব্ধি: entry সত্য, balance তার হিসাব। গতি আসে snapshot, শর্তসহ balance সারি আর hot হিসাব ভাগ করে — কিন্তু কখনো ইতিহাস মুছে নয়।

অনুশীলন

আগে কাগজে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. হিসাব: একটি ছোট শহরের বাস-টিকিট বিক্রিতে (কাল্পনিক) ধরা যাক S = ৬০,০০০ সিট, ভর্তির হার A = সেকেন্ডে ২৫০ জন, ঢোকা মানুষের ৬০% কেনেন, প্রতি অর্ডারে ২টি সিট, hold-এর মেয়াদ ৩ মিনিট; ডেটাবেসে লেখা অংশ ২-এর মতো (hold-চেষ্টা ১.৩, ক্রেতাপ্রতি আরও ৯, না-কেনা মানুষের ১)। বের করুন: (ক) সেকেন্ডে কতটি সিট ক্রেতারা আর কতটি না-কেনা মানুষ ধরেন; (খ) প্রথম “বিক্রি শেষ” কখন, আর তখন কত সিট ভূতের hold-এ; (গ) ডেটাবেসে সেকেন্ডে কত লেখা; (ঘ) সব সিট বেচতে কতজনকে ঢোকাতে হবে; (ঙ) মেয়াদ ১ মিনিট হলে।
    • (ক) ক্রেতা: ২৫০ × ০.৬ × ২ = ৩০০; না-কেনা: ২৫০ × ০.৪ × ২ = ২০০ সিট/সেকেন্ড।
    • (খ) সব সিট আটকাতে লাগে ৬০,০০০ ÷ (৩০০ + ২০০) = ১২০ সেকেন্ড; মেয়াদ ১৮০ সেকেন্ড, তাই তখনো কোনো hold ফেরেনি — ভূতের hold ২০০ × ১২০ = ২৪,০০০টি (৪০%), ফিরতে শুরু করবে ১৮০ সেকেন্ডে।
    • (গ) ২৫০ × (১.৩ + ০.৬ × ৯ + ০.৪ × ১) = ২৫০ × ৭.১ = ১,৭৭৫ লেখা/সেকেন্ড।
    • (ঘ) ৬০,০০০ ÷ (০.৬ × ২) = ৫০,০০০ জন।
    • (ঙ) মেয়াদ ৬০ সেকেন্ড < ১২০, তাই ভূতের hold স্থির থাকে ২০০ × ৬০ = ১২,০০০টিতে (২০%); প্রথম “বিক্রি শেষ” (৬০,০০০ − ১২,০০০) ÷ ৩০০ = ১৬০ সেকেন্ডে।

    অংশ ২-এর calculator-এ S = 60000, A = 250, p = 0.6, k = 2 আর মেয়াদ [1, 3] দিলে একই সংখ্যা: 1,775 writes/s, 300 by payers + 200 by leavers, 50,000, আর 12,000 20.0% 160.0 s ও 24,000 40.0% 120.0 s.

  2. Requirement বদলালে নতুন নকশা: সিটঘর এবার একটি ক্রিকেট ম্যাচের গ্যালারির টিকিট বেচবে (কাল্পনিক) — কোনো সিট নম্বর নেই, প্রতিটি গ্যালারিতে ১০,০০০ টিকিট, এক অর্ডারে সর্বোচ্চ ২টি। ধরা যাক প্রথম মিনিটে একটি গ্যালারিতে সেকেন্ডে ৩,০০০ টিকিটের চাহিদা, আর একটি counter সারির প্রতিটি লেখা lock ধরে ৪ ms. Inventory, hold, cache আর waiting room-এ কী বদলাবে?
    • Inventory: সিটের সারির বদলে counter. একটি সারির সীমা ১ ÷ ০.০০৪ = সেকেন্ডে ২৫০ লেখা, তাই ৩,০০০ ÷ ২৫০ = অন্তত ১২টি bucket, যেমন প্রতিটিতে প্রায় ৮৩৪টি; প্রতিটির CHECK (remaining >= 0), কমানো WHERE remaining >= n দিয়ে; একটি খালি হলে পাশেরটি। মাথা-গোনা চাপ বাড়লে আরও bucket, বা single-writer worker যে batch-এ লেখে।
    • Hold: holds সারি (hold_id, bucket, n, held_until); bucket থেকে n কমানো আর hold-এর সারি এক transaction-এ। মেয়াদ গেলে বা বাতিলে n ফেরত একই bucket-এ, শর্তসহ (hold তখনো held হলেই), যাতে দুবার ফেরত না যায়।
    • Cache: seat map লাগে না; শুধু “আনুমানিক বাকি” — bucket-গুলোর যোগ, কয়েক সেকেন্ড পুরোনো।
    • Waiting room: একই যন্ত্র; ভর্তির হার bucket-গুলোর মোট সামর্থ্য (১২ × ২৫০ = ৩,০০০ লেখা/সেকেন্ড) থেকে, headroom রেখে।
    • যা বদলায় না: idempotency key, payment-এর state machine, saga, ledger — সবই একই।
  3. ত্রুটিপূর্ণ নকশার সমালোচনা: একজন সহকর্মী লিখেছেন: (ক) app Redis-এর seat map-এ সিট খালি দেখলে SET seat:7:A3 user NX, তারপর MySQL-এ শর্তহীন INSERT INTO bookings; (খ) payment-এর সময় সিটের সারিতে SELECT … FOR UPDATE ধরে রেখে transaction-এর ভেতরেই gateway ডাকা; (গ) gateway timeout হলে “ব্যর্থ” দেখিয়ে নতুন payment অনুরোধ, নতুন key; (ঘ) UPDATE accounts SET balance = balance + 17.0 (FLOAT), আর refund মানে payment-এর সারি মুছে ফেলা; (ঙ) commit-এর আগে SMS পাঠানো। প্রতিটির ক্ষতি আর সংশোধন লিখুন।
    • (ক) Lock-এর মেয়াদ নেই — crash করলে সিট চিরকাল আটকে; মেয়াদ দিলে থেমে থাকা process-এর হাতে দুই মালিক; শর্তহীন insert-এ double booking. সংশোধন: seat DB-তে UPDATE … WHERE status = 'available' বা UNIQUE (show_id, seat); Redis শুধু ছাঁকনি বা cache.
    • (খ) Gateway-এর পুরো সময় lock ধরা — সারির throughput ১ ÷ h-এ ধসে, connection pool ফুরায় (পর্ব ২৪)। সংশোধন: hold-এর অবস্থা আর মেয়াদ, ছোট transaction, gateway-এর call বাইরে।
    • (গ) Timeout মানে অজানা; নতুন key-তে দ্বিতীয় চার্জ — অংশ ৭-এর simulator-এ key ছাড়া ১৬৪টি অর্ডারে দুবার চার্জ হয়েছে। সংশোধন: UNKNOWN অবস্থা, একই key-তে খোঁজ, reconciliation.
    • (ঘ) FLOAT-এ গোলাকারের ভুল জমে; balance কলামে ইতিহাস নেই; সারি মুছলে audit অসম্ভব। সংশোধন: পয়সায় পূর্ণসংখ্যা, double-entry ledger, refund একটি নতুন উল্টো লেনদেন।
    • (ঙ) Commit ব্যর্থ হলে গ্রাহক “নিশ্চিত” SMS পান অথচ টিকিট নেই — dual write. সংশোধন: outbox-এ event একই transaction-এ, relay পরে পাঠায়, consumer event_id দিয়ে duplicate বাদ দেয়।
  4. আউটপুট অনুমান করুন: অংশ ৮-এর ledger-এ T5-এর ভুলটি শুধরে ["bank", -tk(750)]-এর জায়গায় ["bank", -tk(765)] লেখা হলো। চালানোর আগে লিখুন: T5-এর লাইনে কী আসবে, trial balance-এ কয়টি হিসাব আর কোনটির মান কত, আর শেষের “buggy direct write”-এর অংশে কী বদলাবে। তারপর চালিয়ে মিলিয়ে দেখুন।
    T5 payout    posted 2 lines
    
    trial balance
      gateway_clearing      820.00 Tk
      operator_payable        0.00 Tk
      platform_revenue      -85.00 Tk
      gateway_fees           30.00 Tk
      bank                 -765.00 Tk
      sum of all lines           0 paisa
      unbalanced txns: none

    T5-এর যোগফল এখন শূন্য, তাই লেখা হলো, আর একটি নতুন হিসাব bank যুক্ত হলো — মোট পাঁচটি। অপারেটরের পাওনা ৭৬৫ শোধ হয়ে operator_payable শূন্য, আর bank ঋণাত্মক ৭৬৫ — এই উদাহরণে gateway-এর টাকা ব্যাংকে ঢোকার (settlement) entry লেখাই হয়নি; বাস্তবে সেটি bank-এ debit, gateway_clearing-এ credit. যোগফল তবু 0. শেষ অংশে একই bug-এর লাইন, তাই আগের মতোই platform_revenue -170.00, sum of all lines -8500 আর unbalanced txns: T3 book O2 (-8500) — বাকি হিসাবগুলো উপরের মতো, bank -765.00-সহ। শিক্ষা: সঠিক লেখা যোগফল শূন্য রাখে, আর পথ এড়ানো লেখা লেনদেন-ধরে যোগ করলেই ধরা পড়ে।

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

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

ক্রমএকটি নির্ভরযোগ্য টিকিট কেনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Waiting room থেকে স্বাক্ষর-করা admitted token পাওয়া
  2. Seat DB-তে শর্তসহ UPDATE দিয়ে সিট hold, মেয়াদসহ
  3. Idempotency key-সহ অর্ডার তৈরি, অবস্থা PENDING_PAYMENT
  4. একই অর্ডার থেকে বানানো key দিয়ে gateway-তে charge
  5. Payment SUCCEEDED আর ledger-এর লাইন একই transaction-এ
  6. সিট booked, অর্ডার CONFIRMED আর outbox-এ event; তারপর SMS

দুটি সার্ভার একই মুহূর্তে চালাল: UPDATE seats SET status = 'held', hold_id = ? WHERE show_id = 7 AND seat = 'A3' AND status = 'available'. সিট A3 আগে খালি ছিল। কী হবে?

  • দুটিই ১টি সারি বদলাবে, আর A3 দুজনের নামে
  • একটি ১টি সারি বদলাবে, অন্যটি ০টি — দ্বিতীয়জন অন্য সিট বাছবেন
  • দুটিই deadlock-এ আটকে যাবে
  • SERIALIZABLE না হলে ফল অনিশ্চিত

Gateway-তে চার্জের অনুরোধ পাঠানোর পর ৩০ সেকেন্ডে কোনো উত্তর এল না। সবচেয়ে নিরাপদ পরের কাজ কোনটি?

  • Payment-কে UNKNOWN রেখে একই idempotency key দিয়ে অবস্থা জানতে চাওয়া
  • নতুন idempotency key দিয়ে আবার চার্জ পাঠানো
  • Payment-কে FAILED ধরে সঙ্গে সঙ্গে সিট ছেড়ে দেওয়া
  • গ্রাহককে আবার “পরিশোধ” চাপতে বলা

একটি বিক্রিতে প্রতি সেকেন্ডে গড়ে ২০০টি সিট এমন মানুষ hold করেন, যাঁরা শেষে কেনেন না; hold-এর মেয়াদ ৩ মিনিট, আর বিক্রি দীর্ঘক্ষণ চলে বলে অবস্থা স্থির। গড়ে কতটি সিট এই না-কেনা মানুষদের hold-এ আটকে থাকে?

  • ১২,০০০
  • ৩৬,০০০
  • ৬০০
  • ১,২০,০০০

Double-entry ledger-এ ৳১,০০০-এর একটি টিকিট বিক্রি লেখা হবে; প্ল্যাটফর্মের কমিশন ১০%, বাকিটা অপারেটরের পাওনা, আর টাকা আগে আসে gateway-এর কাছে। Debit = +, credit = − ধরলে কোন লাইনগুলো সঠিক?

  • gateway_clearing +১,০০০ · operator_payable +৯০০ · platform_revenue +১০০
  • platform_revenue −১০০
  • gateway_clearing +১,০০০ · operator_payable −১,০০০
  • gateway_clearing +১,০০০ · operator_payable −৯০০ · platform_revenue −১০০

একটি গ্যালারির আসনহীন টিকিটের counter একটি সারিতে; প্রতিটি কমানো সারিটিতে lock ধরে ৪ ms. প্রথম মিনিটে চাহিদা সেকেন্ডে ২,০০০ টিকিট-অনুরোধ। Counter-কে সমান ভাগে ভাগ করলে অন্তত কয়টি ভাগ লাগবে?

  • ২
  • ৪
  • ৮
  • ৫০০

একটি টিকিট সেবার seat map cache-এ ১ সেকেন্ড পুরোনো তথ্য থাকে। Cache দেখাল সিট B4 খালি, অথচ ডেটাবেসে B4 ইতিমধ্যে অন্য কারও hold-এ। ক্রেতা B4 চাইলে সঠিক নকশায় কী ঘটে?

  • Cache খালি দেখিয়েছে, তাই B4 তাঁকে বিক্রি হয়
  • Hold-এর শর্তসহ লেখা ০ সারি পায়; 409 আর নতুন seat map
  • পুরো cache মুছে দিতে হয়, নইলে double booking
  • Seat map কখনো cache করা যায় না

শব্দকোষ

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

মনে রাখুন

  1. দুই জগৎ: দেখার পথ cache আর CDN থেকে, একটু পুরোনো চলে; কেনার পথে প্রতিটি সিট আর টাকার একটিই সত্য।
  2. সিট: প্রতিটি একটি সারি, show_id-তে shard; hold = শর্তসহ UPDATE + মেয়াদ, মেয়াদের যাচাই শর্তেই; সংঘাতের একক সিট, show নয়; আসনহীন counter ভাগ করা, প্রতিটি ভাগ ≤ ১ ÷ h.
  3. ভিড়: edge-এ waiting room, lottery আর স্বাক্ষর-করা token; ভর্তির হার ডেটাবেসের সামর্থ্য থেকে; ভূতের hold = না-কেনা হার × মেয়াদ; সিট ফুরালে আগেভাগে জানানো।
  4. Payment: প্রতিটি কড়ায় idempotency key; timeout = UNKNOWN; একই key-তে খোঁজ; hold → charge → confirm saga, compensation শর্তসহ; খবর outbox দিয়ে; প্রতিদিন reconciliation.
  5. Ledger: double-entry, প্রতি লেনদেনে যোগফল শূন্য, append-only, পয়সায় পূর্ণসংখ্যা, balance গোনা হয়; trial balance আর gateway-এর রিপোর্ট দিয়ে প্রতি রাতে যাচাই।

পরের পর্বে: পর্ব ৬১ — ক্যাপস্টোন: একটি distributed key-value store নকশা আর simulator. Consistent hashing, replication, quorum, failure detection আর conflict — পুরো কোর্সের যন্ত্রগুলো একটি সিস্টেমে জুড়ে নিজে চালিয়ে দেখবেন।

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

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