Tizim dizayni kursi · 3-mavzu
TuzdiDinMuhammad

Event-Driven Architecture — reaksiya orqali bog‘lash

Xizmatlar bir-birini chaqirmaydi — hodisa e’lon qiladi, qiziqqan o‘zi reaksiya bildiradi. Noldan boshlab pub/sub mexanikasini, RabbitMQ implementatsiyasini, so‘ng productionda hayotiy muhim narsalarni — idempotency, outbox, DLQ, sxema versiyalash va Kafka bilan farqni o‘rganamiz.

Stack: NestJS · RabbitMQ · KafkaNaqshlar: Pub/Sub · Outbox · DLQ · IdempotencyBog‘liq: 1, 2-mavzu

Bu darsda nima bor

1–4-qismlar EDAni noldan quradi: nega kerak, 4 ta asosiy tushuncha, xabar turlari va NestJS+RabbitMQ implementatsiya. 5–6-qismlar — productionda hal qilinishi shart bo‘lgan ishonchlilik (idempotency/outbox/DLQ) va broker tanlovi. 7-qism EDAning qorong‘i tomoni va uni qachon ishlatmaslik kerakligini ochadi.

01
Event-Driven · qism 1

Nega kerak — bog‘liqlik muammosi

📢

Oddiy o‘xshatish: har bir kishiga alohida qo‘ng‘iroq qilib “buni qil” deyish o‘rniga, e’lonlar taxtasiga xabar osasiz: “Buyurtma yaratildi”. Kim qiziqsa — o‘qib, o‘z ishini qiladi. Siz kim o‘qishini, qachon o‘qishini bilishingiz shart emas.

1-mavzuda mikroservislar bir-birini to‘g‘ridan chaqirardi. Lekin shunday chaqiruv ikki tomonni mahkam bog‘laydi. Buyurtma xizmati email, ombor, analitika va sodiqlikni o‘zi chaqirsa:

// MUAMMO: hamma narsani birma-bir, SINXRON chaqiramiz
async createOrder(dto) {
  const order = await this.orders.save(dto);
  await this.email.sendConfirmation(order);    // kutamiz...
  await this.inventory.reserve(order.items);   // kutamiz...
  await this.analytics.track(order);           // kutamiz...
  await this.loyalty.addPoints(order.userId);  // kutamiz...
  return order;
  // email xizmati 7s sekin bo'lsa -> mijoz 7s kutadi
  // analytics o'lik bo'lsa -> BUTUN buyurtma yiqiladi
  // yangi "consumer" qo'shsang -> SHU kodni yana o'zgartirasan
}
Muammo — vaqtinchalik bog‘liqlik (temporal coupling)

1. Producer hamma consumer javobini kutadi — eng sekin halqa butun zanjirni sekinlashtiradi.
2. Bitta consumer o‘lik bo‘lsa — butun amal yiqiladi (mo‘rtlik).
3. Yangi reaksiya qo‘shmoqchi bo‘lsang (masalan SMS) — producer kodini har safar o‘zgartirasan (yopiq emas).
4. Producer 4 ta xizmatning manzilini, kontraktini va xatosini bilishga majbur.

Yechim — hodisaga asoslangan arxitektura

Xizmatlar bir-birini chaqirmaydi. Producer faqat “nima sodir bo‘lganini” e’lon qiladi (order.created) va darhol davom etadi. Qiziqqan consumerlar o‘zi obuna bo‘lib, asinxron reaksiya bildiradi. Producer ularni bilmaydi ham — bu bo‘sh bog‘lanish (loose coupling).

02
Event-Driven · qism 2

Asosiy g‘oya — 4 ta tushuncha

EDA atigi 4 ta tushunchadan iborat. Ularni aniq tushunsang, qolgani oson:

TushunchaMa’nosi
Event (hodisa)Sodir bo‘lib bo‘lgan fakt — o‘tgan zamonda nomlanadi: OrderCreated, PaymentFailed. O‘zgarmas (immutable), bekor qilib bo‘lmaydi.
ProducerHodisani e’lon qiluvchi. Kim ushlashini bilmaydi va bilishi shart emas.
BrokerHodisani qabul qilib, saqlab, obunachilarga yetkazuvchi vositachi (RabbitMQ, Kafka).
ConsumerHodisaga obuna bo‘lib, reaksiya bildiruvchi. Bir hodisani ko‘p consumer ushlashi mumkin.
// YECHIM: producer faqat FAKTNI e'lon qiladi, kim ushlashini bilmaydi
async createOrder(dto) {
  const order = await this.orders.save(dto);
  this.bus.emit('order.created', new OrderCreated(order));  // bir zumda qaytadi
  return order;
}
// email/inventory/analytics/loyalty -> har biri mustaqil tinglaydi.
// Yangi consumer qo'shish: producer'ga TEGMAYSAN.
Orders (producer) order.created BrokerRabbitMQ / Kafka Notifications Inventory Analytics
Producer fakt e’lon qiladi → broker → ko‘p mustaqil consumer
Muhim nuANS — bog‘liqlik teskari aylanadi

Sinxronda producer consumerni biladi. EDAda consumer producerni biladi (qaysi eventga obuna bo‘lishni o‘zi tanlaydi). Bog‘liqlik yo‘nalishi teskari aylandi — shuning uchun producer hech narsani o‘zgartirmasdan yangi consumerlar qo‘shiladi.

03
Event-Driven · qism 3

Command/Event va Queue/Pub-Sub

EDAda eng ko‘p chalkashtiriladigan narsa — xabar turlari va tarqatish shakllari. Bularni aralashtirsang, arxitekturang noto‘g‘ri ishlaydi.

Xabar turlari — Command vs Event

Command (buyruq)Event (hodisa)
Ma’noBuni qil” — buyruqBu sodir bo‘ldi” — fakt
NomlashBuyruq mayli: SendEmailO‘tgan zamon: OrderCreated
Qabul qiluvchiBitta handler (aniq egasi bor)0..N consumer (kim qiziqsa)
Bog‘liqlikYuboruvchi natijani biladiYuboruvchi kim ushlashini bilmaydi

Tarqatish shakli — Queue vs Pub/Sub

Bu eng muhim farq. Queue (navbat): bitta xabarni faqat bitta consumer oladi — yuk bir nechta nusxa o‘rtasida taqsimlanadi (competing consumers). Pub/Sub (fan-out): bitta xabarning nusxasi har bir obunachiga boradi.

QUEUE — bittasi oladi (yuk taqsimlash) Producer Queue C1 ✓ C2 faqat bittasi oladi PUB/SUB — hammasi nusxa oladi Producer Exchange C1 ✓ C2 ✓ C3 ✓
Queue = yuk taqsimlash (bir nusxa) · Pub/Sub = fan-out (har kimga nusxa)
Amaliy qoida

Bitta xizmatning bir nechta nusxasi orasida ishni bo‘lish kerakmi? → Queue (har nusxa raqobatlashadi). Turli xizmatlar bir xil hodisaga reaksiya bildirishi kerakmi? → Pub/Sub (har xizmatga o‘z navbati). RabbitMQda buni exchange + har consumerga alohida navbat orqali quriladi.

04
Event-Driven · qism 4

NestJS implementatsiya (RabbitMQ)

@EventPattern · emit · ack

1-mavzudagi loyihani davom ettiramiz, lekin endi haqiqiy pub/sub bilan: Orders bitta hodisa tashlaydi, uchta mustaqil xizmat uni oladi. RabbitMQ ishlatamiz.

Orders order.created topic exchangedomain_events notif_queue inv_queue analytics_queue Notifications Inventory Analytics
Bitta hodisa → exchange → 3 ta alohida navbat → 3 ta mustaqil consumer

1 · Producer — hodisani e’lon qiladi

// orders/src/app.module.ts — producer EVENT_BUS klientini ulaydi
ClientsModule.register([{
  name: 'EVENT_BUS',
  transport: Transport.RMQ,
  options: {
    urls: ['amqp://localhost:5672'],
    exchange: 'domain_events',     // umumiy topic-exchange
    exchangeType: 'topic',
    wildcards: true,
    queueOptions: { durable: true },
  },
}])

// orders.service.ts
this.bus.emit('order.created', {        // 'order.created' = routing key
  eventId: randomUUID(),
  orderId: order.id,
  userId: order.userId,
  total: order.total,
});

2 · Consumer — o‘z navbati bilan obuna bo‘ladi

// notifications/src/main.ts — HAR consumer O'Z navbatiga ega (fan-out shu tarzda)
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  transport: Transport.RMQ,
  options: {
    urls: ['amqp://localhost:5672'],
    exchange: 'domain_events',
    exchangeType: 'topic',
    wildcards: true,
    queue: 'notifications_queue',   // <-- inventory_queue, analytics_queue ... har xil
    routingKey: 'order.*',          // qaysi hodisalarni tinglaydi
    queueOptions: { durable: true },
    noAck: false,                   // qo'lda tasdiqlash
  },
});
await app.listen();

3 · Handler — reaksiya va tasdiq (ack)

// notifications/src/order.consumer.ts
@EventPattern('order.created')
async handle(@Payload() data: any, @Ctx() ctx: RmqContext) {
  const channel = ctx.getChannelRef();
  const msg = ctx.getMessage();
  try {
    await this.email.sendConfirmation(data.userId, data.orderId);
    channel.ack(msg);                    // muvaffaqiyat -> tasdiq
  } catch (err) {
    // requeue=false -> qayta navbatga solmaymiz, DLQ ga ketadi
    channel.nack(msg, false, false);
  }
}
Eslatma

exchange, wildcards, routingKey kabi sozlama nomlari @nestjs/microservices versiyasiga qarab biroz farq qilishi mumkin. Asosiy g‘oya o‘zgarmaydi: har consumer xizmatga alohida durable navbat + ularni bitta exchangega bog‘lash = fan-out.

05
Chuqurlashtirilgan mavzu

Ishonchlilik — idempotency, outbox, DLQ

at-least-once · outbox · DLQ
🛡️

Bu yerda “ishlaydi” bilan “productionda ishonchli” farqlanadi. Tarmoq va broker mukammal emas: xabar yo‘qolishi, ikki marta yetishi yoki tartibsiz kelishi mumkin. Professional EDA aynan shularni boshqaradi.

Yetkazish kafolatlari — afsona va haqiqat

KafolatMa’nosiAmalda
at-most-onceKo‘pi bilan 1 marta (yo‘qolishi mumkin)Kam ishlatiladi (ma’lumot yo‘qoladi)
at-least-onceKamida 1 marta (takror bo‘lishi mumkin)Standart tanlov + idempotency
exactly-onceAniq 1 martaDeyarli afsona — amalda “at-least-once + idempotency” bilan taqlid qilinadi

Demak qoida: at-least-once’ni qabul qil, consumerni idempotent qil.

1 · Idempotency — takrorga chidamlilik

// IDEMPOTENCY: bir hodisa 2 marta yetsa ham, bir marta ishlanadi
@EventPattern('order.created')
async handle(@Payload() data, @Ctx() ctx) {
  const channel = ctx.getChannelRef(); const msg = ctx.getMessage();

  if (await this.processed.exists(data.eventId)) {  // allaqachon ishlangan?
    return channel.ack(msg);                         // -> tashlab yubor (no-op)
  }
  await this.doWork(data);
  await this.processed.save(data.eventId);           // belgilab qo'yamiz
  channel.ack(msg);
}

2 · Outbox pattern — eng muhim professional naqsh

Dual-write muammosi

Bazaga yozish va brokerga event yuborish — ikki alohida tizim, ularni bitta atomik amalda bajarib bo‘lmaydi. Bazaga yozdingu, event yuborishdan oldin server qulasa — ma’lumot bor, lekin hech kim xabardor emas. Yoki teskarisi.

// 1) DUAL-WRITE muammosini hal: order + outbox BITTA tranzaksiyada
await this.prisma.$transaction([
  this.prisma.order.create({ data: order }),
  this.prisma.outbox.create({ data: {
    id: randomUUID(), type: 'order.created',
    payload: JSON.stringify(order), sent: false,
  }}),
]);
// endi "baza yozildi-yu, event yo'qoldi" holati BO'LMAYDI

// 2) Alohida RELAY yuborilmaganlarni brokerga uzatadi
@Cron('*/2 * * * * *')
async relay() {
  const pending = await this.prisma.outbox.findMany({
    where: { sent: false }, take: 50, orderBy: { createdAt: 'asc' },
  });
  for (const e of pending) {
    this.bus.emit(e.type, { eventId: e.id, ...JSON.parse(e.payload) });
    await this.prisma.outbox.update({ where: { id: e.id }, data: { sent: true } });
  }
}

3 · Dead Letter Queue (DLQ) — “o‘lik xatlar”

Bir xabar qayta-qayta xato bersa (poison message), uni cheksiz qayta urinish butun navbatni bloklaydi. Yechim — bunday xabarni alohida DLQga surib, navbatni bo‘shatish; keyin uni tinch o‘rganasan.

// DLQ: nack qilingan (ishlanmagan) xabar yo'qolmaydi, "o'lik xat navbati"ga ketadi
queueOptions: {
  durable: true,
  deadLetterExchange: 'dlx',          // nack -> shu exchange'ga
  deadLetterRoutingKey: 'order.failed',
}
// dlx -> dead_letter_queue ga bog'lanadi; u yerdagi xabarlarni
// keyin qo'lda yoki avtomatik qayta ishlaysan / tahlil qilasan
Tartib (ordering)

EDAda global tartib kafolati yo‘q. Tartib muhim bo‘lsa (masalan, bitta hisob bo‘yicha amallar), bitta kalit (aggregate id)ni bitta navbat/partisiyaga yo‘naltirasan — shunda shu kalit ichida tartib saqlanadi. Kafkada bu partition key orqali tabiiy bo‘ladi.

06
Chuqurlashtirilgan mavzu

Kafka vs RabbitMQ va event sxemasi

broker tanlovi · versioning

Ikki yetakchi broker bor va ular tubdan farq qiladi. To‘g‘ri tanlash — professional qaror.

MezonRabbitMQKafka
Model“Aqlli broker” — navbat, marshrutlash“Doimiy log” — ketma-ket yozuv
YetkazishBroker consumerga itaradi (push)Consumer o‘zi o‘qiydi (pull), offsetni boshqaradi
SaqlashIshlangach o‘chadiMuddat bo‘yicha saqlanadi — qayta o‘qish (replay) mumkin
TartibNavbat ichidaPartisiya ichida (kalit bo‘yicha)
HajmO‘rta–yuqoriJuda yuqori (millionlab/sek)
MarshrutlashKuchli (exchange, routing key)Oddiy (topic + partition)
Eng mosVazifa navbatlari, murakkab marshrut, RPCEvent oqimi, log, analitika, event sourcing, replay
Oddiy tanlov

“Ishni bajaruvchilarga taqsimla, murakkab marshrut kerak” → RabbitMQ. “Katta hajmli hodisa oqimi, tarix saqlansin, keyin qayta o‘qiy olay” → Kafka. Ko‘p kompaniyalar ikkalasini birga ishlatadi.

Event sxemasini loyihalash — Thin vs Fat

Eventga qancha ma’lumot solish kerak? Ikki yondashuv, ikki tomonlama kelishuv:

// THIN event (notification) — faqat ID. Consumer kerak bo'lsa o'zi so'rab oladi.
class OrderCreated {
  constructor(public eventId: string, public orderId: string) {}
}
// + Kichik, bog'liqlik kam.  - Consumer qo'shimcha so'rov qiladi (chaqiruv ko'payadi).

// FAT event (event-carried state transfer) — kerakli ma'lumot ichida.
class OrderCreated {
  constructor(
    public eventId: string, public orderId: string,
    public userId: string, public total: number, public items: Item[],
  ) {}
}
// + Consumer mustaqil, qo'shimcha so'rovsiz.  - Kattaroq, ma'lumot dublikati.

Sxema versiyalash — consumerlarni sindirmaslik

Event — bu xizmatlar orasidagi ommaviy kontrakt. Uni o‘zgartirsang, sen bilmagan consumerlar sinadi. Oltin qoida — faqat qo‘shimcha (backward-compatible) o‘zgartirishlar:

// SXEMA VERSIYALASH: eski consumerlarni sindirmaslik uchun FAQAT qo'shimcha
// o'zgartirishlar (additive) qil — maydon o'chirma, tip o'zgartirma.
interface OrderCreatedV1 { eventId: string; orderId: string; total: number; }
interface OrderCreatedV2 extends OrderCreatedV1 {
  currency?: string;      // YANGI maydon optional -> eski consumer buzilmaydi
}
// Katta o'zgarishda: 'order.created.v2' nomli YANGI hodisa chiqar,
// eskisini bir muddat parallel qo'llab-quvvatla, keyin to'xtat.
07
Muhim

Qorong‘i tomoni va qachon ishlatmaslik

observability · eventual consistency
🌑

EDA bepul emas. U bog‘liqlikni kamaytiradi, lekin murakkablikni boshqa joyga ko‘chiradi. Buni bilmaslik — “event spaghetti”ga olib keladi.

Asosiy xavf-xatarlar

1. Ko‘rinmas oqim: “bu event qayoqqa boradi?” — kodga qarab bilib bo‘lmaydi, chunki producer consumerni bilmaydi.
2. Yashirin bog‘liqlik: event sxemasi orqali xizmatlar baribir bog‘langan — uni o‘zgartirsang, uzoqdagi consumer sinadi.
3. Zanjirli hodisalar (event storm): bitta event boshqasini, u yana boshqasini chaqiradi — nazoratdan chiqadi.
4. Debug qiyin: xato asinxron, bir necha hop narida sodir bo‘ladi.

Yechim — Observability majburiy

EDAda kuzatuvchanliksiz yashab bo‘lmaydi. Har eventga correlation_id (butun oqimni bog‘lovchi) va causation_id (sababni ko‘rsatuvchi) qo‘shasan:

// Har eventga kuzatuv ma'lumotini qo'shamiz (12-mavzu: Observability bilan bog'liq)
this.bus.emit('order.created', {
  eventId: randomUUID(),
  correlationId: ctx.correlationId,   // BUTUN oqim bo'ylab bir xil -> trace
  causationId: ctx.currentEventId,    // bevosita sabab bo'lgan event
  ...payload,
});
// Endi loglarda correlationId bo'yicha filtrlab, butun zanjirni ko'rasan.
Pirovard izchillik mentaliteti

EDAda ma’lumot darhol izchil bo‘lmaydi. Foydalanuvchi buyurtma berdi-yu, “Mening buyurtmalarim”da hali ko‘rinmasligi mumkin (event hali ishlanmagan). UI buni hisobga olishi kerak: “Qabul qilindi, ishlanmoqda…” kabi holatlar.

Qachon ISHLATMASLIK kerak

Oddiy CRUD — bitta amal, bitta natija bo‘lsa, event keraksiz murakkablik.

Darhol javob/izchillik kerak bo‘lsa (masalan, “balansni hozir ko‘rsat”) — sinxron chaqiruv to‘g‘riroq.

Hammasini eventga aylantirma. EDA — bitta voqea ko‘p mustaqil reaksiya keltirganda yoki xizmatlarni ajratish kerak bo‘lganda kuchli. Aks holda — oddiy chaqiruvdan boshla.

08
Yakuniy

Eslab qolish kerak bo‘lgan 5 jumla

  • Event = o‘tgan zamondagi o‘zgarmas fakt (OrderCreated). Producer e’lon qiladi, kim ushlashini bilmaydi — bog‘liqlik teskari aylanadi.
  • Command = bitta egasi; Event = 0..N consumer. Queue = yuk taqsimlash (bir nusxa); Pub/Sub = fan-out (har kimga nusxa).
  • “at-least-once’ni qabul qil, consumerni idempotent qil”. Tartib kerak bo‘lsa — aggregate kaliti bo‘yicha partisiya.
  • Outbox pattern dual-write muammosini hal qiladi; DLQ poison message’dan saqlaydi; event sxemasini faqat qo‘shimcha o‘zgartir.
  • RabbitMQ = navbat/marshrut; Kafka = log/replay. EDA bepul emas — correlation_id va kuzatuvchanlik majburiy; oddiy CRUDda ishlatma.

Keyingi mavzu — tezlik va masshtabning birinchi quroli:

“4-mavzu: Caching” — Redis, cache-aside, invalidatsiya, stampede va NestJS implementatsiyasi bilan.