---
title: 'Geriye Dönük Uyumluluk'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/geriye-donuk-uyumluluk'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:44+00:00'
license: 'CC BY-SA 4.0'
---

# Geriye Dönük Uyumluluk

Aynı değişikliğin istek ve yanıt yönünde ters sınıflandırılması, iki şema sürümünü karşılaştırıp kırıcı değişiklikleri saptayan bir fark alıcı ve sürüm numarasının tartışmadan farktan türetilmesi.

Bir önceki ders iki sürümü yan yana yayımladı ama sürüm numarasının neden ikiye çıktığını
sormadı. `uye` alanının nesneye dönmesi kırıcı sayıldı, `sube` alanına yeni bir seçenek
eklemek konuşulmadı. Bu sorular sezgiye bırakıldığında her ekip başka yanıt verir; sürüm
numarası kanıya dayanır ve "bu küçük bir değişiklik" cümlesiyle atlanır.

Bu ders kırıcılığı tanımlar, bir fark alıcı yazar ve sürüm numarasını farktan türetir.
Tanımın çıkış noktası basit bir gözlemdir: **aynı değişiklik, istek yönünde uyumluyken
yanıt yönünde kırıcı olabilir.**

## Yönün Belirlediği Kırıcılık

Bir sözleşmenin iki yönü vardır. İstemci gövde **gönderir**, sunucu gövde **üretir**.
İstemcinin gönderdiği için sunucu bir kabul kümesi tanımlar; sunucunun ürettiği için de bir
üretim kümesi.

Sunucunun **kabul ettiği daralırsa** eski istemciler dışarıda kalır. Kabul edilenler
genişlerse eski istemciler etkilenmez; gönderdikleri hâlâ kümenin içindedir. Yani istek
yönünde daralma kırıcı, genişleme uyumludur.

Sunucunun **ürettiği genişlerse** eski istemciler tanımadıkları bir şeyle karşılaşır.
Ürettiği daralırsa istemciler etkilenmez; okudukları alanlar hâlâ yerindedir. Yani yanıt
yönünde genişleme kırıcı, daralma uyumludur.

İki yönün kuralı tam olarak birbirinin tersidir. Bu ters ilişki, `/isbn` alanının kaldırılması
gibi tek bir değişikliğin istekte uyumlu, yanıtta kırıcı olmasının nedenidir: istekten
kaldırılan alanı eski istemci göndermeye devam eder, sunucu yok sayar; yanıttan kaldırılan
alanı ise eski istemci aramaya devam eder ve bulamaz.

Aşağıdaki iki şema, ödünç kaynağının bir sürüm aralığındaki hâlini verir. Her alan üç şey
bildirir: tipi, zorunlu olup olmadığı ve varsa alabileceği değerler.

```js
// semalar.mjs — odunc kaynaginin iki sema surumu
// Alan: { tip, zorunlu, degerler }.  degerler yoksa deger kumesi sinirsizdir.
export const V1 = {
  surum: "1.4.2",
  istek: {
    "/uye":       { tip: "dizgi", zorunlu: true },
    "/isbn":      { tip: "dizgi", zorunlu: true },
    "/sube":      { tip: "dizgi", zorunlu: false, degerler: ["merkez", "sahil"] },
    "/gunSayisi": { tip: "sayi",  zorunlu: false },
  },
  yanit: {
    "/id":         { tip: "dizgi", zorunlu: true },
    "/uye":        { tip: "dizgi", zorunlu: true },
    "/isbn":       { tip: "dizgi", zorunlu: true },
    "/iadeTarihi": { tip: "dizgi", zorunlu: true },
    "/durum":      { tip: "dizgi", zorunlu: true, degerler: ["acik", "kapali"] },
  },
};

export const V2 = {
  istek: {
    "/uye":       { tip: "dizgi",   zorunlu: true },
    "/kalemler":  { tip: "dizi",    zorunlu: true },                                    // yeni zorunlu alan
    "/sube":      { tip: "dizgi",   zorunlu: true, degerler: ["merkez", "sahil", "tepe"] },
    "/gunSayisi": { tip: "tamsayi", zorunlu: false },                                   // tip daraldi
    "/not":       { tip: "dizgi",   zorunlu: false },                                   // yeni istege bagli alan
  },
  yanit: {
    "/id":         { tip: "dizgi", zorunlu: true },
    "/uye":        { tip: "nesne", zorunlu: true },                                     // tip degisti
    "/kalemler":   { tip: "dizi",  zorunlu: true },                                     // yeni alan
    "/iadeTarihi": { tip: "dizgi", zorunlu: false },                                    // artik bulunmayabilir
    "/durum":      { tip: "dizgi", zorunlu: true, degerler: ["acik", "kapali", "gecikmis"] },
  },
};
```

## Fark Alıcı

Fark alıcı iki şemayı gezer, her farkı bulur ve yön kuralını uygular. Tip değişiklikleri
için küçük bir alt tip ilişkisi gerekir: her tam sayı bir sayıdır, tersi doğru değildir.
Aralarında böyle bir ilişki olmayan tipler her iki yönde de kırıcıdır.

```js
// fark.mjs — iki sema surumunu karsilastirir, kirici degisiklikleri saptar, surumu turetir
// Kullanim: node fark.mjs [hosgoru]   ("hosgoru": istemciler bilinmeyen degeri hos gorur)

import { V1, V2 } from "./semalar.mjs";

const HOSGORU = process.argv[2] === "hosgoru";

// Tip kafesi: tamsayi, sayi'nin alt tipidir. Alt tip, ust tipin kabul ettiginin bir bolumudur.
const ALT_TIPLER = { tamsayi: ["sayi"], tarih: ["dizgi"] };
const altTipMi = (a, b) => a === b || (ALT_TIPLER[a] ?? []).includes(b);

// Iki yon, iki kural. Istek: sunucunun kabul ettigi daralirsa kirici.
// Yanit: sunucunun urettigi genislerse kirici.
function karsilastir(eski, yeni, yon) {
  const kirici = yon === "istek"
    ? { daralma: true, genisleme: false, zorunluEkleme: true, zorunluKalkma: false, kaldirma: false }
    : { daralma: false, genisleme: true, zorunluEkleme: false, zorunluKalkma: true, kaldirma: true };
  const bulgular = [];
  const ekle = (sinif, ad, aciklama) => bulgular.push({ sinif, yon, ad, aciklama });

  for (const [ad, e] of Object.entries(eski)) {
    const y = yeni[ad];
    if (!y) { ekle(kirici.kaldirma ? "kirici" : "uyumlu", ad, "alan kaldırıldı"); continue; }
    if (!e.zorunlu && y.zorunlu) ekle(kirici.zorunluEkleme ? "kirici" : "uyumlu", ad, "isteğe bağlıydı, zorunlu oldu");
    if (e.zorunlu && !y.zorunlu) ekle(kirici.zorunluKalkma ? "kirici" : "uyumlu", ad, "zorunluydu, isteğe bağlı oldu");
    if (e.tip !== y.tip) {
      const daralma = altTipMi(y.tip, e.tip), genisleme = altTipMi(e.tip, y.tip);
      const sinif = daralma ? (kirici.daralma ? "kirici" : "uyumlu")
        : genisleme ? (kirici.genisleme ? "kirici" : "uyumlu")
        : "kirici";                                       // ilgisiz tipler her yonde kirici
      ekle(sinif, ad, `tip ${e.tip} → ${y.tip}` + (daralma ? " (daralma)" : genisleme ? " (genişleme)" : " (ilgisiz)"));
    }
    const eD = e.degerler, yD = y.degerler;
    if (eD && yD) {
      const kalkan = eD.filter((d) => !yD.includes(d)), gelen = yD.filter((d) => !eD.includes(d));
      if (kalkan.length) ekle(kirici.daralma ? "kirici" : "uyumlu", ad, `değer kümesi daraldı: ${kalkan.join(",")} kalktı`);
      if (gelen.length) {
        const kiriciMi = kirici.genisleme && !(yon === "yanit" && HOSGORU);
        ekle(kiriciMi ? "kirici" : "uyumlu", ad, `değer kümesi genişledi: ${gelen.join(",")} eklendi`);
      }
    }
  }
  for (const [ad, y] of Object.entries(yeni)) {
    if (eski[ad]) continue;
    const kiriciMi = y.zorunlu && kirici.zorunluEkleme;
    ekle(kiriciMi ? "kirici" : "ekleme", ad, y.zorunlu ? "zorunlu alan eklendi" : "isteğe bağlı alan eklendi");
  }
  return bulgular;
}

const bulgular = [...karsilastir(V1.istek, V2.istek, "istek"), ...karsilastir(V1.yanit, V2.yanit, "yanit")];
const SIRA = { kirici: 0, ekleme: 1, uyumlu: 2 };
bulgular.sort((a, b) => SIRA[a.sinif] - SIRA[b.sinif] || a.yon.localeCompare(b.yon));

console.log(`hoşgörü bildirimi: ${HOSGORU ? "var" : "yok"}\n`);
console.log("sınıf    yön     alan          değişiklik");
for (const b of bulgular) console.log(`${b.sinif.padEnd(8)} ${b.yon.padEnd(7)} ${b.ad.padEnd(13)} ${b.aciklama}`);

const sayim = { kirici: 0, ekleme: 0, uyumlu: 0 };
for (const b of bulgular) sayim[b.sinif]++;
const [ana, ikincil, yama] = V1.surum.split(".").map(Number);
const sonraki = sayim.kirici ? `${ana + 1}.0.0` : sayim.ekleme ? `${ana}.${ikincil + 1}.0` : `${ana}.${ikincil}.${yama + 1}`;

console.log(`\nkırıcı: ${sayim.kirici}   ekleme: ${sayim.ekleme}   uyumlu: ${sayim.uyumlu}`);
console.log(`mevcut sürüm: ${V1.surum}   türetilen sürüm: ${sonraki}`);
console.log(`kırıcılar ayıklansaydı: ${ana}.${ikincil + 1}.0`);
```

```
hoşgörü bildirimi: yok

sınıf    yön     alan          değişiklik
kirici   istek   /sube         isteğe bağlıydı, zorunlu oldu
kirici   istek   /gunSayisi    tip sayi → tamsayi (daralma)
kirici   istek   /kalemler     zorunlu alan eklendi
kirici   yanit   /uye          tip dizgi → nesne (ilgisiz)
kirici   yanit   /isbn         alan kaldırıldı
kirici   yanit   /iadeTarihi   zorunluydu, isteğe bağlı oldu
kirici   yanit   /durum        değer kümesi genişledi: gecikmis eklendi
ekleme   istek   /not          isteğe bağlı alan eklendi
ekleme   yanit   /kalemler     zorunlu alan eklendi
uyumlu   istek   /isbn         alan kaldırıldı
uyumlu   istek   /sube         değer kümesi genişledi: tepe eklendi

kırıcı: 7   ekleme: 2   uyumlu: 2
mevcut sürüm: 1.4.2   türetilen sürüm: 2.0.0
kırıcılar ayıklansaydı: 1.5.0
```

Son iki satır ters ilişkiyi doğrudan gösteriyor. `/isbn` alanının kaldırılması istekte
uyumlu, yanıtta kırıcı. `/sube` alanının değer kümesine `tepe` eklenmesi istekte uyumlu;
`/durum` alanının değer kümesine `gecikmis` eklenmesi yanıtta kırıcı. Aynı işlem, karşı
yönde karşıt sınıfta.

`ekleme` satırlarındaki `/kalemler` alanı da bu kuralın sonucudur: yanıta zorunlu bir alan
eklemek uyumludur, çünkü eski istemci o alanı zaten okumuyordu. İsteğe zorunlu alan
eklemek ise kırıcıdır ve tabloda öyle görünüyor.

Sürüm numarası artık tartışılan değil, **türetilen** bir değerdir. Yedi kırıcı değişiklik
ana sürümü yükseltir. Son satır ayrı bir seçenek gösterir: kırıcılar yayından ayıklanıp
yalnız eklemeler çıkarılsaydı sürüm 1.5.0 olurdu. Bu iki satır, "sürümü ne yapalım"
tartışmasını "yayına neyi koyacağız" tartışmasına çevirir; ikincisi yanıtlanabilir bir
sorudur.

## Hoşgörü Bildirimi

Yanıttaki değer kümesinin genişlemesi, listedeki en tartışmalı kalemdir. `gecikmis`
durumunun eklenmesi eski istemcileri bozar mı? Yanıt, sözleşmede ne yazdığına bağlıdır.
Sözleşme "bilinmeyen bir durum değeri gelirse istemci onu tanınmayan olarak ele almalıdır"
diyorsa, genişleme kırıcı değildir; demiyorsa kırıcıdır.

```bash
node fark.mjs hosgoru | tail -4
```

```
kırıcı: 6   ekleme: 2   uyumlu: 3
mevcut sürüm: 1.4.2   türetilen sürüm: 2.0.0
kırıcılar ayıklansaydı: 1.5.0
```

Kırıcı sayısı yediden altıya iniyor. Buradaki ders, hoşgörünün bir **sözleşme maddesi**
olmasıdır. Sunucu tarafında alınan bir karar değildir; istemcilerin gerçekten öyle
yazılmış olmasını gerektirir ve bu ancak sözleşmede önceden duyurulmuşsa beklenebilir.
Sözleşmeye sonradan eklenen bir hoşgörü maddesi, kendisi kırıcı bir değişikliktir.

## Farkın Söylemediği

Fark alıcı **neyin bozulabileceğini** söyler, **neyin bozulacağını** değil. Ayrımı görmek
için aynı şemalar gerçek bir eski istemciyle sınanır.

```js
// sinama.mjs — siniflandirmayi eski istemciyi calistirarak sinar
// Eski istemci v1 govdesi yollar ve v1 yanitini okur; sunucu v2 semasindadir.
import { V1, V2 } from "./semalar.mjs";

const ESKI_ISTEK = { "/uye": "U-1001", "/isbn": "978-0262033848", "/sube": "sahil", "/gunSayisi": 14.5 };
const YENI_YANIT = { "/id": "O-1", "/uye": { kimlik: "U-1001" }, "/kalemler": [{}], "/durum": "gecikmis" };

const tipi = (d) =>
  Array.isArray(d) ? "dizi" : typeof d === "object" ? "nesne"
    : typeof d === "number" ? (Number.isInteger(d) ? "tamsayi" : "sayi") : "dizgi";

// 1) Eski istemcinin govdesi yeni semaya sokulur.
const istekHatalari = [];
for (const [ad, k] of Object.entries(V2.istek)) {
  const d = ESKI_ISTEK[ad];
  if (d === undefined) { if (k.zorunlu) istekHatalari.push(`${ad}: zorunlu alan gönderilmemiş`); continue; }
  if (k.tip !== tipi(d) && !(k.tip === "sayi" && tipi(d) === "tamsayi")) istekHatalari.push(`${ad}: ${tipi(d)} gönderildi, ${k.tip} bekleniyor`);
  if (k.degerler && !k.degerler.includes(d)) istekHatalari.push(`${ad}: ${d} kabul edilmiyor`);
}

// 2) Yeni yanit, eski istemcinin bekledigi semayla okunur.
const yanitHatalari = [];
for (const [ad, k] of Object.entries(V1.yanit)) {
  const d = YENI_YANIT[ad];
  if (d === undefined) { if (k.zorunlu) yanitHatalari.push(`${ad}: beklenen alan yanıtta yok`); continue; }
  if (k.tip !== tipi(d)) yanitHatalari.push(`${ad}: ${tipi(d)} geldi, ${k.tip} bekleniyordu`);
  if (k.degerler && !k.degerler.includes(d)) yanitHatalari.push(`${ad}: ${d} tanınmayan değer`);
}

console.log("— eski istemcinin gövdesi yeni sunucuda —");
for (const h of istekHatalari) console.log(`  ${h}`);
console.log(`  reddedilen alan sayısı: ${istekHatalari.length}`);

console.log("\n— yeni sunucunun yanıtı eski istemcide —");
for (const h of yanitHatalari) console.log(`  ${h}`);
console.log(`  okunamayan alan sayısı: ${yanitHatalari.length}`);
```

```
— eski istemcinin gövdesi yeni sunucuda —
  /kalemler: zorunlu alan gönderilmemiş
  /gunSayisi: sayi gönderildi, tamsayi bekleniyor
  reddedilen alan sayısı: 2

— yeni sunucunun yanıtı eski istemcide —
  /uye: nesne geldi, dizgi bekleniyordu
  /isbn: beklenen alan yanıtta yok
  /iadeTarihi: beklenen alan yanıtta yok
  /durum: gecikmis tanınmayan değer
  okunamayan alan sayısı: 4
```

Fark alıcı istek yönünde üç kırıcı değişiklik bulmuştu; bu istemcide yalnız ikisi
gerçekleşti. Farkı yaratan `/sube` alanıdır: isteğe bağlıyken zorunlu olmuştur, ama bu
istemci onu zaten gönderiyordu. Değişiklik sözleşme gereği kırıcıdır — sürüm numarasını
yükseltir — ama bu istemci için maliyeti sıfırdır.

Sözleşme ile maliyet ayrı ölçülerdir. Sürüm numarası sözleşmeye bakar ve tek bir istemci
etkilenmese bile yükselir; yayın planı ise maliyete bakar. Tasarım Sistemleri kursundaki
göç maliyeti hesabı da bu ayrımı kurmuştu: kırıcılık sözleşmenin, etkilenen çağrı yeri
sayısı gerçekliğin ölçüsüdür.

Yanıt yönündeki dört kalem ise tam olarak gerçekleşmiştir. Bu, yönler arasındaki başka bir
asimetriyi gösterir: istek yönündeki kırıcılık istemcinin **ne gönderdiğine** bağlıdır,
yanıt yönündeki kırıcılık istemcinin **ne okuduğuna**. İstemciler genellikle şemadaki her
alanı gönderme zorunda değildir ama okudukları alanlar sabittir; bu yüzden yanıt yönündeki
kırıcı değişiklikler daha yüksek oranda gerçek maliyete dönüşür.

## Özet

- Sözleşmenin iki yönü ters kurallara tabidir: sunucunun kabul ettiği daralırsa istek
  yönünde kırıcı, ürettiği genişlerse yanıt yönünde kırıcıdır.
- Aynı değişiklik iki yönde karşıt sınıfa düşer; alanın istekten kaldırılması uyumlu,
  yanıttan kaldırılması kırıcıdır.
- Yanıta zorunlu alan eklemek uyumludur, isteğe zorunlu alan eklemek kırıcıdır; tip
  daralması istekte, tip genişlemesi yanıtta kırıcıdır.
- Sürüm numarası fark sonucundan türetilir; kırıcılar yayından ayıklandığında türetilen
  numara da değişir, böylece tartışma sürümden yayın kapsamına taşınır.
- Bilinmeyen değere hoşgörü bir sözleşme maddesidir ve önceden duyurulmuş olmalıdır;
  sonradan eklenmesi kendisi kırıcı bir değişikliktir.
- Fark alıcı neyin bozulabileceğini söyler; hangi istemcinin gerçekten bozulacağı ayrı bir
  ölçümdür ve yanıt yönündeki kırıcılıklar daha yüksek oranda gerçek maliyete dönüşür.

## Sonraki Adım

Fark alıcı yedi kırıcı değişiklik saptadı ve sürümü 2.0.0'a çıkardı, ama bir soruyu
yanıtsız bıraktı: eski sürüm ne zamana kadar yaşayacak? Sürüm numarasını yükseltmek eski
istemcileri kendiliğinden taşımaz; taşınmaları için önce durumdan haberdar olmaları, sonra
da geçiş için süre bulmaları gerekir. Sonraki ders bu süreyi bir politikaya çevirir:
kullanımdan kaldırmayı yanıtın kendisinde duyuran başlıkları ekler, kullanım telemetrisini
modelleyip geçiş süresinin sonunda hâlâ eski sürümde kalan tüketici oranını hesaplar ve
kapatma tarihinin bu orana göre nasıl seçileceğini gösterir.
