---
title: 'Alan Olayları'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani/alan-olaylari'
course: 'Veri Erişim Katmanı ve İş Mantığı'
language: tr
updated: '2026-08-17T18:06:50+00:00'
license: 'CC BY-SA 4.0'
---

# Alan Olayları

Yan etkilerin olay yayımıyla gevşek bağlanması: aynı yayın çağrısına birden çok dinleyicinin bağlanması, işlem içinde yayımlanan olayın geri alma sonrası ürettiği yanlış bildirimlerin sayılması ve giden kutusu düzeniyle bu sayının sıfıra indirilmesi.

Ödünç verme isteği iki doğrulama katmanından geçti ve kayıt yazılabilir duruma geldi.
Yazmayla birlikte başlaması gereken başka işler de var: üyeye bildirim gitmeli, günlük
istatistik güncellenmeli, ilerideki raporlar için kütüğe satır düşülmeli.

Bunların hepsini ödünç verme işlevinin içine yazmak, o işlevi her yeni yan etkide
değiştirmek demektir. **Alan olayı** (domain event), aradaki bağı gevşetir: ödünç verme
işlevi olup biteni bildirir, kimin ne yapacağını bilmez. Bu dersin asıl konusu bildirimin
zamanıdır — olay ne zaman yayımlanırsa doğru olur?

## Olay, Yayınlayıcı, Dinleyici

Olay geçmiş zamanda bir olguyu anlatır: "ödünç verildi". Bir istek değildir, bir emir
değildir; olmuş bir şeyin kaydıdır. Yayınlayıcı, olay türüne bağlı dinleyicileri çağırır.
Dersin dosyaları iki dizine yerleşir.

```sh
mkdir -p alan uygulama
```

```js
// alan/olay-yayinlayici.mjs — olay turune gore dinleyici cagiran en kucuk yayinlayici
export class OlayYayinlayici {
  constructor() { this.dinleyiciler = new Map(); }
  abone(tur, dinleyici) {
    if (!this.dinleyiciler.has(tur)) this.dinleyiciler.set(tur, []);
    this.dinleyiciler.get(tur).push(dinleyici);
    return this;
  }
  yayinla(olay) {
    for (const d of this.dinleyiciler.get(olay.tur) ?? []) d(olay);
    return (this.dinleyiciler.get(olay.tur) ?? []).length;
  }
}
```

Dinleyiciler yan etkilerdir. Üçü de aynı olayı alır, hiçbiri diğerini bilmez.

```js
// uygulama/dinleyiciler.mjs — olaya tepki veren uc yan etki
export const bildirimDinleyicisi = (kutu) => (olay) =>
  kutu.push({ oduncId: olay.oduncId, uyeId: olay.uyeId, kitapId: olay.kitapId,
              metin: `Kitap ${olay.kitapId} ${olay.tarih} tarihinde odunc verildi.` });

export const istatistikDinleyicisi = (sayac) => (olay) => {
  sayac[olay.tarih] = (sayac[olay.tarih] ?? 0) + 1;
};

export const kutukDinleyicisi = (satirlar) => (olay) =>
  satirlar.push(`${olay.tarih} ${olay.tur} odunc=${olay.oduncId}`);
```

Gevşek bağlanmanın ölçüsü şudur: dinleyici eklendiğinde üreten tarafın kodu değişmez.

```js
// yayin.mjs — ayni yayin cagrisi, farkli sayida dinleyici
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";
import { bildirimDinleyicisi, istatistikDinleyicisi, kutukDinleyicisi }
  from "./uygulama/dinleyiciler.mjs";

const OLAYLAR = [
  { tur: "odunc_verildi", oduncId: 1, uyeId: 1, kitapId: 5, tarih: "2025-06-20" },
  { tur: "odunc_verildi", oduncId: 2, uyeId: 3, kitapId: 7, tarih: "2025-06-20" },
];

for (const dinleyiciSayisi of [2, 3]) {
  const kutu = [], sayac = {}, kutuk = [];
  const yayinlayici = new OlayYayinlayici()
    .abone("odunc_verildi", bildirimDinleyicisi(kutu))
    .abone("odunc_verildi", istatistikDinleyicisi(sayac));
  if (dinleyiciSayisi === 3) yayinlayici.abone("odunc_verildi", kutukDinleyicisi(kutuk));

  // Ureten taraf her iki durumda da ayni tek satiri calistirir.
  for (const olay of OLAYLAR) yayinlayici.yayinla(olay);

  console.log(`dinleyici=${dinleyiciSayisi}  bildirim=${kutu.length}  ` +
              `istatistik=${JSON.stringify(sayac)}  kutuk=${kutuk.length}`);
}
```

```sh
node yayin.mjs
```

```
dinleyici=2  bildirim=2  istatistik={"2025-06-20":2}  kutuk=0
dinleyici=3  bildirim=2  istatistik={"2025-06-20":2}  kutuk=2
```

Üçüncü yan etki, yayımlayan satıra dokunulmadan eklendi. Aboneliğin kurulduğu yer uygulama
katmanıdır; alan katmanı yalnız olayın kendisini üretir ve ona kimin abone olduğunu bilmez.

## Olayın İşlem Sınırıyla İlişkisi

Yayının nereye konduğu, gevşek bağlanmadan daha ince bir sorundur. Aşağıdaki veritabanında
ödünç verme iki yazma yapar: kayıt eklenir ve kitabın stoğu bir azalır. Stok sıfırın altına
inemez; bu kısıt bazı isteklerin ikinci adımda başarısız olmasını sağlar.

```sh
# kur.sh — dersin veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    stok INTEGER NOT NULL CHECK (stok >= 0));
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, govde TEXT NOT NULL,
                           durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO uye VALUES (1,'Ayse Demir'),(2,'Bora Kaya');
INSERT INTO kitap VALUES (1,'Korluk',1),(2,'Tutunamayanlar',0),
                         (3,'Kum Kitabi',1),(4,'Puslu Kitalar',0);
SQL
```

İlk sürüm olayı kayıt eklenir eklenmez, yani işlem sınırının içinde yayımlar.

```js
// islem-icinde.mjs — olay islem icinde yayinlanir; islem geri alinsa bile dinleyici calisti
import { DatabaseSync } from "node:sqlite";
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";
import { bildirimDinleyicisi, istatistikDinleyicisi } from "./uygulama/dinleyiciler.mjs";

const db = new DatabaseSync("kutuphane.db");
const kutu = [], sayac = {};
const yayinlayici = new OlayYayinlayici()
  .abone("odunc_verildi", bildirimDinleyicisi(kutu))
  .abone("odunc_verildi", istatistikDinleyicisi(sayac));

function oduncVer(uyeId, kitapId, tarih) {
  db.exec("BEGIN");
  try {
    const oduncId = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(kitapId, uyeId, tarih).lastInsertRowid);
    yayinlayici.yayinla({ tur: "odunc_verildi", oduncId, uyeId, kitapId, tarih });
    db.prepare("UPDATE kitap SET stok = stok - 1 WHERE kitap_id = ?").run(kitapId);
    db.exec("COMMIT");
    return `verildi (odunc ${oduncId})`;
  } catch (e) {
    db.exec("ROLLBACK");
    return `geri alindi (${e.message.split("\n")[0]})`;
  }
}

const ISTEKLER = [[1, 1], [1, 2], [2, 3], [2, 4]];
for (const [uyeId, kitapId] of ISTEKLER) {
  console.log(`uye ${uyeId} kitap ${kitapId}: ${oduncVer(uyeId, kitapId, "2025-06-20")}`);
}

// Bir bildirim, veritabaninda karsiligi olmayan bir odunc icin gonderilmisse yanlistir.
const yanlis = kutu.filter((b) => db.prepare(
  "SELECT 1 FROM odunc WHERE odunc_id = ? AND kitap_id = ?").get(b.oduncId, b.kitapId) === undefined);
console.log(`odunc satiri = ${db.prepare("SELECT count(*) AS n FROM odunc").get().n}`);
console.log(`gonderilen bildirim = ${kutu.length}, yanlis bildirim = ${yanlis.length}`);
console.log(`yanlis olanlar: ${yanlis.map((b) => `odunc=${b.oduncId} kitap=${b.kitapId}`).join("  ")}`);
console.log(`istatistik = ${JSON.stringify(sayac)}`);
```

```sh
sh kur.sh
node islem-icinde.mjs
```

```
uye 1 kitap 1: verildi (odunc 1)
uye 1 kitap 2: geri alindi (CHECK constraint failed: stok >= 0)
uye 2 kitap 3: verildi (odunc 2)
uye 2 kitap 4: geri alindi (CHECK constraint failed: stok >= 0)
odunc satiri = 2
gonderilen bildirim = 4, yanlis bildirim = 2
yanlis olanlar: odunc=2 kitap=2  odunc=3 kitap=4
istatistik = {"2025-06-20":4}
```

Veritabanında iki kayıt var, gönderilen bildirim dört. İkisi hiç var olmamış ödünçler için
gitti: iki üye kendilerine verilmeyen kitapların bildirimini aldı. İstatistik de dördü
saydı; günlük rapor gerçeğin iki katını gösteriyor.

Yanlış olanlardan biri ayrıca dikkat çekicidir. `odunc=2 kitap=2` satırındaki iki numaralı
kimlik veritabanında **vardır**, ama üç numaralı kitaba aittir. Geri alınan işlemin kullandığı
kimlik serbest kaldı ve sonraki kayıt onu aldı. Bildirimi yalnız kimliğe bakarak
denetleseydik bu yanlışı göremezdik; denetim kimlik ile kitabın birlikte tutulmasına
dayanıyor.

Sorunun kaynağı yayının yerindedir. Dinleyici çağrıldığında işlem henüz kesinleşmemişti;
kesinleşmeyeceği de belli değildi. Bildirim gönderimi ise geri alınamaz — ileti gitmişse
gitmiştir. Böylece işlemin atomikliği yalnız veritabanı yazmalarını kapsadı, yan etkileri
kapsamadı.

## Giden Kutusu

Çözüm, İşlemler konusunda kurulan düzenle aynıdır: olay, doğrudan dinleyiciye değil, aynı
işlemin içinde bir tabloya yazılır. İşlem kesinleşirse ileti orada olur, geri alınırsa
olmaz. Gönderim kesinleşmeden sonra ayrı bir adımda yapılır.

```js
// giden-kutusu.mjs — olay ayni islemde tabloya yazilir, kesinlesince dinleyiciye gider
import { DatabaseSync } from "node:sqlite";
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";
import { bildirimDinleyicisi, istatistikDinleyicisi } from "./uygulama/dinleyiciler.mjs";

const db = new DatabaseSync("kutuphane.db");
const kutu = [], sayac = {};
const yayinlayici = new OlayYayinlayici()
  .abone("odunc_verildi", bildirimDinleyicisi(kutu))
  .abone("odunc_verildi", istatistikDinleyicisi(sayac));

function oduncVer(uyeId, kitapId, tarih) {
  db.exec("BEGIN");
  try {
    const oduncId = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(kitapId, uyeId, tarih).lastInsertRowid);
    const olay = { tur: "odunc_verildi", oduncId, uyeId, kitapId, tarih };
    db.prepare("INSERT INTO giden_kutusu (govde) VALUES (?)").run(JSON.stringify(olay));
    db.prepare("UPDATE kitap SET stok = stok - 1 WHERE kitap_id = ?").run(kitapId);
    db.exec("COMMIT");
    return `verildi (odunc ${oduncId})`;
  } catch (e) {
    db.exec("ROLLBACK");
    return `geri alindi (${e.message.split("\n")[0]})`;
  }
}

function gidenKutusunuBosalt() {
  const bekleyen = db.prepare(
    "SELECT ileti_id, govde FROM giden_kutusu WHERE durum = 'bekliyor' ORDER BY ileti_id").all();
  for (const i of bekleyen) {
    yayinlayici.yayinla(JSON.parse(i.govde));
    db.prepare("UPDATE giden_kutusu SET durum = 'gonderildi' WHERE ileti_id = ?").run(i.ileti_id);
  }
  return bekleyen.length;
}

const ISTEKLER = [[1, 1], [1, 2], [2, 3], [2, 4]];
for (const [uyeId, kitapId] of ISTEKLER) {
  console.log(`uye ${uyeId} kitap ${kitapId}: ${oduncVer(uyeId, kitapId, "2025-06-20")}`);
}
console.log(`giden kutusunda bekleyen = ${db.prepare(
  "SELECT count(*) AS n FROM giden_kutusu WHERE durum = 'bekliyor'").get().n}`);
console.log(`gonderilen ileti = ${gidenKutusunuBosalt()}`);

const yanlis = kutu.filter((b) => db.prepare(
  "SELECT 1 FROM odunc WHERE odunc_id = ? AND kitap_id = ?").get(b.oduncId, b.kitapId) === undefined);
console.log(`odunc satiri = ${db.prepare("SELECT count(*) AS n FROM odunc").get().n}`);
console.log(`gonderilen bildirim = ${kutu.length}, yanlis bildirim = ${yanlis.length}`);
console.log(`istatistik = ${JSON.stringify(sayac)}`);
```

```sh
sh kur.sh
node giden-kutusu.mjs
```

```
uye 1 kitap 1: verildi (odunc 1)
uye 1 kitap 2: geri alindi (CHECK constraint failed: stok >= 0)
uye 2 kitap 3: verildi (odunc 2)
uye 2 kitap 4: geri alindi (CHECK constraint failed: stok >= 0)
giden kutusunda bekleyen = 2
gonderilen ileti = 2
odunc satiri = 2
gonderilen bildirim = 2, yanlis bildirim = 0
istatistik = {"2025-06-20":2}
```

İstekler aynı, sonuçlar aynı; yanlış bildirim sayısı ikiden sıfıra indi. Geri alınan iki
işlemin giden kutusu satırı da geri alındığı için ortada gönderilecek ileti kalmadı.
İstatistik de artık iki diyor.

Kazanılan şey, olayın **kesinleşmiş bir olguyu** anlatmasıdır. İşlem içinde yayımlanan olay
bir olgu değil, bir tahmindir; giden kutusundaki olay ise veritabanına yazılmış bir gerçeğin
kaydıdır.

## Yan Etki Ana İşlemi Belirlememeli

Yayının yeri ikinci bir sorunu daha çözer. Dinleyici doğrudan çağrılıyorsa, dinleyicinin
hatası ödünç verme işlemini de geçersiz kılar. Aşağıdaki betik bildirim servisinin bir kitap
için yanıt vermediği durumu iki düzende de dener.

```js
// dinleyici-hatasi.mjs — bildirim gonderimi hata verirse odunc kaydinin akibeti ne olur
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";

function kur(dosya) {
  rmSync(dosya, { force: true });
  const db = new DatabaseSync(dosya);
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                               uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
           CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, govde TEXT NOT NULL,
                                      durum TEXT NOT NULL DEFAULT 'bekliyor');`);
  return db;
}

// Bildirim servisi kitap 3 icin bir kez hata veriyor.
const arizaliDinleyici = (kutu, arizali) => (olay) => {
  if (arizali.has(olay.kitapId)) throw new Error(`bildirim servisi yanit vermedi (kitap ${olay.kitapId})`);
  kutu.push(olay.oduncId);
};

const ISTEKLER = [[1, 1], [2, 3], [1, 5]];

function dogrudanYayin() {
  const db = kur("dogrudan.db");
  const kutu = [], arizali = new Set([3]);
  const y = new OlayYayinlayici().abone("odunc_verildi", arizaliDinleyici(kutu, arizali));
  for (const [uyeId, kitapId] of ISTEKLER) {
    db.exec("BEGIN");
    try {
      const oduncId = Number(db.prepare(
        "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,'2025-06-20',NULL)")
        .run(kitapId, uyeId).lastInsertRowid);
      y.yayinla({ tur: "odunc_verildi", oduncId, uyeId, kitapId });
      db.exec("COMMIT");
    } catch { db.exec("ROLLBACK"); }
  }
  return { odunc: db.prepare("SELECT count(*) AS n FROM odunc").get().n, bildirim: kutu.length };
}

function gidenKutusuyla() {
  const db = kur("kutulu.db");
  const kutu = [], arizali = new Set([3]);
  const y = new OlayYayinlayici().abone("odunc_verildi", arizaliDinleyici(kutu, arizali));
  for (const [uyeId, kitapId] of ISTEKLER) {
    db.exec("BEGIN");
    const oduncId = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,'2025-06-20',NULL)")
      .run(kitapId, uyeId).lastInsertRowid);
    db.prepare("INSERT INTO giden_kutusu (govde) VALUES (?)")
      .run(JSON.stringify({ tur: "odunc_verildi", oduncId, uyeId, kitapId }));
    db.exec("COMMIT");
  }
  const bosalt = () => {
    for (const i of db.prepare(
      "SELECT ileti_id, govde FROM giden_kutusu WHERE durum = 'bekliyor' ORDER BY ileti_id").all()) {
      try {
        y.yayinla(JSON.parse(i.govde));
        db.prepare("UPDATE giden_kutusu SET durum = 'gonderildi' WHERE ileti_id = ?").run(i.ileti_id);
      } catch { /* ileti bekliyor durumunda kalir, sonraki turda denenir */ }
    }
    return db.prepare("SELECT count(*) AS n FROM giden_kutusu WHERE durum = 'bekliyor'").get().n;
  };
  const ilkTur = bosalt();
  arizali.clear();                       // bildirim servisi ayaga kalkti
  const ikinciTur = bosalt();
  return { odunc: db.prepare("SELECT count(*) AS n FROM odunc").get().n,
           bildirim: kutu.length, ilkTur, ikinciTur };
}

const a = dogrudanYayin();
console.log(`dogrudan yayin : odunc satiri=${a.odunc}  bildirim=${a.bildirim}`);
const b = gidenKutusuyla();
console.log(`giden kutusu   : odunc satiri=${b.odunc}  bildirim=${b.bildirim}  ` +
            `bekleyen ileti: ilk tur=${b.ilkTur} ikinci tur=${b.ikinciTur}`);
```

```sh
node dinleyici-hatasi.mjs
```

```
dogrudan yayin : odunc satiri=2  bildirim=2
giden kutusu   : odunc satiri=3  bildirim=3  bekleyen ileti: ilk tur=1 ikinci tur=0
```

Doğrudan yayında üç isteğin ikisi kaydedildi. Üçüncü kitabın ödüncü, kitapla ya da üyeyle
ilgili hiçbir sorun olmamasına rağmen kaybedildi; kaydın geri alınmasına bildirim servisinin
erişilemez olması karar verdi. Giden kutusunda üç kayıt da yazıldı, iletinin biri ilk turda
gönderilemeyip beklemede kaldı, servis yeniden erişilebilir olunca ikinci turda gönderildi.

Ayrım, kalıcılık ile teslimin farklı güvenceler taşımasıdır. Ödünç kaydının yazılması
işlemin garantisidir; bildirimin gönderilmesi ise **sonunda** olacak bir iştir. Etkisiz
İşlemler dersindeki koşul burada da geçerlidir: bekleyen ileti yeniden denendiği için
dinleyici aynı olayı iki kez alabilir ve bunun ikinci kez etki üretmemesi gerekir.

## Olayın Sınırları

Olay yayımı bedelsiz değildir; iki tuzağı vardır.

Birincisi **gizli denetim akışı**dır. Ödünç verme işlevini okuyan biri yayın satırını görür
ama ne olacağını göremez; abonelikler başka bir dosyadadır. Bir kural gerçekten iş akışının
zorunlu parçasıysa — örneğin kitabın durumunun güncellenmesi — onu olaya bağlamak akışı
görünmez kılar. Ölçüt şudur: **işlemin doğruluğu için gerekli olan iş dinleyiciye
verilmez.** Dinleyiciye verilen iş, yapılmasa da ödünç kaydının geçerli kaldığı iştir.

İkincisi **sıra ve zamanlama**dır. Giden kutusu düzeninde dinleyici işlemden sonra çalışır;
arada geçen sürede veri değişmiş olabilir. Dinleyici, olayın taşıdığı bilgiyle yetinmeli ya
da veriyi yeniden okuyup güncel duruma göre davranmalıdır. Olayın içine kararı verecek
bilginin konması bu yüzden yararlıdır: `oduncId` ve `kitapId` olayın içinde olduğu için
dinleyici sorgu yapmadan bildirim metnini kurabildi.

## Özet

- Alan olayı geçmiş zamanda bir olguyu anlatır; yayımlayan taraf kimin abone olduğunu
  bilmez. Üçüncü dinleyici eklendiğinde yayımlayan satır değişmedi.
- Olay işlem içinde yayımlandığında geri alınan iki işlem için de bildirim gitti: iki kayıt
  karşılığında dört bildirim, ikisi yanlış; istatistik gerçeğin iki katını saydı.
- Geri alınan işlemin kimliği serbest kaldığı için yanlış bildirimlerden biri var olan bir
  kimliği taşıyordu; denetimin kimlik ile kitabı birlikte kontrol etmesi gerekti.
- Olay aynı işlemde giden kutusu tablosuna yazılıp kesinleşmeden sonra gönderildiğinde
  yanlış bildirim sayısı sıfıra indi.
- Dinleyici doğrudan çağrıldığında bildirim servisinin hatası geçerli bir ödünç kaydını geri
  aldırdı; giden kutusunda kayıt yazıldı, ileti beklemede kaldı ve sonraki turda gönderildi.

## Sonraki Adım

Bu konudaki dört ders alan katmanını adım adım yalnızlaştırdı: bağımlılık yönü kuralıyla
altyapıdan, veri aktarım nesneleriyle dış sözleşmeden, doğrulama ayrımıyla girdi biçiminden,
olaylarla yan etkilerden ayrıldı. Geriye son bir soru kalıyor. Alan kuralları hâlâ bir depo
alıyor, bir işlem sınırının içinde çalışıyor ve sınanmak için bir veritabanı dosyası
bekliyor. Kuralların kendisi bunların hiçbirine ihtiyaç duyuyor mu? Kursun son dersi iş
kurallarını saf fonksiyonlarla yazar, veritabanı olmadan sınar, aynı kuralın veritabanına bağlı
sürümüyle sınama süresini karşılaştırır ve alan modülünde kalıcılık izi kalmadığını bir
taramayla gösterir.
