Full-stack geçişini yalnızca yeni bir backend dili öğrenmek olarak görmüyorum. Benim için asıl değişim, bir özelliğin ekranda başlayan yolculuğunu domain kararlarına, ölçüme ve iş sonucuna kadar takip edebilmekti.
Getmobil'de frontend ağırlıklı çalışırken önce NestJS ve MySQL kullanan backend tarafında sorumluluk almaya başladım. Ardından C# ve .NET 10 ile, DDD prensiplerini gözeten well-sliced monolith mimarisine geçiş sürecinin parçası oldum.
Altı ayda backend tarafında 100'e yakın commit oluştu. Bu sayı tek başına bir başarı ölçüsü değil; geçişin kısa süreli bir deneme yerine düzenli bir sorumluluğa dönüştüğünü gösteren bir işaret. Asıl ownership hikâyesi ise checkout ve ödeme akışlarını etkileyen bir A/B testiyle ortaya çıktı.
Geçişin kısa özeti
| Başlık | Kapsam |
|---|---|
| Süre | Yaklaşık 6 ay |
| Backend katkısı | 100'e yakın commit |
| Mevcut teknoloji | NestJS ve MySQL |
| Mimari yön | C# / .NET 10, DDD ve well-sliced monolith |
| En geniş feature | Peşin ödeme checkout A/B testi |
| Etkilenen domain'ler | Order ve Financials |
| Deney dağılımı | %90 control, %10 treatment |
| Cache yaklaşımı | SSR'da başlayan deney, ortak client hook'u ile devam eden fiyat journey'si |
| Nihai karar | Control ile devam |
Not: Deney dağılımı dışında dönüşüm adetleri, oranları ve finansal etkiler şirket içi bilgi olduğu için paylaşılmamıştır.
Buradaki mimari hedef, sistemi gereksiz yere parçalara ayırmak değildi. Domain sınırları belirgin, sorumlulukları ayrışmış ve tek bir ürün gibi yönetilebilen bir yapı kurmaktı. Daha önce Turborepo geçişinde repo ownership'ını anlatırken vardığım sonuçla benzerdi: sınırları çizmek yetmiyor, günlük değişiklik akışında çalıştıklarını da görmek gerekiyor.
Full-stack geçişi nasıl başladı?
Frontend tarafında bir checkout değişikliği çoğu zaman bileşen, state ve API isteği gibi görünür. Fakat kullanıcı “öde” butonuna yaklaştıkça konu hızla genişler:
- Sepetin güncel durumu,
- Siparişin oluşturulma kuralları,
- Ödeme yönteminin uygunluğu,
- Tutar ve kampanya hesapları,
- Başarılı veya başarısız ödeme sonrası durum geçişleri,
- Ölçüm event'lerinin doğru sırada üretilmesi.
Bu noktada yalnız arayüzü geliştirmek yeterli değildi. Order ve Financials domain'lerinin hangi karardan sorumlu olduğunu, client'ın hangi bilgiyi yalnızca gösterdiğini ve backend'in hangi kurallarda tek doğruluk kaynağı olduğunu anlamam gerekiyordu.
NestJS tarafındaki geliştirmeler bu geçişin ilk güvenli alanını oluşturdu. Mevcut sözleşmeleri, MySQL üzerindeki veri akışını ve üretimde çalışan davranışı okuyarak başladım. .NET 10 ve DDD geçişinde ise aynı davranışı yeni mimari sınırlar içinde nasıl daha açık ifade edebileceğimize odaklandım.
Full-stack olmak burada her katmanda aynı derinlikte uzman olmak değildi. Bir değişikliğin hangi katmanda neden yapılması gerektiğini anlayabilmek ve katmanlar arasındaki boşluğu sahipsiz bırakmamak anlamına geliyordu.
Büyük feature: peşin ödeme deneyini kurmak
Deneyin amacı, peşin ödeme kullanan müşteriler için checkout deneyiminde yaptığımız değişikliğin satın alma davranışına etkisini görmekti.
Statsig üzerinden kullanıcıların:
- %90'ı mevcut deneyimde, yani control grubunda,
- %10'u yeni deneyimde, yani treatment grubunda
kaldı.
Dağılımı kontrollü tutmamızın nedeni yalnız teknik risk değildi. Checkout ve ödeme akışında küçük bir davranış değişikliği bile sipariş tamamlama oranını, finansal kayıtları ve operasyonel süreçleri etkileyebilir. Önce sınırlı bir grupta ölçmek, hem kullanıcı etkisini hem de sistem davranışını gözlemlememizi sağladı.
Statsig deneyinin başlangıç bağlamı SSR tarafında oluşturuldu. Ancak kullanıcının gördüğü fiyat journey'sini yalnız server çıktısına bırakmadık. Deney arayüzü ve akışı değiştirebilirdi; Order ve Financials domain'lerindeki temel kurallar yine backend tarafından korunmalıydı.
Deneyi neden client tarafında sürdürdük?
Bu kararın temel nedeni cache mimarisiydi.
Order API kullanıcıya özel çalışıyor ve burada paylaşımlı bir cache katmanı bulunmuyordu. Buna karşılık ana sayfa, ürün listeleme sayfaları (PLP) ve ürün detay sayfaları (PDP), Next.js ve Cloudflare cache katmanlarından geçiyordu.
Bu nedenle yalnız API'deki fiyatı değiştirmek yeterli değildi. Aynı kullanıcı journey boyunca:
- Ana sayfada cache'lenmiş bir fiyat,
- PLP'de başka bir render anı,
- PDP'de farklı bir cache çıktısı,
- Sepet ve checkout'ta kullanıcıya özel güncel fiyat
görebilirdi.
Sorun fiyat hesabından çok, farklı cache sınırları arasında aynı deney bağlamını korumaktı.
Çözüm olarak A/B testini SSR tarafında başlattık; varyanta bağlı fiyat gösterimlerini ise tek bir hook üzerinden client component'lara taşıdık. Böylece server ilk deney bağlamını sağladı, client hook'u da ana sayfa, PLP ve PDP boyunca kullanıcıya ait journey'yi cache'e takılmadan sürdürdü.
| Katman | Cache durumu | Sorumluluk |
|---|---|---|
| Next.js SSR | Sayfaya göre cache'lenebilir | Deneyin başlangıç bağlamını oluşturmak |
| Cloudflare | Ortak içeriği edge'de cache'leyebilir | Genel sayfa teslimini hızlandırmak |
| Client hook'u | Kullanıcı oturumunda çalışır | Varyanta bağlı fiyat gösterimini journey boyunca korumak |
| Order API | Kullanıcıya özel, paylaşımlı cache yok | Nihai fiyat ve sipariş kurallarını doğrulamak |
Buradaki önemli ayrım şuydu: client fiyatın doğruluk kaynağı olmadı. Client yalnızca deney varyantına uygun sunumu ve akışı yönetti. Sipariş oluşturulurken geçerli fiyat, ödeme koşulları ve finansal doğruluk backend tarafından korunmaya devam etti.
Bu hibrit yaklaşım sayesinde Next.js SSR'ın performans avantajını tamamen kaybetmeden, paylaşımlı cache'e kullanıcıya özel bir fiyat kararı gömmemiş olduk. Deney bir noktada başlayıp başka bir sayfada varyant değiştirmedi; kullanıcı control veya treatment journey'sinde tutarlı biçimde ilerledi.
A/B testinde yalnız son ekrana bakmadık
Deneyi tek bir dönüşüm rakamına indirgemek yerine funnel boyunca izledik:
| Funnel adımı | Gözlenen yön |
|---|---|
view_product |
Treatment lehine iyileşme |
add_to_cart |
Treatment lehine iyileşme |
view_basket |
Treatment lehine iyileşme |
| Checkout ilerleyişi | Olumlu davranış sinyalleri |
purchase |
Anlamlı ticari iyileşme oluşmadı |
Treatment grubu ürün görüntüleme, sepete ekleme ve sepet görüntüleme seviyelerinde güçlü bir iyileşme gösterdi. İlk bakışta bu, yeni deneyimin başarılı olduğu izlenimini veriyordu.
Ancak checkout deneyinin iş hedefi daha fazla ara etkileşim üretmek değildi. Nihai karar metriği purchase ve bunun business tarafına kattığı değerdi. Bu seviyede istatistiksel ve ticari olarak anlamlı bir iyileşme oluşmadı.
Bu nedenle control varyantıyla devam etme kararı aldık.
Bu sonucu “deney başarısız oldu” diye okumuyorum. Ürün hipotezi desteklenmedi; deney sistemi ise doğru kararı verecek kadar başarılı çalıştı. Üst funnel hareketlendiği için treatment'ı kalıcılaştırmak kolay olurdu. Fakat ownership, olumlu görünen ara metrikleri savunmak değil, başta belirlenen karar metriğine sadık kalmaktı.
A/B testinin değeri treatment'ı kazandırmak değil, belirsizliği güvenilir bir karara dönüştürmektir.
Ownership yalnız geliştirmeyi kapsamıyordu
Bu feature'daki sorumluluk kodla başlamadı ve merge ile bitmedi.
| Aşama | Ownership kapsamı |
|---|---|
| Hipotez | Değişikliğin hangi kullanıcı davranışını etkilemesini beklediğimizi netleştirmek |
| Tasarım | Control ve treatment deneyimlerini, uygun kullanıcı grubunu ve dağılımı belirlemek |
| Geliştirme | SSR başlangıcı, ortak client hook'u, checkout, Order ve Financials sınırlarındaki değişiklikleri birlikte yürütmek |
| Ölçüm | Statsig exposure'larını ve funnel event'lerini aynı deney diliyle izlemek |
| Raporlama | Ara metriklerle purchase sonucunu birbirinden ayırmak |
| Karar | Ticari karşılığı olmayan treatment yerine control ile devam etmek |
| Temizlik | Deney kodunu ve geçici dallanmaları sistemden kaldırmak |
En öğretici aşama sonuncusuydu. Bir deney tamamlandığında yalnız feature flag'i kapatmak yeterli değil. Karar control yönündeyse treatment için eklenen geliştirmelerin de sistemde kalıcı bir alternatif yol gibi yaşamaması gerekiyor.
Karardan sonra kodu neden temizledik?
Deney dalları zamanla görünmeyen teknik borca dönüşebilir. Aktif olmayan bir treatment:
- Checkout akışını okumayı zorlaştırır,
- Order ve Financials tarafında artık gerçekleşmeyen senaryolar bırakır,
- Event şemasında anlamını kaybetmiş alanları yaşatır,
- Sonraki geliştiricinin hangi yolun geçerli olduğunu anlamasını güçleştirir,
- Yeni deneylerle çakışabilecek koşullar üretir.
Bu nedenle karar sonrasında:
- Treatment'a ait client akışı kaldırıldı.
- Geçici koşullar ve artık kullanılmayan sözleşmeler temizlendi.
- Order ve Financials domain'lerinde control tek geçerli yol haline getirildi.
- Deney raporu ve karar gerekçesi korunurken çalışma zamanı bağımlılıkları kaldırıldı.
- Funnel event'lerinin deney sonrası anlamı yeniden kontrol edildi.
Silme aşaması, feature'ın eksik kalan işi değil; deney yaşam döngüsünün planlı sonuydu. Kod tabanında yalnız alınan kararın kalması, sonraki değişikliklerin maliyetini düşürdü.
AI ile çalışırken nelere dikkat ettim?
Full-stack geçişinde AI, bilmediğim bir katmanda hızla bağlam kurmama yardımcı oldu. Özellikle mevcut akışı özetlemek, benzer uygulamaları bulmak ve küçük refactor seçeneklerini karşılaştırmak için değerliydi.
Fakat ödeme ve sipariş gibi kritik domain'lerde hız kadar sınır da önemliydi.
Domain kuralını AI'a bırakmadım
AI mevcut koddan bir desen çıkarabilir, ancak bir ödemenin hangi durumda geçerli sayılacağına karar veremez. Order ve Financials kurallarını önce mevcut davranış, ürün kararı ve domain sözleşmeleri üzerinden netleştirdim; AI'ı bu sınırların içinde kullandım.
Event isimlerini ve deney kapsamını uydurmasına izin vermedim
Analytics tarafında birbirine benzeyen ama farklı anlam taşıyan event'ler kolayca üretilebilir. Statsig exposure'ı, funnel event'i ve karar metriği aynı hipoteze bağlanmadığında rapor güvenilirliğini kaybeder. Bu yüzden event sözleşmelerini mevcut gerçek akıştan doğruladım.
Büyük değişiklikleri küçük parçalara böldüm
Client deneyimi, backend sözleşmesi, domain davranışı ve raporlama aynı anda değişse bile tek bir belirsiz görev gibi ele alınmadı. Her adımın girdisini, çıktısını ve geri alma sınırını ayrı tuttum.
Cache katmanlarını da aynı şekilde tek tek izledim. Yalnız API koduna bakmak, ana sayfa, PLP ve PDP'deki Next.js ile Cloudflare davranışını kaçırabilirdi. AI'dan gelen önerileri gerçek request journey'si ve cache sınırlarıyla karşılaştırmadan uygulamadım.
Üretim verisini bağlam olarak taşımadım
Müşteri, ödeme ve şirket içi performans verilerini AI bağlamına koymadan; şema, anonim örnek ve davranış üzerinden ilerledim. Bu, özellikle finansal akışlarda hızdan önce gelen bir sınırdı.
AI geçişi kolaylaştırdı, fakat ownership'ın yerini almadı. Hangi önerinin mevcut sisteme uyduğunu, hangi değişikliğin domain sınırını ihlal ettiğini ve hangi kodun karar sonrasında silinmesi gerektiğini hâlâ insanın değerlendirmesi gerekiyordu.
Bu süreçte full-stack benim için neye dönüştü?
Altı aylık geçiş sonunda full-stack tanımım değişti.
Önceden bunu frontend ve backend geliştirebilmek olarak okuyordum. Bugün ise daha çok şu sorularla ilişkilendiriyorum:
- Kullanıcı deneyimindeki değişiklik hangi domain kararına dayanıyor?
- Backend sözleşmesi bu deneyimi güvenli biçimde destekliyor mu?
- Ölçtüğümüz event gerçekten karar vermek istediğimiz davranışı temsil ediyor mu?
- Teknik sonuç business sonucuyla aynı şeyi mi söylüyor?
- Karar verildikten sonra sistemde ne kalmalı, ne silinmeli?
Bu soruların tamamını takip edebilmek, tek tek kullanılan teknolojilerden daha kalıcı bir beceri oldu. TypeScript tarafındaki tip güvenliği veya DDD tarafındaki domain sınırları aynı amaca hizmet ediyor: değişikliğin etkisini daha erken görmek ve kararları daha güvenli uygulamak.
Sonuç
Bu deneyde treatment üst funnel'da daha iyi sinyaller üretti, fakat purchase seviyesinde anlamlı bir business değeri oluşturmadı. Control ile devam etmek, yaptığımız geliştirmeyi savunmaktan daha doğru bir karardı.
Full-stack ownership da tam burada anlam kazandı: özelliği geliştirmek, yayına almak ve olumlu metrikleri göstermek kadar; hipotez desteklenmediğinde bunu açıkça raporlamak, doğru kararı almak ve artık gerekmeyen kodu temizlemek.
Yaklaşık 100 backend commiti bana yeni teknolojiler öğretti. Ancak asıl geçiş, bir feature'ın yalnız kodunu değil; sınırlarını, ölçümünü, kararını ve sonrasını da sahiplenmeye başladığımda gerçekleşti.
Bir sonraki adımda .NET 10, DDD ve well-sliced monolith geçişinde domain sınırlarını nasıl kurduğumuzu daha teknik bir yazıyla ele alacağım.
