Truncgil'deki stajımda, çok kiracılı (multi-tenant) bir stok ve transfer takip sistemi geliştirdim: NestJS tabanlı bir API, React web paneli ve React Native mobil uygulama içeren bir monorepo. Yirmi günün sonunda ortaya çalışan bir sistem çıktı, ama bu yol düz değildi.

Bu yazıda, en çok uğraştıran beş teknik zorluğu anlatacağım. Her birinde asıl mesele kod yazmak değildi — sorunu doğru teşhis etmekti. Geliştirme sürecinde yapay zeka araçlarından yoğun şekilde faydalandım, ancak fark ettiğim şu oldu: bir aracın hızlı çözüm üretmesi, o çözümün doğru olduğu anlamına gelmiyor. Aşağıdaki örneklerin çoğunda kritik nokta, sorunu fark etmem, kök nedenini bulmam ve önerilen çözümü doğrulayıp gerektiğinde reddetmem oldu.

1. "Başarılı" Görünen ama Sessizce Bozuk Olan Build (DevOps)

Sorun. Projeyi Docker'a taşırken, web uygulamasının imajını oluşturuyordum. Build komutu hatasız tamamlanıyordu — terminalde her şey yolunda görünüyordu. Ama uygulama API'ye hiç bağlanamıyordu.

Nedenini nasıl buldum. İlk içgüdü "bir yerde bağlantı ayarı yanlış" demekti, ama ayarlar doğruydu. Terminale güvenmek yerine, oluşturulan JavaScript paketinin içine baktım. Orada, olması gereken API adresinin yerinde tuhaf bir Windows dosya yolu duruyordu: C:/Program Files/Git/api. Sebep, kullandığım Git Bash terminalinin /api değerini otomatik olarak bir dosya yoluna çevirmesiydi. Yani hata build sırasında değil, değeri terminale yazdığım anda, sessizce oluşuyordu.

Çözüm. Terminalin bu otomatik yol dönüşümünü kapatan bir ortam değişkeni ile sorunu giderdim ve daha kalıcı olması için değerin varsayılanını da güvenceye aldım.

Çıkarım. Bir işlemin "başarılı" bitmesi, doğru sonucu ürettiği anlamına gelmiyor. Çıktının sadece durumuna (başarılı/başarısız) değil, gerçek içeriğine bakmak gerekiyor. Bu tuzağı çıktının içine bakmasam asla bulamazdım.

2. Yerelde Çalışan, Docker'da Çöken Veritabanı Motoru (DevOps + Backend)

Sorun. Uygulama benim makinemde kusursuz çalışıyordu. Ama Docker konteynerinde API açılır açılmaz veritabanı katmanı çöküyordu.

Nedenini nasıl buldum. Klasik "bende çalışıyor" durumuydu ve bunun neredeyse her zaman bir ortam farkından kaynaklandığını biliyordum. Kullandığımız veritabanı aracı (Prisma), performans için işletim sistemine özel bir ikili motor (binary engine) üretiyor. Benim makinem Windows'tu; Docker imajı ise hafif bir Linux dağıtımı olan Alpine kullanıyordu. Bu Linux türü, standart olandan farklı bir sistem kütüphanesi (musl) kullanıyor ve benim makinem için üretilmiş motor orada çalışmıyordu.

Çözüm. Veritabanı yapılandırmasına, hem kendi makinem hem de Alpine Linux için ayrı motorlar üretmesini söyleyen bir ayar ekledim. Böylece her ortam kendi motorunu kullandı. Bu sırada, kalan bağımlılık sorunlarını çözmek için imaja gerekli sistem kütüphanesini de eklemem gerekti.

Çıkarım. "Kendi makinemde çalışıyor" bir doğrulama değil. Kodun gerçekten çalışacağı ortamı — bu durumda konteynerin içindeki Linux'u — baştan hesaba katmak gerekiyor. Docker'ın en büyük faydası da bu farkları erkenden ortaya çıkarması.

3. Aynı Ürünü İki Kez Eklemenin Sunucuyu Çökertmesi (Backend)

Sorun. Bir transfer oluştururken, kullanıcı aynı ürünü yanlışlıkla iki kez eklerse, sunucu "500 - Sunucu Hatası" döndürüyordu.

Nedenini nasıl buldum. Bunu test ederken fark ettim ve beni rahatsız etti. Çünkü 500 hatası, "bizim beklemediğimiz bir şey oldu" demektir — oysa aynı ürünü iki kez eklemek tamamen öngörülebilir bir kullanıcı hatası. Veritabanında aynı transferde bir ürünün yalnızca bir kez olabileceğine dair bir kısıt vardı; bu kısıt ihlal edilince hata en alt katmandan yakalanmadan yukarı fırlıyor ve 500 olarak dönüyordu.

Çözüm. Sunucuya, transferi işlemeye başlamadan önce gelen ürün listesinde tekrar olup olmadığını kontrol eden bir doğrulama ekledim. Tekrar varsa, veritabanına hiç gitmeden anlamlı bir "400 - Geçersiz İstek" hatası ve "Aynı ürün birden fazla kez eklenemez" mesajı dönüyor. Ayrıca aynı kontrolü arayüz tarafında da yaptım, yani iki katmanlı koruma oluşturdum.

Çıkarım. HTTP durum kodları kullanıcıya bir mesaj taşır: 500 "biz hata yaptık", 400 "istek hatalı" demektir. Öngörülebilir kullanıcı hatalarını 400'e çevirmek hem daha dürüst bir davranış, hem de gerçek sunucu hatalarını gürültüden ayırmayı kolaylaştırıyor. Bu, sonradan sistem genelinde standart bir hata formatı oluşturmama da zemin hazırladı.

4. Parametre Adının Yalan Söylemesi: Transfer Filtresi Tuzağı (Backend + Mobil)

Sorun. Mobil uygulamada saha personeli, kendi şubesine "gelen" transferleri görüp teslim alacaktı. Ama listeye şubeden çıkan sevkiyatlar da karışıyordu — yani kullanıcı, kendi gönderdiği malı teslim almaya çalışabilirdi.

Nedenini nasıl buldum. "Gelen kutusu" mantığını kurarken, sunucuya şube kimliğini gönderip filtrelemesini bekliyordum. Ama sonuçlar beklediğimden fazla geliyordu. Parametrenin adına güvenmek yerine sunucudaki filtre kodunu okudum ve sorunu buldum: filtre, şubenin kaynak VEYA hedef olduğu tüm transferleri döndürüyordu. Yani "bu şubeyle ilgili her şey" mantığı, "bu şubeye gelenler" için yetersizdi.

Çözüm. Sunucu filtresini yine kullandım — sonucu daraltmak için işe yarıyordu — ama gelen listeyi arayüz tarafında bir kez daha, yalnızca hedef şubesi doğru olanları bırakacak şekilde süzdüm. Sonra bunu kanıtlamak için özellikle ters yönde bir transfer oluşturdum: sunucu iki kayıt döndürdü, ekranda tek kart göründü. Böylece filtrenin doğru çalıştığını gözlemleyerek doğruladım.

Çıkarım. Bir parametrenin adı, davranışını garanti etmiyor. branchId göndermek "bu şubeye ait" demek gibi görünse de, gerçekte "bu şubeyle bağlantılı" anlamına geliyordu. Bir davranışı varsaymak yerine, altındaki kodu okumak ve sonucu bir test senaryosuyla kanıtlamak gerekiyor.

5. Ortamdan Ortama Değişen Hata: Zaman Aşımının İşe Yaramaması (Mobil)

Sorun. Mobil uygulamada, sunucuya ulaşılamadığında hata ekranı yaklaşık 50 saniye sonra çıkıyordu. Kullanıcı için bu, uygulamanın donduğu anlamına gelir.

Nedenini nasıl buldum. Çözüm basit görünüyordu: bir zaman aşımı süresi ekle. Ekledim ve zaman aşımı hatası için özel bir mesaj yazdım. Ama bu mesaj ekranda hiç görünmedi. Tahmin yürütmek yerine, cihazdan gerçek hata kodunu logladım ve sürpriz bir durumla karşılaştım: React Native'in ağ katmanı, erişilemeyen bir sunucuyu, benim koyduğum zaman aşımı sayacı dolmadan, tamamen farklı bir hata koduyla sonlandırıyordu. Aynı isteği masaüstünde denediğimde ise beklediğim kod geliyordu. Yani sorun bir ortam farkıydı — mobil platform, hatayı farklı isimlendiriyordu.

Çözüm. Yazdığım "zaman aşımı" dalı, o platformda pratikte hiç tetiklenmeyen ölü bir koddu. Bunu fark edip, olası üç farklı hata kodunu tek bir "bağlantı hatası" durumunda birleştirdim. Ayrıca yeniden deneme sayısını da düşürdüm; yoksa varsayılan tekrar denemeler, eklediğim zaman aşımını çarpıp bekleme süresini yine uzatacaktı. Sonuçta bekleme süresi 50 saniyeden yaklaşık 12-25 saniyeye indi.

Çıkarım. Bir çözümün çalıştığını varsaymak yerine, gerçekten çalıştığını gözlemlemek gerekiyor. Eğer cihazdan gerçek hatayı loglamasaydım, mantıklı görünen ama o platformda hiç çalışmayan bir kod bırakmış olacaktım. "Doğru görünen" ile "doğru çalışan" aynı şey değil.

Genel Bir Çıkarım

Bu beş örneğin ortak bir noktası var: hiçbirinde asıl zorluk çözümü yazmak değildi. Zorluk, sorunu doğru teşhis etmekti. Bir build'in neden sessizce bozulduğunu, bir hatanın neden yalnızca belirli bir ortamda çıktığını, bir parametrenin adının neden yanıltıcı olduğunu anlamak — işin asıl kısmı buydu.

Yapay zeka araçları bu süreçte üretim hızımı ciddi şekilde artırdı. Ama en kritik anlar, aracın ürettiği çözümü olduğu gibi kabul ettiğim değil, sorguladığım anlardı: "Bu build başarılı görünüyor ama içinde ne var?", "Bu filtre gerçekten hedef şubeyi mi süzüyor?", "Bu zaman aşımı dalı gerçekten çalışıyor mu?" Bu soruları sormak, üretilen kodun sorumluluğunu üstlenmek demekti — ve stajın bana kazandırdığı en değerli teknik alışkanlık da bu oldu: hiçbir çıktıya, kendim doğrulamadan güvenmemek.