Tizim dizayni kursi · 10-mavzu
TuzdiDinMuhammad

Event Sourcing — holat o‘rniga eventlar

Joriy holatni saqlash o‘rniga, unga olib kelgan barcha eventlar ketma-ketligini saqlaymiz — holat esa ularning natijasi. Bu to‘liq tarix, audit va temporal so‘rovlarni beradi. Noldan boshlab asosiy g‘oya, event store va rehydration, NestJS AggregateRoot implementatsiyasi, snapshot va CQRS bilan bog‘lanishni o‘rganamiz.

Stack: NestJS · @nestjs/cqrs · PostgreSQLMavzular: event store · rehydration · snapshotBog‘liq: 9-mavzu (CQRS), 3 (Event-Driven)

Bu darsda nima bor

1–4-qismlar Event Sourcing’ni noldan quradi: nega kerak, eventlarni saqlash g‘oyasi, event store va rehydration, NestJS AggregateRoot implementatsiya. 5–6-qismlar — snapshot (tezlik), CQRS bilan bog‘lanish va afzallik/qiyinchiliklar. 7-qism qachon ES kerak (va qachon ortiqcha) ekanini hal qiladi.

01
Event Sourcing · qism 1

Nega kerak — tarix yo‘qoladi

🧾

Oddiy o‘xshatish: bank hisobida shunchaki "balans = 100" deb yozilsa, bu pul qayerdan kelganini bilmaysiz. Lekin bank har bir tranzaksiyani (kirim, chiqim) yozadi — balans esa ulardan kelib chiqadi. Daftar (ledger) — haqiqat, balans — natija.

An’anaviy CRUD’da biz faqat joriy holatni saqlaymiz: UPDATE orders SET status='shipped'. Eski holat ustiga yoziladi va yo‘qoladi.

Muammo — tarix yo‘qoladi

1. "Bu buyurtma nega bekor qilindi? Kim, qachon o‘zgartirdi?" — javob yo‘q, faqat oxirgi holat bor.
2. Audit (kim nima qildi) yo‘q — moliya, tibbiyot, huquqda bu qabul qilinmaydi.
3. "O‘tgan seshanba holat qanday edi?" (temporal so‘rov) — javob bera olmaysiz.
4. Bug topilsa, "qanday shu holatga keldik?"ni qayta tiklab bo‘lmaydi.

Yechim — Event Sourcing

Joriy holat o‘rniga, unga olib kelgan barcha eventlar ketma-ketligini saqlash. Joriy holat — eventlarni qayta o‘ynatish (replay) orqali hosil qilinadi. Eventlar — o‘zgarmas (immutable), faqat qo‘shiladigan (append-only) faktlar. Event jurnali — haqiqat manbai.

02
Event Sourcing · qism 2

Asosiy g‘oya — eventlarni saqlash

State emas, faktlar oqimini saqlaymiz. Joriy holat — ularning yig‘indisi.

CRUD (faqat holat) balans = 100tarix yo‘q Event Sourcing (faktlar) +150 kirim −30 chiqim −20 chiqim replay (yig‘indi) balans = 100+ to‘liq tarix
Holat — eventlarning natijasi; eventlar esa to‘liq saqlanadi

Tanish analogiya: Git ham shunday ishlaydi — u faqat fayllarning oxirgi holatini emas, har bir commit (o‘zgarish)ni saqlaydi; istalgan vaqtga qaytish, tarixni ko‘rish mumkin. Event Sourcing — ma’lumotlaringiz uchun "git".

Asosiy xususiyatlar

Append-only: eventlar hech qachon o‘zgartirilmaydi yoki o‘chirilmaydi, faqat yangisi qo‘shiladi. Immutable: bo‘lib o‘tgan fakt — o‘zgarmaydi. O‘tgan zamon: eventlar nomlari fakt sifatida (OrderCreated, ItemAdded, OrderShipped) — niyat (command) emas, bo‘lib o‘tgan narsa.

03
Event Sourcing · qism 3

Event store va rehydration

stream · fold · rehydration

Eventlar har aggregat (masalan, har buyurtma) uchun alohida oqimda (stream) saqlanadi. Joriy holatni olish uchun ularni yuklab, tartib bilan qo‘llaymiz — bu rehydration (qayta tiklash).

OrderCreated ItemAdded ItemAdded OrderShipped holat eventlarni tartib bilan qo‘llaymiz (fold) → joriy holat
Event stream → har eventni navbat bilan apply → joriy holat (rehydration)

Misol: bo‘sh buyurtma → OrderCreated (status=draft) → ItemAdded (2 ta mahsulot) → ItemAdded (yana 1 ta) → OrderShipped (status=shipped). Eventlarni boshidan qo‘llasak — aniq joriy holatga kelamiz. Bu deterministik: bir xil eventlar → har doim bir xil holat.

Event = fakt, command = niyat

Farqni eslang (3 va 9-mavzular): Command — "buyurtma yarat" (niyat, rad etilishi mumkin). Event — "buyurtma yaratildi" (bo‘lib o‘tgan fakt, o‘zgarmaydi). Command muvaffaqiyatli bajarilsa — bir yoki bir nechta event chiqaradi va aynan o‘shalar saqlanadi.

04
Event Sourcing · qism 4

NestJS implementatsiya (AggregateRoot)

apply · loadFromHistory

NestJS’ning @nestjs/cqrs moduli Event Sourcing uchun AggregateRoot beradi: u apply() bilan event chiqaradi va loadFromHistory() bilan eventlardan tiklanadi.

1 · Eventlar

// Eventlar — O'TGAN ZAMONDA bo'lib o'tgan faktlar (immutable)
export class OrderCreatedEvent { constructor(public id: string, public userId: string) {} }
export class ItemAddedEvent    { constructor(public id: string, public sku: string, public qty: number) {} }
export class OrderShippedEvent { constructor(public id: string) {} }

2 · Aggregate — eventlardan holat quradi

// Aggregate — eventlardan o'z holatini quradi (@nestjs/cqrs)
export class Order extends AggregateRoot {
  private id: string;
  private items: { sku: string; qty: number }[] = [];
  private status: "draft" | "shipped" = "draft";

  // COMMAND metodlari: tekshiradi va YANGI event chiqaradi (apply)
  createOrder(id: string, userId: string) {
    this.apply(new OrderCreatedEvent(id, userId));        // fakt qo'shildi
  }
  addItem(sku: string, qty: number) {
    if (this.status === "shipped") throw new Error("Yuborilgan buyurtma yopilgan");
    this.apply(new ItemAddedEvent(this.id, sku, qty));
  }

  // EVENT HANDLER'lar: event holatga QANDAY ta'sir qilishini belgilaydi
  onOrderCreatedEvent(e: OrderCreatedEvent) { this.id = e.id; this.status = "draft"; }
  onItemAddedEvent(e: ItemAddedEvent)       { this.items.push({ sku: e.sku, qty: e.qty }); }
  onOrderShippedEvent(e: OrderShippedEvent) { this.status = "shipped"; }
}

3 · Event store — append va rehydration

// Holatni QAYTA QURISH (rehydration) va saqlash (append-only)
async load(orderId: string): Promise<Order> {
  const events = await this.eventStore.getEvents(orderId);  // shu aggregat eventlari
  const order = new Order();
  order.loadFromHistory(events);    // har eventni navbat bilan apply -> joriy holat
  return order;
}

async save(order: Order) {
  const newEvents = order.getUncommittedEvents();           // faqat YANGI eventlar
  await this.eventStore.append(order.id, newEvents);        // APPEND (insert), update emas
  order.commit();                                           // eventlarni publish (projection'larga)
}
Event store nima

Event store — eventlarni saqlaydigan append-only ombor. Maxsus yechim (EventStoreDB) yoki oddiy PostgreSQL jadval (events(aggregate_id, version, type, data, created_at)) bo‘lishi mumkin. Sizning stack’ingizda Postgres jadvali bilan ham boshlash mumkin.

05
Chuqurlashtirilgan mavzu

Snapshot va CQRS bilan bog‘lanish

snapshot · projection
📸

Event Sourcing ikki tabiiy hamrohga ega: tezlik uchun snapshot, o‘qish uchun CQRS.

Snapshot — minglab eventni qayta o‘ynatmaslik

Muammo

Bir aggregatda 10000 ta event bo‘lsa, har yuklashda hammasini qayta o‘ynatish sekin.

Yechim — snapshot: davriy ravishda joriy holatni saqlab qo‘yish; yuklashda snapshot’dan boshlab, faqat undan keyingi eventlarni qo‘llash:

// SNAPSHOT: minglab eventni har safar qayta o'ynatish sekin -> davriy holat saqlaymiz
async load(orderId: string): Promise<Order> {
  const snap = await this.snapshots.getLatest(orderId);     // oxirgi snapshot (masalan, v100)
  const order = snap ? Order.fromSnapshot(snap) : new Order();
  const from = snap ? snap.version : 0;
  const events = await this.eventStore.getEvents(orderId, { after: from });  // FAQAT keyingilari
  order.loadFromHistory(events);                            // v100 dan keyingi eventlar
  return order;
}

Nega CQRS bilan juft (9-mavzu)

Event store "buyurtma #5 holati"ni tez beradi, lekin "barcha yuborilgan buyurtmalar" kabi so‘rovni bera olmaydi — buning uchun har birini qayta o‘ynatish kerak bo‘lardi. Yechim: eventlardan projection (CQRS read model) qurish. Shuning uchun ES deyarli doim CQRS bilan keladi:

Event storeyozuv haqiqati (ES) eventlar Projection Read model
ES (yozuv) → eventlar → projection → o‘qishga qulay read model (CQRS)
06
Chuqurlashtirilgan mavzu

Afzalliklar va qiyinchiliklar

audit · event versioning
⚖️

Event Sourcing kuchli imkoniyatlar beradi, lekin jiddiy qiyinchiliklar ham keltiradi. Senior ikkalasini ham biladi.

Afzalliklar
  • To‘liq audit/tarix — har o‘zgarish abadiy saqlanadi (moliya, huquq)
  • Temporal so‘rov — istalgan o‘tmish holatini tiklash mumkin
  • Debug/replay — bug holatini qayta o‘ynatib tahlil qilish
  • Istalgan read modelni qurish — eventlardan yangi ko‘rinish (9-mavzu rebuild)
  • Tabiiy event-driven — eventlar allaqachon bor (3-mavzu)
Qiyinchiliklar
  • Event versiyalash — eventlar abadiy yashaydi; sxema vaqt o‘tib o‘zgarsa, eskilarini ham qo‘llay olish kerak
  • O‘chirish qiyin — append-only; GDPR "o‘chirish huquqi" murakkab
  • O‘qish to‘g‘ridan emas — joriy holat uchun projection kerak (eventual consistency)
  • O‘rganish egriligi — jamoaga yangi fikrlash tarzi
Eng katta amaliy qiyinchilik — event versiyalash

Bugun saqlagan event 5 yildan keyin ham o‘qilishi kerak. Lekin biznes o‘zgaradi, event tuzilishi o‘zgaradi. Eski eventlarni yangi shaklga moslashtirish (upcasting) — boshidan rejalanishi shart. Bu Deep qatlamning asosiy mavzusi.

07
Qaror

Qachon Event Sourcing (va qachon yo‘q)

🎯

Muhim: Event Sourcing — eng og‘ir naqshlardan biri. Oddiy CRUD ilovaga uni qo‘llash — katta xato (over-engineering). To‘g‘ri joyda esa — beqiyos qiymat.

ES qachon arziydi
  • Audit/tarix — asosiy talab (moliya, bank, tibbiyot, huquq)
  • Temporal so‘rovlar kerak ("o‘sha paytda qanday edi")
  • Boy domen, murakkab xulq-atvor
  • "Nega shunday bo‘ldi" — debug/tahlil muhim
ES qachon ortiqcha
  • Oddiy CRUD (CRUD yetadi)
  • Tarix muhim emas
  • Jamoa tajribasi/tayyorligi yo‘q
  • Kichik, oddiy domen
Amaliy qoidalar

1. Butun tizimga emas, faqat tarix/audit muhim bo‘lgan aggregatlarga qo‘lla (masalan, faqat "to‘lov" yoki "buyurtma"). Qolgan qismi oddiy CRUD bo‘laversin.

2. O‘qish uchun CQRS projectionni boshidan rejala (ES yolg‘iz so‘rovga noqulay).

3. Event versiyalash intizomini birinchi kundan o‘rnat (eventlar abadiy).

4. Snapshot strategiyasini katta oqimlar uchun rejala.

08
Yakuniy

Eslab qolish kerak bo‘lgan 5 jumla

  • Event Sourcing = joriy holat o‘rniga barcha eventlarni (o‘zgarmas, append-only faktlar) saqlash. Holat — eventlarni qayta o‘ynatish (rehydration) natijasi.
  • Event = o‘tgan zamondagi fakt (OrderShipped); command = niyat. Event jurnali — haqiqat manbai. Analogiya: bank daftari, git.
  • NestJS: AggregateRoot + apply() (event chiqarish) + loadFromHistory() (tiklash); event store append-only.
  • Ikki hamroh: snapshot (minglab eventni qayta o‘ynamaslik) va CQRS (projection — o‘qish uchun, chunki ES so‘rovga noqulay).
  • Afzallik: audit, tarix, temporal, replay. Qiyinchilik: event versiyalash, o‘chirish (GDPR), murakkablik. Faqat tarix muhim aggregatlarga qo‘lla — CRUD uchun ishlatma.

Bu mavzuning Deep/Senior qatlami (event versiyalash/upcasting, snapshot strategiyalari, optimistik concurrency + expectedVersion, GDPR/crypto-shredding, event store tanlovi — EventStoreDB vs Postgres vs Kafka, process manager/saga eventlardan, katta oqimlar) kerak bo‘lsa — ayting.

Yoki keyingi mavzu: “11-mavzu: CAP Theorem” — taqsimlangan tizimlarning asosiy qonuni.