System Design শেখো
সব কেস স্টাডি

Digital Wallet ও Ledger ডিজাইন

14 মিনিটadvanced
এক নজরে
  • Digital wallet-এর হৃদয় হলো double-entry ledger—প্রতিটা টাকা কোথা থেকে এসে কোথায় গেল তা append-only ভাবে লেখা থাকে।
  • Idempotency key দিয়ে duplicate request এক করা হয়, যাতে retry-তে দুবার টাকা না কাটে (exactly-once effect)।
  • Strong consistency আবশ্যক—কোনো টাকা সৃষ্টি বা ধ্বংস হওয়া যাবে না, এবং প্রতিটা entry-র যোগফল সবসময় শূন্য।

bKash, Nagad বা Stripe-এর মতো একটা digital wallet বানাও—যেখানে কোটি মানুষ টাকা রাখে, পাঠায়, তোলে। এখানে একটা পয়সাও হারানো বা বানানো চলবে না। এই correctness-এর গ্যারান্টি আসে একটা পুরনো accounting ধারণা থেকে: double-entry ledger। চলো ডিজাইন করি একটা wallet যেখানে টাকা গোনায় কখনো ভুল হয় না।

১. সমস্যা বোঝা (Requirements)

Functional requirements:

  • User-এর wallet-এ balance রাখা; deposit, withdraw, peer-to-peer transfer।
  • প্রতিটা লেনদেনের ইতিহাস ও statement দেখা।
  • Merchant payment, refund, hold/release (escrow)।

Non-functional requirements (এখানে correctness সর্বোচ্চ):

  • No money lost or created: সিস্টেমে মোট টাকার যোগফল কখনো নিজে থেকে বদলাবে না।
  • Strong consistency: balance read করলে সবসময় সঠিক, latest মান পেতে হবে।
  • Exactly-once: retry বা duplicate request-এ লেনদেন একবারই ঘটবে।
  • Auditability: প্রতিটা পয়সার গতিবিধির অপরিবর্তনীয় (immutable) রেকর্ড।
  • Durability: committed transaction কখনো হারাবে না।
সহজ উদাহরণ

পুরনো দিনের দোকানদারের খাতা ভাবো। প্রতিবার যখন গদি থেকে ক্যাশবাক্সে টাকা যায়, দোকানদার দুই জায়গায় লেখেন—একদিকে "গদি থেকে গেল", আরেকদিকে "ক্যাশবাক্সে এলো"। দুটো সংখ্যা সবসময় মেলে। কোথাও গরমিল থাকলে সাথে সাথে ধরা পড়ে। double-entry ledger ঠিক এই খাতা—কিন্তু ডিজিটাল ও atomic।

২. স্কেল আন্দাজ (Estimation)

Scale:
  Users               = 50,000,000
  Peak transactions   = 20,000 TPS (উৎসব/সেলের সময়)
  Avg transactions    = 2,000 TPS

Latency budget (transfer API):
  Auth + validate     ~ 10 ms
  Ledger write (txn)  ~ 20 ms  (strong consistency, durable)
  Response            ~ 5  ms
  ----------------------------------
  Target p99          < 200 ms

Storage (append-only ledger):
  প্রতি লেনদেনে ~ 2 ledger entries * 200 bytes = 400 B
  20k TPS * 400 B     = 8 MB/sec ≈ 700 GB/দিন
  → archival tier-এ পুরনো entry সরানো হয়, hot index ছোট রাখা হয়

মূল কথা: throughput অসম্ভব বেশি নয়, কিন্তু প্রতিটা write durable + consistent হতে হবে—এটাই latency-র দাম।

৩. API ডিজাইন

POST /v1/transfers
  Header: Idempotency-Key: "txn-9f3a-..."   # বাধ্যতামূলক
  Body:   { from_account, to_account, amount, currency, reference }
  → 200  { transfer_id, status: COMPLETED, ledger_seq }
  → 409  { existing transfer (একই idempotency key) }

GET  /v1/accounts/{id}/balance      → { balance, as_of_seq }
GET  /v1/accounts/{id}/statement    → [ entries... ]
POST /v1/transfers/{id}/refund      → { refund_id, status }
POST /v1/holds                      → { hold_id }  (escrow)
POST /v1/holds/{id}/capture|release

লক্ষ্য করো Idempotency-Key header—টাকা-সম্পর্কিত প্রতিটা mutating call-এ এটা বাধ্যতামূলক।

৪. ডেটা মডেল

Tableমূল ফিল্ডউদ্দেশ্য
accountsaccount_id, owner, type (user/system), currency, statusপ্রতিটা wallet ও system account
ledger_entriesentry_id, txn_id, account_id, direction (DEBIT/CREDIT), amount, seq_no, created_atappend-only, immutable—মূল truth
transactionstxn_id, idempotency_key, status, created_atএকটা logical লেনদেন (একাধিক entry-কে বাঁধে)
idempotencykey, txn_id, response_hash, statusduplicate request ধরা
balancesaccount_id, balance, last_seqদ্রুত read-এর জন্য derived snapshot

কেন SQL ও strong consistency জেতে? এখানে ACID transaction ছাড়া উপায় নেই। একটা transfer-এ debit ও credit একসাথে commit হতে হবে—একটা সফল আর একটা ব্যর্থ হলে টাকা হাওয়া। Relational database-এর atomic multi-row transaction এটা সহজ করে। balances table হলো একটা derived cache (performance-এর জন্য), কিন্তু আসল truth সবসময় ledger_entries—balance যেকোনো সময় entry গুলো যোগ করে পুনরায় হিসাব করা যায়।

৫. হাই-লেভেল ডিজাইন

একটা transfer-এর ধাপ:

  1. API gateway: auth, rate limit, request validation।
  2. Idempotency check: idempotency key দিয়ে দেখা হয় এই request আগে এসেছে কিনা। এসে থাকলে সঞ্চিত response ফেরত, নতুন কিছু করা হয় না।
  3. Ledger service: একটা database transaction শুরু করে—
    • উভয় account-এর balance lock/check (sufficient funds?)।
    • দুটো ledger entry লেখা: from-account DEBIT, to-account CREDIT।
    • balance update।
    • transaction record COMPLETED।
    • সব এক atomic commit-এ।
  4. Event publish: commit সফল হলে একটা transfer.completed event পাঠানো হয় (notification, fraud check, analytics-এর জন্য)।
  5. Reconciliation job: ব্যাকগ্রাউন্ডে নিয়মিত মিলিয়ে দেখা—সব entry-র যোগফল শূন্য কিনা, balance cache আর ledger sum মেলে কিনা।

৬. গভীরে (Deep Dive)

Double-entry: কেন টাকা হারায় না

প্রতিটা লেনদেন কমপক্ষে দুটো entry—একদিকে DEBIT, অন্যদিকে সমপরিমাণ CREDIT। নিয়ম: প্রতি transaction-এ সব entry-র যোগফল শূন্য।

Transfer: করিম → রহিম, 500 টাকা
  ledger_entries:
    { txn: T1, account: করিম, direction: DEBIT,  amount: 500 }
    { txn: T1, account: রহিম, direction: CREDIT, amount: 500 }
  যোগফল (credit - debit) = 0  ✓

এই invariant-এর সৌন্দর্য: যদি কখনো পুরো সিস্টেমের সব entry যোগ করে শূন্য না পাও, তবে নিশ্চিত একটা bug আছে—টাকা সৃষ্টি বা ধ্বংস হয়েছে। deposit ও withdraw-ও double-entry: bank থেকে deposit মানে একটা system "external bank" account থেকে DEBIT, user account-এ CREDIT। সবকিছু balanced থাকে।

Idempotency ও exactly-once

নেটওয়ার্কে "exactly-once delivery" বলে কিছু নেই—client timeout পেলে জানে না transfer হয়েছিল কিনা, তাই retry করে। আমরা চাই exactly-once effect:

  1. Client প্রতিটা transfer-এ একটা unique idempotency key পাঠায়।
  2. Server প্রথমে idempotency table-এ key insert করার চেষ্টা করে (unique constraint সহ)।
  3. সফল হলে—নতুন request—লেনদেন চালাও, ফলাফল key-র সাথে সংরক্ষণ করো।
  4. ব্যর্থ হলে (key আগেই আছে)—duplicate—সংরক্ষিত আগের response ফেরত দাও, কোনো নতুন entry লেখো না।

মূল trick: idempotency record আর ledger entry একই database transaction-এ commit হতে হবে। নাহলে এমন window থাকে যেখানে টাকা কাটা হয়েছে কিন্তু key save হয়নি (বা উল্টো)।

সাবধান

সবচেয়ে বিপজ্জনক race condition: একই idempotency key নিয়ে দুটো request প্রায় একসাথে আসা (double-click বা retry storm)। যদি তুমি "আগে check, পরে insert" করো (check-then-act), দুটোই check-এ "নেই" দেখবে, দুটোই লেনদেন চালাবে—double charge! সঠিক উপায়: database-এর unique constraint-এর উপর নির্ভর করা (insert-আগে-act), যাতে দ্বিতীয় insert atomic ভাবে fail করে। কখনোই application-level check-then-act দিয়ে টাকার idempotency সামলিও না।

Transaction state machine ও reconciliation

প্রতিটা transaction একটা state machine অনুসরণ করে: PENDING → COMPLETED অথবা PENDING → FAILED। Escrow/hold-এর ক্ষেত্রে: HELD → CAPTURED বা HELD → RELEASED। state শুধু সামনে এগোয়, পেছনে যায় না (refund একটা নতুন reverse transaction, পুরনোটা বদলায় না)।

Reconciliation হলো নিরাপত্তা জাল: একটা background job নিয়মিত যাচাই করে—

  • সব ledger entry-র global যোগফল শূন্য?
  • প্রতিটা balances cache = সেই account-এর entry গুলোর sum?
  • External bank statement-এর সাথে আমাদের system account মেলে?

গরমিল পেলে alert—এটাই বড় আর্থিক বিপর্যয়ের আগাম সংকেত।

৭. বটলনেক ও স্কেলিং

  • Hot account contention: একটা জনপ্রিয় merchant account-এ হাজারো concurrent write—row lock contention। সমাধান: balance-কে একাধিক "sub-account" বা shard-এ ভাগ করা, পরে যোগ করে মোট হিসাব।
  • Partition by account: ledger account_id দিয়ে shard করা যায়। কিন্তু cross-shard transfer-এ distributed transaction লাগে—এখানে saga বা two-phase commit, অথবা একটা single ledger service যা ordering নিশ্চিত করে।
  • Read scaling: balance read অনেক বেশি—balances cache ও read replica ব্যবহার, কিন্তু write সবসময় primary-তে (strong consistency)।
  • Append-only growth: পুরনো entry archival storage-এ, hot table-এ শুধু সাম্প্রতিক ও index।

৮. সারসংক্ষেপ

Wallet/ledger ডিজাইনের আসল পরীক্ষা correctness। তিনটা স্তম্ভ মনে রাখো: (১) double-entry ledger—সব entry-র যোগফল শূন্য, তাই টাকা হারায় না; (২) atomic, strongly-consistent transaction—debit ও credit একসাথে commit; (৩) idempotency key (database constraint দিয়ে enforced)—retry-তে double charge নয়। আর সবকিছুর উপরে reconciliation, নিরাপত্তা জাল হিসেবে।

টিপস

ইন্টারভিউতে interviewer পরীক্ষা করে তুমি "money is special" বোঝো কিনা। eventual consistency দিয়ে wallet বানাতে চাইলে রেড ফ্ল্যাগ। বলো: double-entry invariant, single atomic transaction, idempotency via unique constraint, আর reconciliation—এই চারটা শব্দ বললেই বোঝা যায় তুমি financial system ডিজাইন বোঝো।

মিনি কুইজ

1. Double-entry ledger-এ প্রতিটা transaction-এর entry গুলোর যোগফল কত হওয়া উচিত?

2. Network timeout-এর পর client একই transfer আবার পাঠালে দুবার টাকা কাটা ঠেকায় কোনটি?

3. Ledger-এ কেন রেকর্ড update না করে append-only রাখা হয়?