---
title: 'Saga Kalıbı'
source: 'https://academia.sh/tr/kurslar/servis-mimarileri/saga-kalibi'
course: 'Servis Mimarileri'
language: tr
updated: '2026-08-17T18:06:53+00:00'
license: 'CC BY-SA 4.0'
---

# Saga Kalıbı

Telafi edici işlemlerle yürüyen iş akışının iki gerçekleştirim kısıtı: adım sırasının telafi edilebilirliğe göre seçilmesi ve her hata noktasında geride kalan geri alınamayan yan etkinin sayılması; koreografi ile yürütücünün kod tarafındaki farkı — akış bilgisini taşıyan dosya, adlandırılan adım, bir adım eklenince dokunulan dosya.

Önceki ders iki aşamalı işlemenin bedelini gösterdi: kaynak, en yavaş katılımcının hızı kadar
tutuldu ve eşgüdümcü düştüğünde askıda kaldı. Saga kalıbı bu pencereyi tamamen kaldırır. Her
adım kendi işlemini hemen kesinleştirir; bir adım başarısız olursa tamamlananlar tersten
**telafi edilir**.

Telafi adımının kendisi M16/K04'ün `dağıtık işlem sorunu` dersinde kurulmuştur, telafi edici
işlemin bedeli M19/K05'in `dağıtık doğruluk` konusunda ölçülmüştür. İkisi de burada
tekrarlanmıyor. Bu dersin ölçtüğü şey ikisinin de değinmediği bir kısıttır: **adımların
sırası**. Sıra keyfî değildir, çünkü her adım geri alınabilir değildir; ve sıranın seçimi,
başarısızlık anında sistemden dışarı çıkmış ve geri çağrılamayan etkinin sayısını belirler.

## Düzenek

**DT3.** Bu derste servisler ayrı süreçler değil, aynı süreçte çalışan ve **ayrı depolara**
yazan modüllerdir. Modelin taşıdığı özellik, her adımın kendi işlem sınırında kesinleşmesi ve
ortak bir geri alma çağrısının bulunmamasıdır. Ağ gecikmesi ve kısmi hata modelde yoktur.

**DT4.** Bildirim adımının dış etkisi, bir dosyaya satır yazmakla modellenir. Satırı silmek
teslimi geri almadığı için bu adımın telafisi **yoktur**; yapılabilecek tek şey ikinci bir
bildirimle düzeltmedir.

```sh
# kur.sh — dort servisin ayri depolari ve dis dunyaya giden bildirim gunlugu
rm -f katalog.db uyelik.db ucret.db gonderilen.log
sqlite3 katalog.db "CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, durumu TEXT NOT NULL);
                    INSERT INTO kitap VALUES (1,'rafta'),(2,'rafta');"
sqlite3 uyelik.db  "CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, acik_odunc INTEGER NOT NULL);
                    INSERT INTO uye VALUES (4,0);"
sqlite3 ucret.db   "CREATE TABLE hareket (id INTEGER PRIMARY KEY, uye_id INTEGER, tutar INTEGER);"
```

```js
// adimlar.mjs — odunc akisinin dort adimi; her adimin telafisi ya vardir ya yoktur
import { DatabaseSync } from "node:sqlite";
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";

const ac = (d) => new DatabaseSync(d);
const katalogDb = ac("katalog.db"), uyelikDb = ac("uyelik.db"), ucretDb = ac("ucret.db");
const gonder = (satir) => appendFileSync("gonderilen.log", satir + "\n");

export const adimlar = {
  "katalog.ayirt": {
    yap: (i) => katalogDb.prepare("UPDATE kitap SET durumu='odunc' WHERE kitap_id=?").run(i.kitap),
    telafi: (i) => katalogDb.prepare("UPDATE kitap SET durumu='rafta' WHERE kitap_id=?").run(i.kitap) },
  "ucret.tahsil": {
    yap: (i) => ucretDb.prepare("INSERT INTO hareket (uye_id,tutar) VALUES (?,5)").run(i.uye),
    telafi: (i) => ucretDb.prepare("INSERT INTO hareket (uye_id,tutar) VALUES (?,-5)").run(i.uye) },
  "uyelik.oduncEkle": {
    yap: (i) => uyelikDb.prepare("UPDATE uye SET acik_odunc=acik_odunc+1 WHERE uye_id=?").run(i.uye),
    telafi: (i) => uyelikDb.prepare("UPDATE uye SET acik_odunc=acik_odunc-1 WHERE uye_id=?").run(i.uye) },
  "bildirim.gonder": {                       // sistemin disina cikar: geri alinamaz
    yap: (i) => gonder(`uye ${i.uye}: kitap ${i.kitap} hazir`),
    telafi: null, duzeltme: (i) => gonder(`uye ${i.uye}: kitap ${i.kitap} icin onceki bildirim gecersiz`) },
};

export function sifirla() {
  katalogDb.exec("UPDATE kitap SET durumu='rafta'");
  uyelikDb.exec("UPDATE uye SET acik_odunc=0");
  ucretDb.exec("DELETE FROM hareket");
  writeFileSync("gonderilen.log", "");
}
export const gonderim = () =>
  readFileSync("gonderilen.log", "utf8").split("\n").filter((s) => s.length > 0);
```

Dört adımın üçünün telafisi var, birinin yok. Ayırt edici tek fark budur ve bütün sıralama
kısıtı bu farktan doğar.

## Sıralama Kısıtı

Aynı dört adım iki farklı sıraya dizilir ve her sıra bütün hata noktaları için ayrı ayrı
koşturulur. Ölçülen nicelik, telafi bittikten sonra geride kalan **geri alınamayan yan
etki** sayısıdır.

```js
// tarama.mjs — ayni dort adim, iki farkli sira; her hata noktasi ayri ayri taranir
import { adimlar, sifirla, gonderim } from "./adimlar.mjs";

const siralar = {
  "A (bildirim erken)": ["katalog.ayirt", "bildirim.gonder", "ucret.tahsil", "uyelik.oduncEkle"],
  "B (bildirim sonda)": ["katalog.ayirt", "ucret.tahsil", "uyelik.oduncEkle", "bildirim.gonder"],
};

function kos(sira, hataAdimi, istek) {
  sifirla();
  const yapilan = [];
  let telafi = 0, geriAlinamayan = 0, duzeltme = 0;
  try {
    for (const ad of sira) {
      if (yapilan.length + 1 === hataAdimi) throw new Error(`${ad} basarisiz`);
      adimlar[ad].yap(istek); yapilan.push(ad);
    }
  } catch {
    for (const ad of [...yapilan].reverse()) {
      const a = adimlar[ad];
      if (a.telafi) { a.telafi(istek); telafi += 1; }
      else { geriAlinamayan += 1; if (a.duzeltme) { a.duzeltme(istek); duzeltme += 1; } }
    }
  }
  return { tamamlanan: yapilan.length, telafi, geriAlinamayan, duzeltme, gonderim: gonderim().length };
}

for (const [ad, sira] of Object.entries(siralar)) {
  console.log(`\n${ad}: ${sira.join(" > ")}`);
  console.log("hata_adimi tamamlanan telafi geri_alinamayan duzeltme toplam_gonderim");
  let toplam = 0;
  for (let k = 1; k <= sira.length; k++) {
    const s = kos(sira, k, { kitap: 1, uye: 4 });
    toplam += s.geriAlinamayan;
    console.log(`     ${k}          ${s.tamamlanan}        ${s.telafi}           ` +
                `${s.geriAlinamayan}            ${s.duzeltme}          ${s.gonderim}`);
  }
  console.log(`toplam geri alinamayan yan etki: ${toplam}`);
}
```

```sh
sh kur.sh
node tarama.mjs
```

```
A (bildirim erken): katalog.ayirt > bildirim.gonder > ucret.tahsil > uyelik.oduncEkle
hata_adimi tamamlanan telafi geri_alinamayan duzeltme toplam_gonderim
     1          0        0           0            0          0
     2          1        1           0            0          0
     3          2        1           1            1          2
     4          3        2           1            1          2
toplam geri alinamayan yan etki: 2

B (bildirim sonda): katalog.ayirt > ucret.tahsil > uyelik.oduncEkle > bildirim.gonder
hata_adimi tamamlanan telafi geri_alinamayan duzeltme toplam_gonderim
     1          0        0           0            0          0
     2          1        1           0            0          0
     3          2        2           0            0          0
     4          3        3           0            0          0
toplam geri alinamayan yan etki: 0
```

Aynı adımlar, aynı iş, aynı telafi kodu. Fark yalnız sıradadır ve sonuç dört hata noktasında
**2'ye karşı 0**'dır. A sırasında bildirim ikinci adım olduğu için, ondan sonraki her
başarısızlıkta üyeye yanlış bir haber gitmiş olur; geri alınamadığı için tek yapılabilen,
üyeye ikinci bir ileti daha göndermektir. Toplam gönderim `0` değil `2`'dir: iş
gerçekleşmediği hâlde iki teslim yapılmıştır.

B sırası bunu sıfıra indirir ama bedelsiz değildir. B'nin son satırında telafi sayısı **3**;
A'nın aynı satırında **2**. Telafi edilemeyen adım sona alındığında, o adım başarısız
olduğunda geri sarılacak iş en fazladır. Yani sıralama kararı iki nicelik arasında yapılır:
**geri alınamayan yan etki** ile **geri sarılan tamamlanmış adım**. Birincisi kullanıcıya
görünür ve düzeltilemez, ikincisi yalnız pahalıdır. Kural bu asimetriden çıkar: telafi
edilemeyen adımlar sıranın sonuna konur.

## Aynı Akış, İki Gerçekleştirme

Sıra belirlendikten sonra geriye onu kimin yürüteceği kalır. İki gerçekleştirme vardır ve
ikisi de aynı sonucu üretir. **Yürütücü** kipinde akış tek bir modülde durur; servis modülleri
sıradaki yerlerini bilmez.

```js
// yurutucu.mjs — akisin tamami tek modulde; servis modulleri akisi hic bilmez
import { adimlar } from "./adimlar.mjs";
export const sira = ["katalog.ayirt", "ucret.tahsil", "uyelik.oduncEkle", "bildirim.gonder"];
export function yurut(istek) {
  const yapilan = [];
  for (const ad of sira) { adimlar[ad].yap(istek); yapilan.push(ad); }
  return yapilan;                       // telafi sirasi da bu listeden okunur
}
```

**Koreografi** kipinde merkezî bir yürütücü yoktur; her servis kendinden sonrakini tetikler.
Telafi de geriye doğru yürüdüğü için her düğüm önceki komşusunu da bilmek zorundadır.

```js
// d-katalog.mjs — katalog dugumu: kendi adimi ve akistaki iki komsusu
export const dugum = { ad: "katalog", adim: "katalog.ayirt", onceki: null, sonraki: "ucret" };
```

```js
// d-ucret.mjs — ucret dugumu: kendi adimi ve akistaki iki komsusu
export const dugum = { ad: "ucret", adim: "ucret.tahsil", onceki: "katalog", sonraki: "uyelik" };
```

```js
// d-uyelik.mjs — uyelik dugumu: kendi adimi ve akistaki iki komsusu
export const dugum = { ad: "uyelik", adim: "uyelik.oduncEkle", onceki: "ucret", sonraki: "bildirim" };
```

```js
// d-bildirim.mjs — bildirim dugumu: kendi adimi ve akistaki iki komsusu
export const dugum = { ad: "bildirim", adim: "bildirim.gonder", onceki: "uyelik", sonraki: null };
```

```js
// koreografi.mjs — merkezi yurutucu yok; veri yolu sirayi bilmez, her dugum komsusunu bilir
import { adimlar } from "./adimlar.mjs";
import { dugum as a } from "./d-katalog.mjs";
import { dugum as b } from "./d-ucret.mjs";
import { dugum as c } from "./d-uyelik.mjs";
import { dugum as d } from "./d-bildirim.mjs";
const yol = Object.fromEntries([a, b, c, d].map((x) => [x.ad, x]));
export function baslat(ilkDugum, istek) {
  const yapilan = [];
  for (let x = yol[ilkDugum]; x; x = x.sonraki ? yol[x.sonraki] : null) {
    adimlar[x.adim].yap(istek); yapilan.push(x.adim);   // her dugum bir sonrakini tetikler
  }
  return yapilan;
}
```

`koreografi.mjs` dört düğümü de içeri alır, ama sırayı bilmez: hangi düğümün nereye
bağlandığı düğümlerin kendi dosyalarındadır. Modül bir kayıt defteridir, akış tanımı değil.

## Kod Tarafındaki Fark

Ölçüm kaynağın kendisinden okunur: her dosyada, kendisi dışındaki kaç servisin adı geçiyor?

```js
// olc-kod.mjs — akis bilgisinin kaynak metinde kac dosyaya dagildigini sayar
import { readFileSync } from "node:fs";
import { sifirla, gonderim } from "./adimlar.mjs";
import { yurut, sira } from "./yurutucu.mjs";
import { baslat } from "./koreografi.mjs";
const servisler = ["katalog", "ucret", "uyelik", "bildirim"];
const kenar = ["ucret", "uyelik"];                 // yeni adim bu ikisinin arasina girecek

function olc(etiket, dosyalar) {
  let adlandirilan = 0, tasiyan = 0, dokunulan = 0;
  for (const f of dosyalar) {
    const metin = readFileSync(f, "utf8");
    const kendi = f.replace(/^d-/, "").replace(/\.mjs$/, "");
    const n = servisler.filter((s) => s !== kendi && metin.includes(s)).length;
    adlandirilan += n; if (n > 0) tasiyan += 1;
    if (kenar.every((s) => metin.includes(s))) dokunulan += 1;
  }
  console.log(`${etiket}: akis adi tasiyan dosya=${tasiyan}/${dosyalar.length}` +
    ` adlandirilan adim=${adlandirilan} | akisi okumak icin acilacak dosya=${tasiyan}` +
    ` | araya adim eklenince dokunulan mevcut dosya=${dokunulan}`);
}

sifirla(); const y = yurut({ kitap: 1, uye: 4 });
console.log(`yurutucu  sonuc: adim=${y.length} gonderim=${gonderim().length} [${y.join(",")}]`);
sifirla(); const k = baslat("katalog", { kitap: 1, uye: 4 });
console.log(`koreografi sonuc: adim=${k.length} gonderim=${gonderim().length} [${k.join(",")}]`);
console.log(`ayni sonuc mu: ${JSON.stringify(y) === JSON.stringify(k)}`);
olc("yurutucu ", ["yurutucu.mjs"]);
olc("koreografi", ["d-katalog.mjs", "d-ucret.mjs", "d-uyelik.mjs", "d-bildirim.mjs"]);
console.log(`akis uzunlugu (iki gerceklestirmede de): ${sira.length} adim`);
```

```sh
sh kur.sh
node olc-kod.mjs
```

```
yurutucu  sonuc: adim=4 gonderim=1 [katalog.ayirt,ucret.tahsil,uyelik.oduncEkle,bildirim.gonder]
koreografi sonuc: adim=4 gonderim=1 [katalog.ayirt,ucret.tahsil,uyelik.oduncEkle,bildirim.gonder]
ayni sonuc mu: true
yurutucu : akis adi tasiyan dosya=1/1 adlandirilan adim=4 | akisi okumak icin acilacak dosya=1 | araya adim eklenince dokunulan mevcut dosya=1
koreografi: akis adi tasiyan dosya=4/4 adlandirilan adim=6 | akisi okumak icin acilacak dosya=4 | araya adim eklenince dokunulan mevcut dosya=2
akis uzunlugu (iki gerceklestirmede de): 4 adim
```

Koşum tarafı özdeş: dört adım, bir gönderim, aynı adım listesi. Fark tümüyle kod tarafındadır.
Yürütücüde akış bilgisi 1 dosyada ve 4 adlandırma olarak durur. Koreografide 4 dosyaya
dağılır ve 6 adlandırmaya çıkar; fazlalık, telafinin geriye yürüyebilmesi için gereken
`onceki` kenarlarından gelir. Yürütücünün `yapilan` listesi tek bir yerde biriktiği için o
kenarlara ihtiyaç duymaz.

Araya yeni bir adım eklenmesi yürütücüde 1, koreografide 2 mevcut dosyaya dokunur: yeni adımın
öncesindeki düğümün `sonraki` alanı ile sonrasındaki düğümün `onceki` alanı birlikte
değişmek zorundadır. Buna karşılık koreografide hiçbir modül bütün akışı bilmez; bir servisin
değişmesi yürütücüyü değiştirmez ve akışa yeni bir dinleyici eklemek hiçbir mevcut dosyaya
dokunmadan yapılabilir.

**Neyi ucuzlattı:** saga, iki aşamalı işlemenin kilit penceresini sıfırladı; her adım kendi
işlemini hemen kesinleştirdiği için hiçbir kaynak başka bir servisin yanıtı beklenirken
tutulmaz. Yürütücü kipi ayrıca akışın tamamını tek dosyada okunur kıldı.

**Neyi pahalılaştırdı:** her adım için ikinci bir gerçekleştirme — telafi — yazılmak zorunda.
Adım sırası artık serbest değil; telafi edilebilirliğe göre kısıtlı. Koreografi kipinde akış
bilgisi 1 dosyadan 4 dosyaya, 4 adlandırmadan 6 adlandırmaya dağıldı.

**Hangi yeni arıza kipi doğdu:** *yarım kalmış akış*. Telafi edilemeyen adım tamamlandıktan
sonra çıkan hata, sistemi ne başlangıç ne bitiş durumunda bırakır; A sırasında bu, iş hiç
olmadığı hâlde üyeye giden 2 teslim olarak ölçüldü. Yürütücü kipinde ek olarak yürütücünün
kendisi tek arıza noktasıdır: `yapilan` listesi onunla birlikte kaybolursa telafi sırası
kimsede kalmaz.

## Özet

- Saga, iki aşamalı işlemenin kilit penceresini kaldırır; karşılığında her adımın telafisini
  yazmayı ve adımları telafi edilebilirliğe göre sıralamayı zorunlu kılar.
- Aynı dört adım iki sıraya dizildiğinde, dört hata noktası boyunca geri alınamayan yan etki
  A sırasında 2, B sırasında 0 çıktı; A'da iş hiç olmadığı hâlde 2 teslim yapıldı.
- Bedel karşılıklıdır: B sırasında son adımın başarısızlığı 3 telafi çağrısı gerektirdi,
  A'da 2. Telafi edilemeyen adım sona konur, çünkü geri sarılan iş pahalıdır ama düzeltilir;
  geri alınamayan etki düzeltilmez.
- Yürütücü ve koreografi aynı sonucu üretti — 4 adım, 1 gönderim, aynı adım listesi — fark
  yalnız kodda: akış bilgisi 1 dosya ve 4 adlandırmaya karşı 4 dosya ve 6 adlandırma.
- Araya adım eklemek yürütücüde 1, koreografide 2 mevcut dosyaya dokunur; buna karşılık
  koreografide hiçbir modül akışın tamamını bilmez ve yeni dinleyici eklemek mevcut dosyalara
  dokunmaz.

## Sonraki Adım

Bu derste her adım "yap" çağrısını doğrudan yaptı; adımın kesinleşmesiyle bir sonrakine haber
verilmesi arasında hiçbir boşluk yokmuş gibi davranıldı. Gerçekte bu iki iş ayrıdır: servis
kendi deposuna yazar, sonra iletiyi yayımlar — ve ikisinin arasında düşebilir. Sonraki ders
bu boşluğu ölçer: yayıncı çöktüğünde kaç ileti kaybolur, giden kutusuyla aynı yük altında kaç
ileti ikinci kez teslim edilir, aktarıcının gecikmesi ne kadardır ve kutuda kaç satır birikir.
