Tizim dizayni kursi · 10-mavzu · DEEP / SENIOR
TuzdiDinMuhammad

Event Sourcing — Senior chuqurlik

Bu — 10-mavzuning 2-qatlami. Core darslikni o‘qib bo‘lgan deb hisoblanadi. Bu yerda eng chuqur narsalar: event versiyalash/upcasting, optimistik concurrency, snapshot strategiyalari, event store tanlovi, GDPR/crypto-shredding, process manager va katta oqimlar — junior’dan seniorgacha.

Daraja: SeniorMavzular: upcasting · expectedVersion · crypto-shredding · sagaOld shart: 10-mavzu (Core)

Bu qatlamda nima bor

Core darslik asoslarni berdi. Bu Deep qatlam senior darajadagi chuqur mexanikani ochadi: event versiyalash va upcasting, optimistik concurrency (expectedVersion), snapshot strategiyalari, event store tanlovi (EventStoreDB/Postgres/Kafka), GDPR uchun crypto-shredding, process manager/saga va katta oqimlar bilan ishlash. Oxirida — yig‘ma arxitektura va senior cheklist.

01
Deep · Versioning

Event versiyalash va upcasting

upcasting · tolerant reader
🧬

Eng katta senior muammosi: eventlar abadiy yashaydi. Bugun saqlagan event 5 yildan keyin ham o‘qiladi — lekin biznes va event tuzilishi o‘zgaradi. Eski eventlarni hech qachon o‘zgartirib bo‘lmaydi (immutable).

Uchta strategiya

  • Weak/tolerant schema: yangi maydonlarni ixtiyoriy (default bilan) qo‘shish; o‘quvchi yo‘q maydonni e’tiborsiz qoldiradi. Eng oddiy, lekin faqat qo‘shish uchun.
  • Versiyalangan eventlar: OrderCreated.v1, OrderCreated.v2 — yangi shakl yangi versiya sifatida.
  • Upcasting: o‘qishda eski eventni yangi shaklga aylantirish (saqlangan event tegilmaydi).
// Eski event (v1) va yangi (v2): manzil oddiy string edi -> obyektga aylandi
type OrderCreatedV1 = { id: string; address: string };
type OrderCreatedV2 = { id: string; address: { city: string; street: string } };

// UPCASTER: O'QISHDA eski eventni yangi shaklga aylantiradi
// (saqlangan event O'ZGARMAYDI — faqat o'qiyotganda transformatsiya)
function upcast(e: StoredEvent): DomainEvent {
  if (e.type === "OrderCreated.v1") {
    return {
      type: "OrderCreated.v2",
      data: { id: e.data.id, address: parseAddress(e.data.address) },  // string -> obyekt
    };
  }
  return e;   // allaqachon yangi versiya
}
saqlangan v1(o‘zgarmas) upcast v2 (o‘qishda) aggregat faqat v2 ko‘radi
Saqlangan eski event o‘zgarmaydi; upcaster uni o‘qishda yangi shaklga keltiradi
Murakkabroq o‘zgarishlar

Maydon qo‘shish oson. Lekin event bo‘lish/birlashtirish (bitta eski event → ikkita yangi, yoki aksincha), nom o‘zgartirish — upcaster pipeline orqali bosqichma-bosqich (v1→v2→v3) qilinadi. Qoida: upcasting faqat oldinga, saqlangan eventga hech qachon teginma.

02
Deep · Concurrency

Optimistik concurrency + expectedVersion

expectedVersion · primary key
⚔️

Event-sourced aggregat uchun concurrency nazorati — expectedVersion. Bu CQRS Deep’dagi optimistik concurrency’ning event store varianti.

Muammo

Ikki command bir aggregatni (version 5) yuklab, ikkalasi ham event qo‘shmoqchi. Agar tekshiruvsiz yozsa — ikkalasi version 6 yozadi, biri ikkinchisini ustiga yozadi yoki tarix buziladi.

Yechim — expectedVersion bilan append

Aggregat yuklanganda uning joriy versiyasini bilamiz. Append qilganda "men 5-versiyani ko‘rgandim, 6 ga yozmoqchiman" deymiz. Agar oraliqda kimdir 6 ni yozib qo‘ygan bo‘lsa — yozuv rad etiladi (reload + retry). Postgres’da buni PRIMARY KEY (aggregate_id, version) tabiiy beradi:

// Postgres event store — optimistik concurrency PRIMARY KEY orqali
// CREATE TABLE events (aggregate_id UUID, version INT, type TEXT, data JSONB,
//   PRIMARY KEY (aggregate_id, version));   <- bir version ikki marta yozilmaydi

async append(aggregateId: string, expectedVersion: number, events: DomainEvent[]) {
  try {
    await this.prisma.$transaction(
      events.map((e, i) =>
        this.prisma.event.create({
          data: {
            aggregateId,
            version: expectedVersion + i + 1,   // 6, 7, ... (kutilgan + 1 dan)
            type: e.type,
            data: e as any,
          },
        }),
      ),
    );
  } catch (err) {
    if (isUniqueViolation(err))                 // (aggregate_id, version) band -> kimdir yozdi
      throw new ConcurrencyException("Concurrent modification — reload & retry");
    throw err;
  }
}
Cmd A: v5→v6 ✓ yozildi Cmd B: v5→v6 ✕ konflikt → retry PRIMARY KEY (aggregate_id, version) v6 ni faqat BITTASI yoza oladi
expectedVersion: bir versiyani faqat bitta yozuvchi egallaydi
03
Deep · Snapshot

Snapshot strategiyalari

snapshot policy · versioning
📸

Core’da snapshot’ni ko‘rdik. Senior darajada muhimi — qachon olish, qayerda saqlash va snapshot ham versiyalanishini tushunish.

Strategiyalar

  • Qachon: har N eventda (masalan, 100), yoki vaqt bo‘yicha, yoki yuklashda "juda ko‘p event qoldi" deb aniqlanganda.
  • Qayerda: alohida snapshot ombori (event store’dan ajratilgan) — eventlarni bulg‘amaydi.
  • Versiyalash: snapshot — bu aggregat holatining seriyalashtirilgan shakli; holat sxemasi o‘zgarsa, eski snapshot o‘qib bo‘lmaydi.
// SNAPSHOT siyosati — bu OPTIMIZATSIYA, hech qachon haqiqat manbai emas
async save(order: Order) {
  await this.appendEvents(order);
  if (order.version % 100 === 0) {                // siyosat: har 100 eventda
    await this.snapshots.save(order.id, {
      version: order.version,
      state: order.toSnapshot(),
      schemaVersion: 3,                           // snapshot SXEMASI ham versiyalanadi
    });
  }
}

async load(id: string): Promise<Order> {
  const snap = await this.snapshots.getLatest(id);
  // snapshot sxemasi eskirgan/mos kelmasa -> tashlab, eventlardan TO'LIQ qur
  const usable = snap && snap.schemaVersion === 3;
  const order = usable ? Order.fromSnapshot(snap) : new Order();
  const from = usable ? snap.version : 0;
  order.loadFromHistory(await this.eventStore.getEvents(id, { after: from }));
  return order;
}
Eng muhim qoida

Snapshot — kesh/optimizatsiya, hech qachon haqiqat manbai emas. Uni har doim eventlardan qayta qurish mumkin bo‘lishi shart. Snapshot sxemasi mos kelmasa — uni shunchaki tashlab, eventlardan to‘liq tiklaysan. Snapshot’ni yo‘qotsang — hech narsa yo‘qolmaydi (faqat sekinroq yuklanadi).

04
Deep · Store

Event store tanlovi

EventStoreDB · Postgres · Kafka
🗄️

Event store tanlovi muhim qaror. Uch asosiy variant bor — har biri boshqa kelishuv bilan.

EventStoreDBPostgreSQL jadvalKafka
TabiatiMaxsus event storeUmumiy RDBMSTaqsimlangan log
Aggregat oqimi yuklashO‘rnatilgan (streams)Oddiy WHERE aggregate_idQiyin (log, kalit bo‘yicha tasodifiy o‘qish yo‘q)
Optimistik concurrencyO‘rnatilgan (ExpectedVersion)PRIMARY KEY bilan o‘zingYo‘q (tabiiy emas)
Subscription/projectionO‘rnatilgan (catch-up)LISTEN/NOTIFY yoki pollingA’lo (consumer groups)
Eng yaxshi rolTo‘liq event storeOddiy boshlash, tranzaksionTransport (haqiqat manbai emas)
Senior tavsiya

Postgres jadval — sizning stack’ingizda eng amaliy boshlang‘ich: tranzaksion, PRIMARY KEY (aggregate_id, version) bilan concurrency, JSONB bilan moslashuvchan. EventStoreDB — ES asosiy bo‘lsa va ko‘p stream/subscription kerak bo‘lsa. Kafka — ajoyib event transport, lekin haqiqat manbai sifatida emas: u log, bitta aggregat oqimini versiya bo‘yicha yuklash va optimistik concurrency uchun mo‘ljallanmagan. Ko‘pincha: Postgres (haqiqat) + Kafka (tarqatish).

05
Deep · GDPR

Crypto-shredding va unutilish

crypto-shredding · PII
🔒

Event Sourcing’ning huquqiy qiyinchiligi: append-only va GDPR’ning "unutilish huquqi" (o‘chirishni talab qiladi) bir-biriga zid. Eventni o‘chirib bo‘lmaydi — qanday "unutamiz"?

Ziddiyat

Foydalanuvchi "mening barcha ma’lumotimni o‘chiring" deydi. Lekin eventlar immutable, append-only — o‘chirsang, tarix buziladi, snapshotlar va projectionlar mos kelmaydi.

Yechim — crypto-shredding

PII (shaxsiy ma’lumot)ni eventda shifrlangan holda saqla, kalitni esa alohida (har foydalanuvchi uchun). "Unutish" — eventni emas, kalitni o‘chirish: shifrlangan ma’lumot endi tiklanmaydi (amalda o‘chirilgan), lekin event jurnalining tuzilishi buzilmaydi:

// CRYPTO-SHREDDING (GDPR): append-only event'ni o'chirib bo'lmaydi.
// Yechim: PII'ni har foydalanuvchi uchun alohida kalit bilan SHIFRLAYMIZ.
function storeEvent(e: any, userKey: Buffer) {
  return { ...e, email: encrypt(e.email, userKey) };   // PII shifrlangan saqlanadi
}

// "Unutilish huquqi" (right to be forgotten):
await keyStore.delete(userId);   // foydalanuvchi kalitini O'CHIRAMIZ
// -> eventlar joyida qoladi (append-only buzilmaydi),
//    lekin shifrlangan PII endi HECH QACHON o'qilmaydi = amalda o'chirilgan
Boshqa yondashuvlar va chegaralar

PII’ni ajratish: shaxsiy ma’lumotni umuman eventga solmaslik (alohida, o‘chiriladigan jadvalda saqlash, eventda faqat ID). Crypto-shredding mukammal emas (kalit boshqarish murakkab, eski backuplar masalasi) — lekin ES’da GDPR uchun amaldagi standart yondashuv. Buni boshidan rejalashtirish kerak.

06
Deep · Process

Process Manager / Saga eventlardan

orchestration · compensation
🎬

Uzoq davom etadigan biznes jarayonlarini (buyurtma → rezerv → to‘lov → yuborish) eventlar yordamida muvofiqlashtiramiz — bu Process Manager (yoki Saga).

Process Manager nima

U eventlarni tinglaydi, o‘z holatini yuritadi (jarayon qaysi bosqichda) va navbatdagi commandni chiqaradi. Ya’ni: event kiradi → holat yangilanadi → command chiqadi.

event (fakt) Process Managerholat + keyingi qadam command (niyat)
Process manager: event → holat → command (jarayonni boshqaradi)
// PROCESS MANAGER: eventlarga reaksiya qilib, O'Z holatini yuritadi va COMMAND chiqaradi
@EventsHandler(OrderPlacedEvent)
export class OrderProcessManager {
  constructor(private commands: CommandBus) {}

  async handle(e: OrderPlacedEvent) {
    // 1-qadam: inventarni rezerv qil
    await this.commands.execute(new ReserveInventoryCommand(e.orderId, e.items));
  }
}

@EventsHandler(InventoryReservedEvent)
export class OnInventoryReserved {
  async handle(e: InventoryReservedEvent) {
    // 2-qadam: to'lovni amalga oshir
    await this.commands.execute(new ChargePaymentCommand(e.orderId));
  }
}
// muvaffaqiyatsizlikda -> KOMPENSATSIYA (masalan, ReleaseInventory) -> 13-mavzu Saga
Choreography vs Orchestration

1-mavzudagi Saga’ni eslang. Choreography — markaziy boshqaruvsiz, har xizmat eventga o‘zi reaksiya qiladi (oddiy, lekin tarqoq mantiq). Orchestration / Process Manager — markaziy, holatli koordinator (murakkab jarayon uchun aniqroq). Muvaffaqiyatsizlikda kompensatsiya (orqaga qaytaruvchi amal) — 13-mavzu (Distributed Transactions) mavzusi.

07
Deep · Scale

Katta oqimlar va performance

aggregate design · catch-up
🌊

Bitta aggregatda millionlab event to‘planib qolsa — bu odatda dizayn xatosi belgisi. Senior aggregat chegaralarini to‘g‘ri tortishni biladi.

Muammo — cheksiz o‘sadigan oqim

Masalan, "do‘kon" aggregati har sotuvni o‘ziga event qilsa — yillar o‘tib millionlab event, yuklash imkonsiz sekinlashadi, snapshot ham yetmaydi.

Yechimlar

1. Aggregatni kichik tut: "do‘kon" emas, har "buyurtma" alohida aggregat (qisqa umrli, kam eventli). Bu — eng muhim yechim (DDD aggregat dizayni).
2. Oqimni yopish/aylantirish: davriy oqimlar (masalan, har oy/yil yangi oqim), eskisi arxivlanadi.
3. Snapshot (3-bo‘lim) uzunroq oqimlar uchun.

Masshtabda projection — catch-up subscription

Ko‘p eventli tizimda projectionlar eventlarni catch-up subscription orqali ketma-ket o‘qiydi: checkpoint’dan boshlab, yangilarini olib, read modelni yangilaydi (9-mavzu Deep: idempotency + checkpoint). Bu projectionlarni event oqimi bilan moslashtiradi va rebuild’ni mumkin qiladi.

Stream-per-aggregate vs category

Har aggregat o‘z oqimiga ega (order-123). Bundan tashqari "kategoriya oqimi" (order-*) — barcha buyurtma eventlari ketma-ket — projectionlar uchun qulay (bitta joydan hammasini o‘qish).

08
Deep · Arxitektura

Yig‘ma ko‘rinish va senior cheklist

To‘liq Event Sourcing arxitekturasi — yig‘ma ko‘rinish

BosqichNima bo‘ladiBo‘lim
Aggregatni yuklashSnapshot + undan keyingi eventlar (upcast bilan)1, 3
Command bajarishValidatsiya + yangi event(lar) chiqarish
AppendexpectedVersion bilan optimistik concurrency2
PublishEventlar projection va process manager‘larga6
Projection (CQRS)Idempotent + checkpoint read model7
GDPRPII crypto-shredding bilan5

Senior tayyorlik cheklisti

Savol / amaliyotBo‘lim
Event versiyalash va upcasting’ni boshidan rejalayman (eventlar abadiy)1
expectedVersion bilan optimistik concurrency qo‘llayman2
Snapshot — optimizatsiya ekanini, versiyalanishini, qayta qurilishini bilaman3
Event store tanlayman (Postgres boshlash; Kafka = transport, store emas)4
GDPR’ni crypto-shredding yoki PII ajratish bilan hal qilaman5
Uzoq jarayonlarni process manager bilan boshqaraman; kompensatsiyani bilaman6
Aggregatni kichik tutaman; katta oqim/catch-up subscription’ni boshqaraman7

Bu 10-mavzuning Deep/Senior qatlami edi — Core bilan birga, Event Sourcing endi to‘liq.

Endi yozish/o‘qish naqshlari (9–10) yakunlandi. Keyingi mavzu: “11-mavzu: CAP Theorem” — taqsimlangan tizimlarning asosiy qonuni (izchillik vs mavjudlik). Core’dan boshlaymiz.