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-দ্বন্দ্ব।
আগে এটা বুঝি
একটা প্রশ্ন দিয়ে শুরু: আপনার 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-এর সাদামাটা রূপ:
| Event | Handler |
|---|---|
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 queries | Ad-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 streamএকমাত্র source-of-truth — append-only, immutable
- Aggregate stateFold-এর derived product; snapshot শুধু speed-trick
- 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 ← বর্তমান stateRow-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)নিজে চালিয়ে দেখুন
Ledger replay — balance-at-any-date, আর snapshot-এর speedup
"""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)-এ নামায়।
নিজে বানান
EventStore — expected-version appends, snapshots, live projection + rebuild test
- Append-only stream per aggregate-id
- Expected-version optimistic concurrency — stale write reject
- Snapshot every K events — fold-checkpoint store
- Load = snapshot + tail-fold (correctness identical)
- 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 করা যায়।
নিজে বাড়ান:
- Upcaster-chain যোগ করুন —
Withdrawn v1(amount)→v2({amount, currency:'BDT'}), পুরনো fixture দিয়ে পরীক্ষা - Crypto-shredding stub — personal-field user-key-এ encrypt, key-delete-এ fold-এ field অপাঠ্য হওয়া simulate
- 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।
বুঝেছেন কি না দেখুন
1Event 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।
2WAL আর 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-ও করা যায়)।
3Stream-এ Withdrawn v1{amount} ৫০ হাজার event; নতুন handler
currency field চায়। Immutability ভাঙা ছাড়া সমাধান, ধাপে ধাপে?
প্রয়োগ
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 দেখে।
4User 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 করতে হয়।
5Product-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।
আরও পড়ুন
- Designing Data-Intensive Applications, Chapter 11 — Stream Processing — Martin Kleppmann · Event log হিসেবে system-of-record, CDC, immutable-event philosophy-র ভিত্তি
- Microservices Patterns — Event sourcing and CQRS chapters — Chris Richardson · Aggregate, projection, schema-evolution trade-off-এর শিল্প-দৃষ্টি
- Apache Kafka Documentation — Log Compaction / Kafka as a Storage System — Apache Software Foundation · Log storage-semantics — event-store infrastructure হিসেবে Kafka-র ভূমিকা