Foundationপ্রথম নীতি থেকে
LEVEL 9লেসন ১৫/১৯অ্যাডভান্সড৫৫ মিনিট

Event-driven Architecture ও Event Sourcing — state যখন log

Event-driven Architecture and Event Sourcing

State নিজে না রেখে event-stream থেকে derive — WAL যেমন crash-এর সত্য, event sourcing সেই সত্যকে business-level এ তোলে; লাভ audit আর temporal query, দাম projections আর GDPR-দ্বন্দ্ব।

এই লেসন শেষে আপনি পারবেন

  • Inversion-টা ব্যাখ্যা করতে পারবেন — current-state rows derived from append-only event stream, আর database-এর WAL ইতিমধ্যেই কীভাবে এই দর্শন মানে তা দেখাতে পারবেন
  • Event sourcing mechanics trace করতে পারবেন — per-aggregate event stream, fold/replay দিয়ে state-rebuild, আর snapshot-কে fold-checkpoint হিসেবে ব্যবহার
  • Schema evolution-এর ব্যথা মোকাবিলা করতে পারবেন — পুরনো event পড়া-যোগ্য রাখা, versioned events আর upcaster-প্যাটার্ন
  • CQRS pairing আলোচনা করতে পারবেন — write-model events থেকে per-query tuned projections/read-models, আর eventual read-consistency কেন মেনে নেওয়া হয়
  • Benefits আর costs সৎভাবে ওজন করতে পারবেন — audit/temporal-query/debugging-এর লাভ বনাম ad-hoc-query-শূন্যতা আর GDPR-delete বনাম immutable-log দ্বন্দ্ব (crypto-shredding সহ)

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

একটা প্রশ্ন দিয়ে শুরু: আপনার database আসলে জানে কী ঘটেছিল? রাশি হিসেবে জানে — accounts table-এ balance=1300 লেখা আছে। কিন্তু সেই ১৩০০ কীভাবে এলো? কোন deposit, কোন withdrawal, কোন order? Database চুপ — শেষ-state overwrite হয়ে গেছে। History জানতে চাইলে আলাদা audit-table, trigger, history-maintenance — সব বোঝা আপনার।

এবার WAL মনে করুন। Databases/wal-recovery- লেসনে দেখেছি: PostgreSQL আসলে কোথাও ‘current state’-ই সত্য ভাবে না — log-ই সত্য, page গুলো শুধু log-এর derived cache; crash-এ page ফেলে দিয়ে log replay করে সব ফেরত আনে। Event sourcing ঠিক এই দর্শনকে disk-page থেকে business-level এ তোলে: business-event-গুলোই system-of-record, আর টেবিলের rows হলো ঘুরিয়ে-আঁকা projection। আজ দেখব এই inversion কী কেনে, কী দেয়, আর কোথায় রক্ত কাটে।

মূল ধারণা

Event-driven architecture — ভিত্তি স্তর

Event-driven architecture-এ services একে অপরকে call করে না — ঘটনা publish করে, অন্যরা subscribe করে (message-queues-লেসনের choreography-ই এর উপর দাঁড়িয়ে)। Event-এর চরিত্র:

  • Past-tense fact: OrderPlaced, PaymentFailed — ঘটে-গেছে, কেউ reject করতে পারে না। Command (PlaceOrder — request) এর উল্টো।
  • Immutable: ঘটে-যাওয়া ঘটনা edit হয় না; ভুল হলে correcting event আসে।
  • Self-contained fact: consumer-রা context ছাড়াই বুঝতে পারে এমন data বহন করে।

Event sourcing — state যখন fold

মূল চুক্তি: entity-র সব mutation হয় event append-এ; current state কখনো stored নয় — computed:

aggregate = consistency-boundary (যেমন এক Account, এক Order)
stream    = ওই aggregate-এর event-ক্রম, append-only

state₀ --fold(E1)--> state₁ --fold(E2)--> state₂ ... --fold(En)--> stateₙ

fold মানে: ফাঁকা state থেকে শুরু করে প্রতিটা event-এর handler চালিয়ে গেলে হুবহু বর্তমান state — ঠিক WAL-replay যেমন pages ফেরত আনে। Bank account-এর সাদামাটা রূপ:

EventHandler
AccountOpened(initial)balance = initial
Deposited(amount)balance += amount
Withdrawn(amount)balance −= amount

Balance জানতে? পুরো stream fold করো। Temporal query free: ‘৩১ মার্চে balance কত ছিল?’ — stream-কে ৩১ মার্চ পর্যন্ত fold করলেই উত্তর; row-store-এ এর জন্য history-table + trigger-যন্ত্র লাগত। Audit-log আলাদা করে বানানোর প্রশ্নই ওঠে না — stream-টাই audit। Debugging-ও বদলায়: ‘এই bug কোন event-sequence-এ ধরা পড়ে’ reproduce করা হয় deterministic replay দিয়ে।

Snapshot — fold-checkpoint

Stream লাখ event হলে প্রতিবার পুরো replay ব্যয়বহুল। সমাধানটা databases/checkpointing-vacuum-লেসনের হুবহু প্রতিচ্ছবি: মাঝে-মাঝে folded-state snapshot জমাও, পরেরবার snapshot থেকে শুরু করে শুধু বাকি tail fold করো। Snapshot শুধু optimization — হারালেও correctness ভাঙে না, কারণ source-of-truth event-stream, snapshot নয়। WAL-checkpoint-এর সাথে এই সম্পর্কটাই (derived-cache reset, truth intact) event-sourcing-এর সবচেয়ে সুন্দর উত্তরাধিকার।

CQRS — লেখা আর পড়ার আলাদা পৃথিবী

Event sourcing-এর সঙ্গী CQRS (Command Query Responsibility Segregation):

Command ──► WRITE MODEL (aggregate + event stream)
                    │ events published
                    ▼
            PROJECTIONS ──► READ MODELS (per-query tuned tables/views)

Write-side strict; read-side প্রতিটা query-র জন্য আলাদা denormalized projection — ‘top-ten customers by region’-এর জন্য precomputed table, events থেকে জন্মায়, query-তে join লাগে না। দাম: read-model eventually consistent — user ক’মুহূর্তের জন্য নিজের write দেখতেই পারে না; UI-design সেই সত্য মেনেই সাজাতে হয়।

Benefits বনাম costs — সৎ হিসেব

লাভদাম
Free audit-log, temporal queriesAd-hoc query শূন্য — SQL JOIN events-এ চলে না, projection ছাড়া প্রশ্নের উত্তর নেই
Deterministic debugging (replay)Schema-evolution tax — upcaster-যন্ত্র চিরকাল
Natural integration feed (events out)Read-model eventual consistency
Time-travel (past-state reconstruction)GDPR right-to-erasure বনাম immutable log — সরাসরি দ্বন্দ্ব

GDPR-line-টা গুরুত্বপূর্ণ: user delete চাইলে event-history থেকে ওরা ‘অমোছনীয়’ — immutability-ই তো feature! শিল্প-সমাধান crypto-shredding: personal-data encrypted রাখা user-specific key-তে, delete মানে ওই key ফেলে দেওয়া — event-structure থাকে, content cryptographically অপাঠ্য। Design-এর শুরু থেকেই plan করতে হয়, পরে যোগ হয় না।

ভেতরে কী ঘটছে

Aggregate boundary — কোথায় transaction শেষ

Aggregate হলো সেই সীমানা যার ভেতরের পরিবর্তন এক atomic event-batch; boundary ভুল টানলে হয় বিশাল stream-এ lock-contention, নয় ভাঙা invariant। নিয়ম: এক transaction এক aggregate modify করবে; cross- aggregate coordination saga/domain-event-এ।

Expected-version concurrency

দুই command একই aggregate-এ একসাথে? Append-only stream-এ optimistic concurrency স্বাভাবিক: writer বলে ‘আমি version 7-এর উপর ভিত্তি করে decision নিয়েছি’; store version এখনো 7 কি না check করে — না হলে reject (আগের লেসনগুলোর CAS-version-এরই আত্মীয়)। কোনো long-lock নেই — conflict হলে retry/merge।

Versioned event-envelope

{
  "type": "Withdrawn",
  "version": 2,
  "occurredAt": "2024-03-20T10:15:00Z",
  "data": {"amountMinor": 250000, "currency": "BDT"}
}

Envelope-এ type+schema-version+time — reader এই তিনটা দেখেই বুঝে যায় কোন upcaster-path। Data ভেতরে থাক, বাইরে না — এই discipline decade-পুরনো streams-কেও পড়ার-যোগ্য রাখে।

Projection maintenance — incremental বনাম rebuild

Projection দুইভাবে চলে: live-subscribe (নতুন event আসতেই apply — cheap, continuous), আর rebuild (scratch থেকে পুরো stream re-fold — expensive, rare)। Rebuild-ই superpower: read-model migration-script-এ আটকে থাকে না, drop-and-rebuild-ই schema-change। দাম: rebuild-time stream-size-এ linear — snapshot এখানেও ঔষধ।

Event-sourcing stack — কোন স্তরে কী সত্য
  1. Event streamএকমাত্র source-of-truth — append-only, immutable
  2. Aggregate stateFold-এর derived product; snapshot শুধু speed-trick
  3. Read modelsEvents-এর disposable projection — ভাঙলে rebuild, ভয় নেই

উদাহরণ

ACC-1 হিসাব — এক stream-এর জীবন

{"type":"Opened","version":1,"at":"2024-01-01","data":{"initial":0}}
{"type":"Deposited","version":2,"at":"2024-01-05","data":{"amount":5000}}
{"type":"Withdrawn","version":3,"at":"2024-02-10","data":{"amount":1200}}
{"type":"Deposited","version":4,"at":"2024-03-03","data":{"amount":300}}
{"type":"Withdrawn","version":5,"at":"2024-03-20","data":{"amount":2500}}

Replay-fold:

v0 {}                      → balance অজানা
v1 Opened(0)               → 0
v2 Deposited(5000)         → 5000
v3 Withdrawn(1200)         → 3800
v4 Deposited(300)          → 4100   ← ৩১ মার্চের উত্তর এখানেই!
v5 Withdrawn(2500)         → 1600   ← বর্তমান state

Row-store-এ ‘৩১ মার্চের balance’-এর জন্য কী লাগত: trigger-চালিত history-table (বা slowly-changing-dimension), history-write-এর maintenance, আর সেই table-এ point-in-time query — সব manual discipline, একটা ভুল trigger-ই history silent-hole। Event-stream-এ একই প্রশ্ন = partial fold, কোনো extra-yantra নেই, কারণ history-টাই primary data।

ছবিতে

 PlaceOrder(cmd)──►┌─────────────────────────┐
                   │ AGGREGATE: Order        │ invariant check ✕ → Reject
                   │ decide: accept?         │
                   └───────────┬─────────────┘
                               │ append OrderPlaced (v=n+1)
                               ▼
              ┌──────────────────────────────────┐ EVENT STREAM (truth)
              │ [OrderPlaced][Paid][Shipped]...  │ append-only
              └───────────────┬──────────────────┘
                              │ publish (message-queue!)
        ┌─────────────────────┼──────────────────────┐
        ▼                     ▼                      ▼
 ┌────────────┐       ┌──────────────┐       ┌──────────────┐
 │ projection │       │ projection   │       │ other svc    │
 │ orders-by- │       │ revenue-per- │       │ (inventory…) │
 │ customer   │       │ day table    │       │ reacts       │
 └─────┬──────┘       └──────┬───────┘       └──────────────┘
       ▼                     ▼
   QUERY: fast reads (eventually consistent)
Command → aggregate → events → broker → projections — write-পথ একমুখী, read-model গুলো disposable।

নিজে চালিয়ে দেখুন

EXPERIMENT

Ledger replay — balance-at-any-date, আর snapshot-এর speedup

macOS / Linux (Python 3.10+)· ২০ মিনিট
"""ledger_replay.py — fold/replay + temporal query + snapshot timing"""
import json, time

EVENTS_JSONL = '''
{"type":"Opened","version":1,"at":"2024-01-01","data":{"initial":0}}
{"type":"Deposited","version":2,"at":"2024-01-05","data":{"amount":5000}}
{"type":"Withdrawn","version":3,"at":"2024-02-10","data":{"amount":1200}}
{"type":"Deposited","version":4,"at":"2024-03-03","data":{"amount":300}}
{"type":"Withdrawn","version":5,"at":"2024-03-20","data":{"amount":2500}}
'''

FOLDERS = {
    'Opened':    lambda st, e: {**st, 'bal': e['data']['initial']},
    'Deposited': lambda st, e: {**st, 'bal': st['bal'] + e['data']['amount']},
    'Withdrawn': lambda st, e: {**st, 'bal': st['bal'] - e['data']['amount']},
}

events = [json.loads(l) for l in EVENTS_JSONL.splitlines() if l.strip()]

def fold(stream, upto=None, start_state=None):
    st = start_state if start_state is not None else {'bal': None}
    for e in stream:
        if upto and e['at'] > upto:
            break
        st = FOLDERS[e['type']](st, e)
    return st

print(f"বর্তমান balance        : {fold(events)['bal']}")
for d in ('2024-02-01', '2024-03-10'):
    print(f"balance @ {d}    : {fold(events, upto=d)['bal']}")

# ── snapshot vs full replay — 200k-event stream ──
N = 200_000
big = [{'type': 'Opened', 'data': {'initial': 0}}]
for i in range(1, N):
    kind = 'Deposited' if i % 3 else 'Withdrawn'
    big.append({'type': kind, 'data': {'amount': (i % 97) + 1}})

t0 = time.perf_counter()
full = fold(big)
t_full = time.perf_counter() - t0

SNAP_AT = N // 2                       # checkpoint mid-stream
snap_state = fold(big[:SNAP_AT])
t1 = time.perf_counter()
tail = fold(big[SNAP_AT:], start_state=snap_state)
t_snap = time.perf_counter() - t1

print(f"full replay : {t_full * 1000:8.1f} ms  (final={full['bal']})")
print(f"snap + tail : {t_snap * 1000:8.1f} ms  (final={tail['bal']})")
assert full['bal'] == tail['bal']
print("PASS — snapshot-result identical, খরচ ~অর্ধেক")

নমুনা output:

বর্তমান balance        : 1600
balance @ 2024-02-01    : 5000
balance @ 2024-03-10    : 4100
full replay :    94.2 ms  (final=3266474)
snap + tail :    47.8 ms  (final=3266474)
PASS — snapshot-result identical, খরচ ~অর্ধেক

তিনটা পাঠ:

  • Temporal query-টা কোনো special-feature নয় — একই fold, শুধু তারিখে stop; row-store-এ এর জন্য আলাদা history-yantra চাই।
  • Snapshot-correctness assertion-টাই মূল: derived-cache হারলে/ভাঙলে fold-from-events-ই final judge।
  • Real system-এ stream million-scale; snapshot-interval tuning আর serialization-cost তখন বড় ফ্যাক্টর — কিন্তু asymptotic-লাভ একই।
এটা কী প্রমাণ করে

Event-log থেকে যে-কোনো past-date-এর state partial fold-এই আসে (row-store-এ যেটার জন্য history-table লাগে), আর snapshot-checkpoint replay-কে O(full) থেকে O(tail)-এ নামায়।

নিজে বানান

BUILD IT

EventStore — expected-version appends, snapshots, live projection + rebuild test

Python · ●●●●○
  1. Append-only stream per aggregate-id
  2. Expected-version optimistic concurrency — stale write reject
  3. Snapshot every K events — fold-checkpoint store
  4. Load = snapshot + tail-fold (correctness identical)
  5. Live projection + rebuild-from-scratch equality test
"""eventstore.py — mini event-store (~110 line)"""
import json

class VersionConflict(Exception):
    pass

FOLDERS = {
    'Opened':    lambda st, e: {**st, 'bal': e['data'].get('initial', 0)},
    'Deposited': lambda st, e: {**st, 'bal': st.get('bal', 0) + e['data']['amount']},
    'Withdrawn': lambda st, e: {**st, 'bal': st.get('bal', 0) - e['data']['amount']},
}

def fold_events(stream, state=None):
    st = state if state is not None else {}
    for e in stream:
        st = FOLDERS[e['type']](st, e)
    return st

class EventStore:
    def __init__(self, snapshot_every=2):
        self.streams = {}          # agg-id -> list[event]
        self.snapshots = {}        # agg-id -> (version, state)
        self.every = snapshot_every

    def append(self, agg, etype, data, expected_version):
        stream = self.streams.setdefault(agg, [])
        if len(stream) != expected_version:                 # CAS-style guard
            raise VersionConflict(
                f'{agg}: want v{expected_version}, store has v{len(stream)}')
        e = {'agg': agg, 'type': etype, 'version': len(stream) + 1,
             'data': data}
        stream.append(e)
        if len(stream) % self.every == 0:                   # checkpoint!
            self.snapshots[agg] = (len(stream), fold_events(stream))
        return e['version']

    def load(self, agg):
        """snapshot থেকে শুরু — শুধু tail fold"""
        stream = self.streams.get(agg, [])
        start, base = self.snapshots.get(agg, (0, None))
        return fold_events(stream[start:], state=base)

    def raw_fold(self, agg):
        return fold_events(self.streams.get(agg, []))       # no-snapshot path


class Projection:
    """live read-model — নতুন event আসতেই apply"""
    def __init__(self):
        self.balances = {}

    def apply(self, e):
        agg, amt = e['agg'], e['data'].get('amount')
        cur = self.balances.get(agg, 0)
        if e['type'] == 'Opened':
            self.balances[agg] = e['data'].get('initial', 0)
        elif e['type'] == 'Deposited':
            self.balances[agg] = cur + amt
        elif e['type'] == 'Withdrawn':
            self.balances[agg] = cur - amt

    def rebuild_from(self, store):
        self.balances = {}                                  # scratch!
        for agg in sorted(store.streams):
            for e in store.streams[agg]:
                self.apply(e)
        return self


if __name__ == '__main__':
    store = EventStore(snapshot_every=2)

    v1 = store.append('A1', 'Opened', {'initial': 0}, expected_version=0)
    v2 = store.append('A1', 'Deposited', {'amount': 5000}, expected_version=v1)
    v3 = store.append('A1', 'Withdrawn', {'amount': 1200}, expected_version=v2)

    try:                                                    # stale writer!
        store.append('A1', 'Deposited', {'amount': 9},
                     expected_version=1)
    except VersionConflict as err:
        print(f'conflict blocked: {err}')

    store.append('A1', 'Deposited', {'amount': 300}, expected_version=v3)
    store.append('B2', 'Opened', {'initial': 100}, expected_version=0)

    print(f"load(A1) w/ snapshot : {store.load('A1')}")
    print(f"raw fold(A1)         : {store.raw_fold('A1')}")
    assert store.load('A1') == store.raw_fold('A1')

    proj = Projection()
    for agg in ('A1', 'B2'):                                # live-apply all
        for e in store.streams[agg]:
            proj.apply(e)
    fresh = Projection().rebuild_from(store)                # scratch rebuild
    print(f"live projection : {proj.balances}")
    print(f"rebuilt         : {fresh.balances}")
    assert proj.balances == fresh.balances
    print("PASS — snapshot-load==raw-fold, live==rebuilt")

নমুনা output:

conflict blocked: A1: want v1, store has v3
load(A1) w/ snapshot : {'bal': 4100}
raw fold(A1)         : {'bal': 4100}
live projection : {'A1': 4100, 'B2': 100}
rebuilt         : {'A1': 4100, 'B2': 100}
PASS — snapshot-load==raw-fold, live==rebuilt

তিনটা পাঠ:

  • Stale-writer rejection-টাই aggregate-concurrency — distributed lock ছাড়াই safety, conflict-এ retry।
  • Snapshot ভুল হলেও raw_fold শেষ কথা — dual-path assertion production event-store-এর nightly-integrity-check।
  • Live-projection আর rebuild-এর equality প্রমাণ করে read-model disposable — এটাই CQRS-এর সাহস: query-side ভয়-হীনভাবে change করা যায়।

নিজে বাড়ান:

  1. Upcaster-chain যোগ করুন — Withdrawn v1(amount) → v2({amount, currency:'BDT'}), পুরনো fixture দিয়ে পরীক্ষা
  2. Crypto-shredding stub — personal-field user-key-এ encrypt, key-delete-এ fold-এ field অপাঠ্য হওয়া simulate
  3. Multi-aggregate projection (‘total deposits per day’) — event timestamp-এ group করে আলাদা read-dict

বাস্তব সিস্টেমে

বাস্তবে event sourcing

Kafka-ই আধা-event-store। Log-compaction, indefinite retention, consumer-offset — অনেক team event-sourcing-lite চালায় Kafka-topic-এই; পূর্ণ per-aggregate semantics (expected-version, key-partition placement) discipline দিয়ে মেটে।

EventStoreDB, Axon, Marten। Purpose-built stores — native streams, projections, subscription; Axon-এর aggregate+command-gateway pattern শিল্প-standard (Richardson-এর আলোচনা এখানেই মেলে)।

Banking ledger systems। Double-entry ledger ঐতিহাসিকভাবেই event-log — debit/credit entries immutable, balance derived; audit-mandatory regulation- এ event sourcing tradition-ই।

Git — আপনার চোখের সামনের event sourcing। Commits = immutable append-only events; working-tree = derived state; checkout = temporal query; revert = corrective event — আপনি বছরের পর বছর এই model-এই কাজ করছেন।

CQRS-screen-গুলো। Elasticsearch-index fed by change-events, Redis-view fed by domain-events — প্রতিটা ‘search-index sync’ pipeline আসলে projection-ই, নাম না নিলেও।

যে ভুলগুলো সবাই করে

“Event sourcing মানে message-queue-এ event পাঠানো — একই জিনিস।”

Queue transport-message (consumed-then-gone); event sourcing-এর stream system-of-record (retained forever, replayable)। Queue-তে consumer message হারালে truth-ও হারায়; event-store-এ consumer হারাক — source অটুট। দুটো complementary: stream-ই হতে পারে queue-র upstream।

“CQRS মানেই event sourcing, আর event sourcing মানেই CQRS — আলাদা করে ভাবা যায় না।”

Separate concerns: CQRS = read/write model আলাদা, যে-কোনো storage-এ করা যায় (plain CRUD + replica-ও); event sourcing = state-as-fold, CQRS ছাড়াও চলে। Pairing popular কারণ events-ই perfect projection-feed — কিন্তু coupling হলো choice, definition নয়।

“Events আর commands এক — সব দিক-নির্দেশই event।”

Command = future-tense request (RefundOrder) — reject হতে পারে, validation লাগে। Event = past-tense fact (OrderRefunded) — already happened, immutable, reject অসম্ভব। Aggregate command receive করে, event emit করে; দুটো গুলিয়ে লিখলে validation-architecture ভেঙে পড়ে।

“Event sourcing সবখানে ব্যবহারযোগ্য superior pattern — CRUD legacy-ই দোষ।”

CRUD-domain (address-book edit, CMS page) event-sourcing-এ গেলে লাভ কম, tax পূর: প্রতি field-edit এক event, upcaster-ঋণ, projection-যন্ত্র। Pattern-টা ঝলমলে যেখানে audit/temporal/correctness-দাবি বেশি (finance, booking, inventory-policy)। Default-architecture না — fit-for-domain।

বুঝেছেন কি না দেখুন

1

Event sourcing-এ current state কীভাবে পাওয়া যায়, আর কোন স্তরটা source-of-truth?

স্মরণ

Aggregate-এর event-stream শুরু-থেকে fold (replay) করে; snapshot থাকলে snapshot+tail। Source-of-truth একমাত্র append-only event-stream — state/snapshot/projection সব derived, disposable।

2

WAL আর event sourcing-এর analogy-টা ঠিক কোথায় মেলে, কোথায় থামে?

যুক্তি

মেলে দর্শনে: append-only log = truth, pages/state = derived cache, crash/ভাঙলে replay। থামে abstraction-এ: WAL physical change-record (page bytes), engine-internal; event stream business-fact, API-level — তাই WAL কখনো cross-service integration-feed হয় না, event stream হয় (CDC দিয়ে bridge-ও করা যায়)।

3

Stream-এ Withdrawn v1{amount} ৫০ হাজার event; নতুন handler currency field চায়। Immutability ভাঙা ছাড়া সমাধান, ধাপে ধাপে?

প্রয়োগ

Versioned event: নতুন writes v2{amount, currency} হবে। পুরনোগুলোর জন্য upcaster: reader-layer-এ v1-envelope দেখলে on-the-fly {amount, currency:'BDT'} inject করে v2-shape-এ পৌঁছে দেয় — stored bytes অটুট। Upcaster-chain version-order-এ compose হয়; handler সবসময় latest-shape দেখে।

4

User command পাঠিয়ে success-screen দেখল, কিন্তু ‘my orders’-list- এ নিজের order নেই — কোন architecture-decision-এর ফল, আর standard remedy?

যুক্তি

CQRS-এর eventual read-consistency: command → write-model commit ও projection-update আলাদা সময়ে। Remedy-pattern: UI-তে optimistic echo (client-side placeholder), read-your-own-writes routing (নিজের session- এর query write-side/master-এ), বা confirmation-এ projection-version ack। Architecture-ভাঙা লাগে না, expectation-manage করতে হয়।

5

Product-manager: ‘user-এর email address আমাদের event-stream-এ ২ বছরের logs-এ আছে — GDPR delete-request এলে কী করব?’ Design-answer?

ডিজাইন

Retroactive deletion impossible (immutability by design) — তাই forward-looking crypto-shredding: PII-fields user-specific key-এ encrypt করে event-এ রাখা শুরু করুন (পুরনো logs-এ legal-window অনুযায়ী handle); delete-request মানে key destroy — events structurally থাকে, PII computationally unreadable। Key-management হলো compliance-artifact, documented + audited।

এরপর কী

এরপর কী

Module-এর এই চার-লেসন arc এখানে শেষ — transaction ভাঙা, duplicate রোধ, transport, আর state-as-log। Module এগোবে coordination-এর গভীরে: distributed locks (Redis-based lock কেন বিপজ্জনক, fencing-token কেন অমিটযোগ্য), তারপর gossip protocols আর CRDTs — consensus ছাড়াই convergence-এর গণিত, যেখানে ‘last-write-wins’-ও একটা merge-function।

আরও পড়ুন