Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Debugging ve Problem Çözme — #11

Root Cause Analysis: 5 Whys, Fishbone ve Blameless Postmortem

Mert Özen 8 Kas 2026 14 dk 16 görüntülenme
Root Cause Analysis: 5 Whys, Fishbone ve Blameless Postmortem

Root cause bir kişi değil, hatanın production'a ulaşmasına izin veren koşullardır. Sipariş toplamı vakasında belirti, proximate cause ve root cause'u ayırıyor; 5 Whys ve fishbone ile etkenleri buluyor, etkiyi ölçüyor, Money value object ve BannedApiAnalyzers ile bu sınıf bug'ı imkânsız kılıyor ve blameless postmortem'i yazıyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; analyzer örneği Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0, testler xUnit v3 (xunit.v3 4.0.2) ile çalıştırılmıştır.

Önce Tahmin Et

Sipariş toplamı hatası 10 gün production'da kaldı: hatalı commit'i içeren v1.9.0, 11 Eylül Cuma deploy edildi; muhasebe ticket'ı 21 Eylül Pazartesi açtı ve düzeltme aynı akşam çıktı. Bookstore o günlerde günde yaklaşık 350 sipariş alıyor, satırların bir kısmı indirimli ve fiyatlar çoğunlukla ,00, ,45, ,50, ,90 ve ,95 ile bitiyor.

Sence siparişlerin yüzde kaçı yanlış tutarla tahsil edildi: %1 mi, %5 mi, %20 mi? Bir de şu: "Bu bug'ın kök sebebi ne?" sorusuna cevabın "refactor yapan geliştirici bir argümanı unuttu" ise, bu yazının sonunda o cevabı neden yetersiz bulduğumuzu göreceksin.

Belirti, Proximate Cause ve Root Cause

Serinin ilk on yazısı bir bug'ı bulmakla ilgiliydi. Bu yazı, bulduktan sonra sorulması gereken soruyla ilgili: bu bug neden mümkündü ve bir daha nasıl imkânsız hale gelir? Önce üç kavramı ayıralım, çünkü RCA (Root Cause Analysis) tartışmalarının çoğu bu üçünü karıştırdığı için verimsiz geçer.

Belirti (symptom), dışarıdan görünen şeydir: mutabakat raporunda bazı siparişler faturadan 1 kuruş az tahsil edilmiş. Proximate cause (yakın sebep), belirtiyi doğrudan üreten teknik olaydır: PriceCalculator'daki Math.Round(x, 2) çağrısı, ortadaki değerleri MidpointRounding.ToEven ile yuvarlıyor; bu serinin 03 numaralı yazısında git bisect run ile bunun "Extract PriceCalculator" refactoring'iyle geldiğini bulduk. Root cause (kök sebep) ise proximate cause'un ortaya çıkmasına ve fark edilmeden production'a ulaşmasına izin veren koşullardır. Onu tek cümleyle söylemek nadiren mümkündür.

Ayrım neden önemli? Çünkü her seviyenin düzeltmesi farklı. Belirtiyi düzeltmek, muhasebeye farkları açıklamaktır. Proximate cause'u düzeltmek, eksik argümanı geri koymaktır; serinin 02 numaralı yazısında bunu tek satırla yaptık. Root cause'u düzeltmek ise aynı sınıftaki bir sonraki bug'ı da önlemektir.

5 Whys: Gücü ve Sınırı

5 Whys, Toyota Production System'den gelen ve Taiichi Ohno'nun yaygınlaştırdığı basit bir tekniktir: belirtiden başlayıp "neden?" sorusunu, cevap artık bir sürece ya da tasarım kararına dayanana kadar tekrar sorarsın. Beş sayısı bir kural değil, "ilk cevapta durma" hatırlatmasıdır. Bizim vakada zincir şöyle:

1. Neden müşteriden 1 kuruş az tahsil edildi? Çünkü checkout 11,205 TL'lik satır fiyatını 11,20'ye yuvarladı, fatura servisi 11,21'e.

2. Neden checkout 11,20'ye yuvarladı? Çünkü PriceCalculator, Math.Round(decimal, int) overload'unu kullanıyor ve bu overload ortadaki değerleri en yakın çifte yuvarlıyor.

3. Neden bu overload kullanılıyor? Çünkü "Extract PriceCalculator" refactoring'inde, kodu OrderService'ten taşırken MidpointRounding.AwayFromZero argümanı düştü. İki overload da derlendiği için derleyici bir şey söylemedi.

4. Neden bu değişiklik merge'den önce yakalanmadı? Çünkü testlerin hiçbirinde ortadaki bir değer yoktu ve PR (pull request), "saf refactoring, davranış değişmiyor" açıklamasıyla hızlıca onaylandı.

5. Neden sistemin doğruluğu tek bir argümanın doğru yazılmasına bağlıydı? Çünkü Bookstore'da para düz bir decimal; yuvarlama kuralı hiçbir tipte ya da kuralda yaşamıyor, checkout'ta ve fatura servisinde ayrı ayrı, elle yazılıyor.

Beşinci cevap bir kişiye değil bir tasarım kararına işaret ediyor; doğru yerde durduk. Ama zincirin sınırlarını da gör. Dördüncü adımda zincir aslında dallandı: testler, review ve derleyici üç ayrı sebepti ve biz birini seçmek zorunda kaldık. 5 Whys tek bir doğrusal zincir üretir, gerçek olaylar ise çoğu zaman birbirini besleyen birkaç etkenden oluşur. İkinci sınır, cevabın soranın bakış açısına bağlı olması: aynı ekipte başka biri üçüncü adımdan sonra "çünkü geliştirici dikkatsizdi" diyerek zinciri bir kişide bitirebilirdi. Kişiyle biten bir zincir, "daha dikkatli ol" gibi işe yaramayan bir aksiyonla biter.

Fishbone ile Birden Fazla Etkeni Görmek

Fishbone diyagramı, Kaoru Ishikawa'nın kalite kontrol için geliştirdiği ve onun adıyla Ishikawa diyagramı da denen bir araç. Belirti balığın başına yazılır; olası sebepler kategorilere ayrılıp kılçıklara dizilir. Klasik üretim kategorileri (insan, makine, yöntem, malzeme, ölçüm, çevre) yazılıma uymadığı için ben kod, tasarım, test, review, izleme ve bilgi kategorilerini kullanıyorum:

11-fishbone-tr.png

Diyagram, 5 Whys'ın tek zincire sığdıramadığı şeyi gösteriyor: bu bug'ın tek bir kökü yok. Kod seviyesinde tehlikeli bir varsayılan, tasarım seviyesinde sahipsiz bir kural, testlerde eksik veri, review'da "refactoring zararsızdır" varsayımı, izlemede 10 günlük bir kör nokta ve ekipte Math.Round'un varsayılanının bilinmemesi. Bunlardan herhangi biri farklı olsaydı ya bug hiç oluşmayacak ya da saatler içinde yakalanacaktı. İyi bir RCA, aksiyonları bu kılçıkların her birine dağıtır.

Pratik bir not: fishbone'u tek başına doldurma. Olay toplantısında ekiple birlikte, önce kategoriler boşken herkesin aklındaki etkenleri yazdığı bir oturumla doldurmak, tek kişinin göremeyeceği kılçıkları ortaya çıkarır. Muhasebeden birinin "mutabakat raporuna haftada bir bakıyoruz" demesi, izleme kılçığının asıl kaynağı oldu.

Etkiyi Ölçmek

Postmortem'in ilk sorusu "ne kadar kötüydü?" Bu yazıda gerçek sipariş verisi kullanmıyorum; etkiyi, 10 günlük trafiği tohumlanmış (seeded) bir sipariş üreticisiyle tekrar oynatarak tahmin ediyorum. Gerçek bir olayda aynı hesabı, sipariş tablosundaki satırları iki kuralla ayrı ayrı yeniden hesaplayan bir sorguyla yaparsın:

// etkilenen 10 günü tohumlanmış bir sipariş üreticisiyle tekrar oynatır: kaç sipariş ayrışıyor?
Random random = new(4127);
decimal[] discounts = [0.00m, 0.00m, 0.00m, 0.10m, 0.15m];
decimal[] endings = [0.00m, 0.45m, 0.50m, 0.90m, 0.95m];
int orders = 10 * 350, affected = 0;
decimal undercharged = 0;

for (int i = 0; i < orders; i++)
{
    decimal buggy = 0, correct = 0;
    int lineCount = random.Next(1, 5);
    for (int l = 0; l < lineCount; l++)
    {
        decimal price = random.Next(20, 400) + endings[random.Next(endings.Length)];
        decimal raw = price * (1 - discounts[random.Next(discounts.Length)]);
        int quantity = random.Next(1, 4);
        buggy += Math.Round(raw, 2, MidpointRounding.ToEven) * quantity;
        correct += Math.Round(raw, 2, MidpointRounding.AwayFromZero) * quantity;
    }
    if (buggy != correct)
    {
        affected++;
        undercharged += correct - buggy;
    }
}

Console.WriteLine($"orders = {orders}, affected = {affected} ({affected * 100.0 / orders:0.0}%), undercharged = {undercharged:0.00} TL");
$ dotnet run
orders = 3500, affected = 634 (18.1%), undercharged = 14.39 TL

Baştaki sorunun cevabı: 3.500 siparişin 634'ü, yani %18,1'i. İlk yazıda fiyatların %5'inde fark çıkmıştı; sipariş düzeyinde oran çok daha yüksek, çünkü bir siparişte birden fazla satır var, fiyat sonları rastgele değil ve tek bir satırın etkilenmesi siparişi etkilemeye yetiyor. Toplam fark ise yalnız 14,39 TL. Bir de asimetri var: pozitif bir tutarda AwayFromZero ortadaki değeri her zaman yukarı yuvarlar, ToEven ise bazen yukarı bazen aşağı. Yani checkout'un tahsil ettiği tutar faturadakinden hiçbir zaman fazla olamaz, yalnız az olabilir. Fark hep müşterinin lehine olduğu için hiçbir müşteri şikâyet etmedi; sinyal ancak muhasebenin haftalık mutabakatından geldi. 14 TL'lik bir zarar küçük, ama 634 siparişin her biri için düzeltme faturası ya da açıklama gerekmesi, muhasebe ekibi için günlerce iş demek.

Düzeltme Seviyeleri: Hotfix'ten İmkânsız Kılmaya

Düzeltmeyi üç seviyede düşünmek, RCA'nın "ne yapacağız?" kısmını somutlaştırır.

Birinci seviye hotfix: eksik argümanı geri koymak. 02 numaralı yazıda kırmızı testle birlikte yaptık ve v1.10.1 olarak aynı akşam deploy edildi. Hotfix'in görevi kanamayı durdurmaktır; tek başına bir sonraki benzer bug'ı önlemez.

İkinci seviye regression test: bu bug'ı ve aynı sınıftaki varyasyonlarını yakalayan testler. Ticket'taki siparişi birebir test etmek iyi bir başlangıç, ama tek bir örnek yalnız o örneği korur. Bu yüzden bir de bütün fiyat aralığını, birkaç indirim oranında, bağımsız bir referans hesapla karşılaştıran bir test ekliyoruz:

using Bookstore.Ordering;
using Xunit;

namespace Bookstore.Ordering.Tests;

public class OrderTotalRegressionTests
{
    [Fact]
    public void Order_10482_matches_the_invoice()
    {
        List<OrderLine> lines =
        [
            new("978-605-0000-01-1", Money.Of(12.45m), 1, 0.10m),
            new("978-605-0000-02-8", Money.Of(89.90m), 1, 0.00m),
            new("978-605-0000-03-5", Money.Of(45.50m), 2, 0.15m),
        ];

        Assert.Equal(Money.Of(178.47m), PriceCalculator.OrderTotal(lines));
    }

    [Theory]
    [InlineData(0.05)]
    [InlineData(0.10)]
    [InlineData(0.15)]
    [InlineData(0.25)]
    public void Every_price_rounds_midpoint_away_from_zero(decimal rate)
    {
        for (decimal price = 0.01m; price <= 999.99m; price += 0.01m)
        {
            decimal raw = price * (1 - rate);
            decimal expected = decimal.Floor(raw * 100 + 0.5m) / 100;   // referans: pozitif tutarlar için okulda öğrenilen yuvarlama

            Assert.Equal(expected, Money.Of(price).ApplyDiscount(rate).Amount);
        }
    }
}

Bir testin bug'ı gerçekten yakaladığını görmenin yolu, bug'ı bilerek geri getirmektir. Aşağıdaki Money tipinde yuvarlamayı geçici olarak ToEven'a çevirip testleri koştum:

$ dotnet test | grep -E "^failed|Expected|Actual|total:|failed:"
failed Bookstore.Ordering.Tests.OrderTotalRegressionTests.Every_price_rounds_midpoint_away_from_zero(rate: 0.15) (9ms)
  Expected: 0.09
  Actual:   0.08
failed Bookstore.Ordering.Tests.OrderTotalRegressionTests.Every_price_rounds_midpoint_away_from_zero(rate: 0.05) (0ms)
  Expected: 0.29
  Actual:   0.28
failed Bookstore.Ordering.Tests.OrderTotalRegressionTests.Every_price_rounds_midpoint_away_from_zero(rate: 0.1) (0ms)
  Expected: 0.05
  Actual:   0.04
failed Bookstore.Ordering.Tests.OrderTotalRegressionTests.Every_price_rounds_midpoint_away_from_zero(rate: 0.25) (0ms)
  Expected: 0.05
  Actual:   0.04
failed Bookstore.Ordering.Tests.OrderTotalRegressionTests.Order_10482_matches_the_invoice (2ms)
  Expected: 178.47
  Actual:   178.46
  total: 5
  failed: 5

Beş testin beşi de kırmızı ve her biri farklı bir örnekle. Değişikliği geri alınca hepsi yeşile döndü. Bu küçük deney, testin kendisinin de test edilmesidir.

Üçüncü seviye, bu sınıf bug'ı imkânsız kılan değişiklik. Root cause "yuvarlama kuralının sahibi yok" idi; o zaman kurala bir sahip veriyoruz. Para artık düz bir decimal değil, tek bir yuvarlama politikası olan bir value object:

namespace Bookstore.Ordering;

public readonly record struct Money
{
    public decimal Amount { get; }

    private Money(decimal amount) => Amount = amount;

    // Bookstore'un tek yuvarlama politikası: 2 hane, ortadaki değer sıfırdan uzağa
    public static Money Of(decimal amount) =>
        new(Math.Round(amount, 2, MidpointRounding.AwayFromZero));

    public Money ApplyDiscount(decimal rate) => Of(Amount * (1 - rate));

    public static Money operator +(Money left, Money right) => new(left.Amount + right.Amount);
    public static Money operator *(Money money, int quantity) => new(money.Amount * quantity);

    public override string ToString() => Amount.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
}

Money'nin constructor'ı private; dışarıdan yuvarlanmamış bir tutarla Money üretmenin tek yolu Of, ve Of her zaman aynı kuralla yuvarlar. PriceCalculator artık yuvarlamayı hiç bilmiyor:

namespace Bookstore.Ordering;

public sealed record OrderLine(string Isbn, Money UnitPrice, int Quantity, decimal DiscountRate);

public static class PriceCalculator
{
    public static Money DiscountedUnitPrice(OrderLine line) => line.UnitPrice.ApplyDiscount(line.DiscountRate);

    public static Money LineTotal(OrderLine line) => DiscountedUnitPrice(line) * line.Quantity;

    public static Money OrderTotal(IEnumerable<OrderLine> lines) =>
        lines.Aggregate(Money.Of(0), (total, line) => total + LineTotal(line));
}

Tip, doğru yolu kolaylaştırır ama yanlış yolu kapatmaz: biri yarın başka bir hesapta yine Math.Round(x, 2) yazabilir. Bunu derleme zamanında kapatmak için Microsoft'un BannedApiAnalyzers paketini kullanıyoruz. Paket, BannedSymbols.txt dosyasında listelenen API'leri (Application Programming Interface) kullanan her satırda RS0030 diagnostic'ini üretir; biz bunu warning yerine error yapıyoruz:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <WarningsAsErrors>RS0030</WarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" PrivateAssets="all" />
    <AdditionalFiles Include="BannedSymbols.txt" />
  </ItemGroup>
</Project>
M:System.Math.Round(System.Decimal);Use Money: money is rounded only inside Money
M:System.Math.Round(System.Decimal,System.Int32);Use Money: money is rounded only inside Money

Yasaklanan iki overload, yuvarlama kuralını sessizce varsayılana bırakanlar. MidpointRounding alan üç parametreli overload serbest; kuralı açıkça yazan biri bilinçli bir seçim yapıyor demektir. Şimdi biri kargo ücreti için yeni bir hesap yazıp eski alışkanlıkla Math.Round çağırırsa:

$ dotnet build
ShippingFeeCalculator.cs(5,52): error RS0030: The symbol 'Math.Round(decimal, int)' is banned in this project: Use Money: money is rounded only inside Money (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md) [Bookstore.Ordering.csproj]

Build kırılıyor ve mesaj ne yapılması gerektiğini söylüyor. "Extract PriceCalculator" commit'i bu kural varken yazılsaydı, PR açılmadan önce geliştiricinin kendi makinesinde derlenmeyecekti. Testler ve tip ile birlikte son durum:

$ dotnet test
./tests/Bookstore.Ordering.Tests/bin/Debug/net10.0/Bookstore.Ordering.Tests.dll (net10.0|arm64) passed (718ms)
Test run summary: Passed!
  total: 5
  failed: 0
  succeeded: 5
  skipped: 0

Son adım tek bir servisle sınırlı kalmamak. Fatura servisi de aynı kuralı ayrı bir kodla uyguluyor; kural iki yerde yaşadıkça iki yerde ayrışabilir. Money'yi iki servisin de kullandığı ortak bir paket haline getirmek, kuralın tek kaynağı olmasını sağlar.

Blameless Postmortem: Sipariş Toplamı Vakası

Postmortem, bir olayın yazılı kaydıdır: ne oldu, etkisi neydi, nasıl giderildi, kök sebepleri neydi ve tekrar etmemesi için ne yapılacak. Google'ın SRE (Site Reliability Engineering) kitabının "Postmortem Culture: Learning from Failure" bölümü, blameless postmortem'i şöyle tarif ediyor: katkıda bulunan sebepleri, herhangi bir kişiyi ya da ekibi suçlamadan ortaya koyan, herkesin o anki bilgisiyle iyi niyetle davrandığını varsayan bir kayıt. Amaç ceza değil, sistemi düzeltmek. Aynı bölüm, postmortem'in kıdemli mühendislerce gözden geçirilmesini ve sonuçların paylaşılmasını da şart koşuyor; okunmayan bir postmortem, yazılmamış sayılır.

Blameless olmak, kimin ne yaptığını gizlemek değildir. Zaman çizelgesinde "refactoring commit'i merge edildi" yazar; "X unuttu" yazmaz. Fark ince ama belirleyici: insanlar suçlanacaklarını düşündüklerinde bilgi saklar, ve bilgi saklanan bir postmortem'den kök sebep çıkmaz. Aşağıda bu vakanın postmortem'i, kullandığım şablonla.

Özet

11-21 Eylül 2026 arasında checkout, indirimli ve ham fiyatı tam iki kuruşun ortasına düşen satırları faturadan farklı yuvarladı. Etkilenen siparişlerde tahsil edilen tutar faturadan 1 ya da birkaç kuruş az oldu. Sorun 21 Eylül'de muhasebenin mutabakat ticket'ıyla fark edildi ve aynı gün v1.10.1 ile giderildi.

Etki

Tahmini 634 sipariş (10 günlük trafiğin %18,1'i), toplam 14,39 TL eksik tahsilat. Müşteri tarafında görünür bir hata yok; muhasebe tarafında 634 siparişin fatura ve tahsilat kayıtlarının uzlaştırılması gerekiyor.

Zaman çizelgesi

Zaman (İstanbul saati)Olay
5 Eylül 14.00"Extract PriceCalculator from OrderService" commit'i; PR "saf refactoring" olarak onaylandı
11 Eylül 16.00v1.9.0 production'a deploy edildi; hatalı yuvarlama başladı
15 Eylül 11.00v1.10.0 deploy edildi (ilgisiz değişiklikler)
21 Eylül 09.30Muhasebe, haftalık mutabakatta ticket #4127'yi açtı
21 Eylül 11.00Fark reproduce edildi, midpoint hipotezi 9.000 fiyatla doğrulandı
21 Eylül 14.20Kırmızı xUnit testi yazıldı, düzeltme yeşile çevirdi
21 Eylül 15.10git bisect run hatalı commit'i (6b6e917) 7 adımda buldu
21 Eylül 17.30v1.10.1 hotfix deploy edildi; yeni siparişlerde fark yok

Kök sebep ve katkıda bulunan etkenler

Proximate cause, refactoring sırasında MidpointRounding.AwayFromZero argümanının düşmesi. Kök sebep, para tutarlarının yuvarlama kuralının tek bir yerde tanımlı olmaması: kural iki serviste elle yazılıyordu ve derleyici, testler ya da tipler onu korumuyordu. Katkıda bulunan etkenler: test verisinde ortadaki değer olmaması, refactoring PR'larının davranış değişikliği için ayrıca incelenmemesi, mutabakat farkının günlük değil haftalık izlenmesi, Math.Round'un varsayılanının ekipte yaygın olarak bilinmemesi.

Neler iyi gitti, neler kötü gitti

İyi giden: ticket'ta sipariş numarası ve iki tutar vardı, bu sayede reproduce bir saat sürmedi. Debugging günlüğü tutuldu, elenen hipotezler kayıtlıydı. Bisect, deterministik bir kontrol programı sayesinde dakikalar içinde sonuç verdi. Kötü giden: hata 10 gün fark edilmedi. İlk tepki olarak ödeme sağlayıcısından şüphelenildi ve buna yarım saat harcandı.

Aksiyonlar

  • Para tutarları için Money value object'i ve tek yuvarlama politikası; sahibi Ordering ekibi, 2 Ekim
  • Money'yi fatura servisiyle ortak bir pakete taşımak; sahibi Ordering ve Fatura ekipleri, 16 Ekim
  • BannedApiAnalyzers ile varsayılan yuvarlama overload'larını yasaklamak; sahibi Platform ekibi, 28 Eylül
  • Bütün fiyat aralığını tarayan regression testi; sahibi Ordering ekibi, 25 Eylül (tamamlandı)
  • Tahsilat ile fatura toplamını günlük karşılaştıran ve fark olduğunda alarm üreten bir job; sahibi Platform ekibi, 9 Ekim
  • Etkilenen 634 siparişin listesini muhasebeye iletmek; sahibi Ordering ekibi, 22 Eylül (tamamlandı)

Her aksiyonun bir sahibi ve bir tarihi var; sahipsiz bir aksiyon, yazıldığı an unutulmuş bir aksiyondur. Aksiyonların dağılımına da bak: fishbone'daki altı kılçıktan beşine en az bir aksiyon düşüyor. "Bilgi" kılçığının aksiyonu bu postmortem'in kendisi: ekip içinde paylaşılan ve okunan bir postmortem, Math.Round'un varsayılanını herkesin öğrenmesini sağlıyor.

Üretimde Dikkat

Postmortem'i ne zaman yazacağına önceden karar ver. SRE kitabı, kullanıcının gördüğü belirli bir eşiği aşan kesinti, her türlü veri kaybı, rollback gibi manuel müdahale, belirli bir sürenin üstünde süren çözüm ve izlemenin kaçırıp insanların fark ettiği olaylar gibi tetikleyiciler öneriyor. Bizim vakamız son tetikleyiciye tam uyuyor: hatayı izleme değil muhasebe buldu. Olay yönetiminin süreci (on-call, severity seviyeleri, iletişim) ilerideki SRE ve Production Incident'ları serisinde.

Postmortem'i olaydan sonraki birkaç gün içinde, detaylar unutulmadan yaz. Debugging günlüğü ve ticket geçmişi, zaman çizelgesinin hammaddesi. Postmortem içinde müşteri verisi, sipariş sahiplerinin adları ya da secret olmamalı; etkilenen siparişlerin listesi postmortem'e değil, erişimi kısıtlı ayrı bir kayda konur.

Aksiyonları takip et. Postmortem'in değeri, aksiyonlar kapandığında ortaya çıkar. Açık aksiyonları sprint planlamasında diğer işlerle aynı backlog'da görmek, "postmortem yazdık, unuttuk" döngüsünü kırar.

Sık Düşülen Tuzaklar

En yaygın tuzak, kök sebebi bir kişide bitirmek. "Geliştirici argümanı unuttu" doğru bir cümle, ama bir kök sebep değil. İnsanlar hata yapar; sistem, tek bir insan hatasının 10 gün boyunca 634 siparişi etkilemesine izin vermemeli. Baştaki ikinci sorunun cevabı bu.

İkinci tuzak, proximate cause'u düzeltip durmak. Hotfix deploy edildiğinde baskı kalkar ve RCA ertelenir. Bir sonraki benzer bug geldiğinde aynı postmortem'i yeniden yazarsın.

Üçüncü tuzak, 5 Whys'ı tek doğru cevabı olan bir sınav gibi görmek. Aynı olaya iki kişi iki farklı zincirle gelebilir; bu bir sorun değil, fishbone'a geçme işaretidir.

Dördüncü tuzak, ölçülemeyen aksiyonlar. "Daha dikkatli review yapılacak" kapanamaz, çünkü ne zaman yapıldığını kimse söyleyemez. "Refactoring PR'larında davranışı karşılaştıran test zorunlu" ya da "RS0030 error olarak etkin" kapanabilir.

Beşinci tuzak, etkiyi hesaplamamak. "1 kuruş, önemsiz" diye kapatılan bir olay, 634 siparişlik bir muhasebe işini ve bir sonraki denetimde açıklanması gereken bir farkı arkasında bırakır.

Küçük Bir Deneme

★☆☆ Yakın zamanda yaşadığın bir bug için 5 Whys zincirini yaz. Zincir hangi adımda bir kişide bitme eğilimi gösterdi? O adımda soruyu "neden sistem buna izin verdi?" diye değiştir ve devam et.

★★☆ Kendi projende BannedApiAnalyzers'ı kur ve DateTime.Now'ı (P:System.DateTime.Now) yasakla. Build kaç yerde kırılıyor? Her birini TimeProvider'a çevirmenin maliyetini tahmin et.

★★★ Money'ye negatif tutarlar (iade) için bir test ekle. AwayFromZero, -11,205'i nereye yuvarlar ve fatura servisi iadede aynı kuralı mı uygulamalı? Kararı bir postmortem aksiyonu gibi yaz: sahibi ve tarihiyle.

Yöntemi, araçları ve postmortem'i tamamladık. Serinin son iki yazısında bunları gerçek hayattan vakalara uyguluyoruz; bir sonraki yazıda backend'den beş bug avı var: Türkçe culture'da bozulan karşılaştırma, 3 saat kayan rapor, socket exhaustion, Dictionary'de değişen key ve process'i çökerten bir async void.