Referans Rehberi · Mimari

İş mantığı cont­rol­ler'a ait değildir

İş kuralları neden controller'a değil, domain'e aittir — belirli bir framework için değil, her modern backend için geçerli bir ilke. CTO'lar, mühendislik yöneticileri, mimarlar ve kıdemli geliştiriciler için.

Bu nedir? · Referans Rehberi

Bir mühendislik sorusunu ayrıntılı olarak ele alan teknik rehber. Önerilerin avantajlarını, maliyetlerini, sınırlamalarını ve hangi koşullarda farklı bir seçimin uygun olacağını açıklar. Genel bakışa git

Yazar
Batunet Engineering
Okuma süresi
13 dk
Seviye
İleri düzey
Durum
Onaylandı
Son inceleme
21 Temmuz 2026
Güncelleme
21 Temmuz 2026
Bu sayfada

Bu, backend geliştirmedeki en eski ve en pahalı hatalardan biridir ve başlangıçta bir hatanın tam tersi gibi görünür: İş mantığını isteğin geldiği yere — controller'a — yazarsınız. Çalışan bir fonksiyona giden en kısa yoldur, her eğitim içeriği böyle gösterir ve bedeli yıllar içinde büyüyene kadar kimse fark etmez.

Bu metin bir framework sorusu değildir. İster bir Laravel controller'ı, ister Symfony controller'ı, ASP.NET controller'ı, bir Spring @RestController'ı ya da bir Rails controller'ı olsun — rol her yerde aynıdır ve iş kurallarını oraya yerleştirme hatası için de aynısı geçerlidir. İlke framework'ten bağımsızdır, çünkü özü teknolojiyle değil, bir sınırla ilgilidir: Controller sistemin kenarına, protokole aittir. İş kuralları ise sistemin çekirdeğine, hiçbir framework'ü tanımayan bir domain'e aittir. Bu sınırı bulanıklaştıran kişi, sisteminin en değerli parçasını — işi tanımlayan kuralları — en kolay değiştirilebilir parçasına bağlar: framework'e.


Controller'lar neden fazla büyür

Hiç kimse bir controller'ı şişirmeye karar vermez. Controller büyür, çünkü onu hiçbir şey durdurmaz. Controller, isteğin geldiği yerdir ve dolayısıyla en az direnç gösteren yoldur: Request burada, o hâlde doğrulama burada yapılır; giriş noktası burası, o hâlde veriler burada yüklenir, kural burada kontrol edilir, burada kaydedilir ve burada yanıt verilir. Her yeni gereksinim "yalnızca birkaç satır" ekler. Controller'ı tek bir karar şişirmez — birikim şişirir.

Bunu iki güç pekiştirir. Birincisi, framework'lerin kendi çekim gücüdür: İskeletler ve örnekler mantığı controller'lara ve modellere koyar, çünkü çalışan bir demoya giden en kısa yol budur — ve çoğu kişi demolardan öğrenir. İkincisi, doğal bir karşı basınç yoktur: Çalıştığı sürece kuralı ayırmak için hiçbir neden yoktur. Böylece controller, bir değişiklik canı yakana kadar büyür — ve o noktada çoktan her şeyin birleştiği yer hâline gelmiştir.

Bu çekimin ikinci bir kolaylığı vardır: veri modeli. ORM nesnesi zaten verileri tuttuğu için kuralları da hemen onun içine yazmak cazip gelir — ve böylece aynı nesne veriyi, kalıcılığı ve iş mantığını aynı anda taşır. Şişkin controller böylece çoğu zaman şişkin bir modele dönüşür ve temel sorunda hiçbir şey değişmez: Kural hâlâ altyapıya yapışıktır, yalnızca HTTP yerine veritabanına. Her iki durumda da sonradan uzayan aynı kısa yoldur.

Bir controller'ın asıl sorumluluğu

Bir controller'ın tek bir görevi vardır: çevirmek. Dış dünya — HTTP gibi bir protokol — ile sistemin içi arasında aracılık eder. İsteği okur, biçimini kontrol eder, uygulamaya temiz bir çağrı iletir, sonucu alır ve ondan bir yanıt oluşturur: durum kodu, format, header'lar. Fazlası değil. İyi bir controller bir karar verici gibi değil, bir tercüman gibi okunur. İş kararı vermez; onu iletir.

Mihenk taşı basittir: Controller'ı okuduğunuzda bir kuralın uygulandığını gördüğünüzü, ancak hangi kuralın uygulandığını görmediğinizi düşünün. Kuralın kendisi başka bir yerde yaşar. Protokolü çıkardığınızda — HTTP yok, request nesnesi yok, durum kodu yok — ideal olarak controller'da eksikliğini hissedeceğiniz hiçbir iş mantığı kalmaz.

İsim bazen bu evrenselliği gölgeler. Burada "controller" denen şey her ekosistemde karşınıza çıkar: controller metodu, action, route handler, endpoint olarak. Arkasındaki katman da geleneğe göre farklı adlar taşır — use case, interactor, application service, command handler. Terimler birbirinin yerine kullanılabilir, roller ise kullanılamaz: Çeviren bir yer, orkestre eden bir yer ve karar veren bir yer vardır. Bu üçünü temiz şekilde ayıran kişi, adı ve framework'ü ne olursa olsun aynı şeyi inşa etmiş olur.

Öneri: Controller'ları protokol çevirisiyle sınırlayın ve her kararı içeri taşıyın. Bedeli ek bir çağrı ve ek bir katmandır — kural artık isteğin geldiği yerde doğrudan durmaz, bir seviye daha derindedir. Farklı karar verdiğimiz durum, "iş mantığı" gerçek kurallar içermeyen saf oluşturma, okuma, güncelleme ve silmeden ibaret olan çok küçük bir uygulamadır; orada controller üzerinden giden yalın yol uygun sadeliktir ve bir domain katmanı karşılığı olmayan bir seremoni olurdu.

İş kuralları ve orkestrasyon

Protokole ait olmayan her şey aynı değildir. Controller'da çoğu zaman iç içe geçen iki şeyi ayırmak faydalıdır.

İş kuralları, işi tanımlayan kararlar ve değişmezlerdir (invariant) — istek nasıl gelirse gelsin doğrudurlar. Bir sipariş ödenmeden gönderilemez. Bir indirim belirli bir sınırı aşamaz. İki rezervasyon çakışamaz. Bu kurallar domain'e aittir: veritabanı, HTTP ve framework olmadan iş alanını temsil eden nesnelere.

Orkestrasyon, bir işlemi gerçekleştiren adımların sırasıdır: girdiyi kontrol etmek, entity'yi yüklemek, kararı domain'e bırakmak, sonucu kaydetmek, bir bildirim tetiklemek, yanıt vermek. Bu da controller'a ait değildir — ancak domain de değildir. İnce bir uygulama katmanında yaşar: dile göre farklı adlandırılan bir use case'te, bir service'te, bir handler'da. Controller use case'i çağırır; use case orkestre eder; domain karar verir.

Bu dağılım bir sorumluluk haritası olarak okunabilir:

GörevAit olduğu yerAit olmadığı yer
HTTP durum kodunu ve yanıt formatını seçmekControllerDomain
İsteğin biçimini kontrol etmek (tipler, zorunlu alanlar)Controller / kenarDomain
İş kuralını ve değişmezi kontrol etmekDomainController
Bir işlemin adımlarının sırasıUse caseController
Transaction sınırını belirlemekUse caseDomain / Controller
Verileri yüklemek ve kaydetmekAltyapı (repository)Domain
E-posta, mesaj, harici çağrıAltyapıDomain
Controller merkezli Controller HTTP İş kuralları Kalıcılık E-posta Sorumluluk ayrılmış Controller · çevirmek Use case · orkestre etmek Domain · karar vermek Altyapı · DB, e-posta

Şema: Aynı görevler — bir kez tek bir controller'da karışık hâlde, bir kez her biri tek bir sorumluluk taşıyan katmanlara dağıtılmış hâlde.

Öneri: Kuralı ve orkestrasyonu ayırın — kuralları domain'e, akışı bir use case'e, çeviriyi controller'a. Bedeli daha fazla yapıdır: daha fazla dosya, daha fazla adlandırılmış yer, katmanlar arasında biraz eşleme (mapping) emeği. Bir işlem gerçekten yalnızca "yükle, asgari kontrol et, kaydet"ten ibaretse farklı karar veririz; o zaman ayrı bir orkestrasyon katmanı gereksizdir ve gerçek kurallar ortaya çıkana kadar doğrudan yol yeterlidir.

Controller merkezli sistemlerin belirtileri

Bu kalıbı tekrarlayan işaretlerden tanırsınız — ahlaki değil, teşhis amaçlı:

  • Bir controller metodu yüzlerce satır uzunluğundadır ve bütün bir işlem gibi okunur.
  • Aynı kural birden fazla controller'da görünür — kopyalanmış ve zamanla birbirinden uzaklaşmış.
  • Bir iş kuralına yalnızca HTTP üzerinden erişilebilir; bir arka plan işi ya da komut satırı komutu onu çağırmak yerine yeniden yazar.
  • Basit bir hesaplamayı test etmek için tüm framework'ü ayağa kaldırmak ve bir HTTP isteğini simüle etmek gerekir.
  • İşle ilgili bir değişiklik controller'ları düzenlemek demektir — ve başka neyin bozulacağından kimse emin değildir.
  • ORM modeli aynı anda veriyi, iş kurallarını ve verilerin saklanmasını yönetir.
  • "X kuralı nerede yazıyor?" sorusunun net bir cevabı yoktur.

Bu belirtiler framework'ten bağımsızdır. "Fat Controller" ve ikiz kardeşi "Fat Model" — aktif kayıt (active record) nesnesinde kalıcılıkla iç içe geçmiş iş kuralları — her dilde ve her stack'te aynı şekilde ortaya çıkar.

Uzun vadeli bakım maliyetleri

Karıştırmanın bedeli ilk gün görünmezdir ve sonrasında sürekli büyür. Kopyalanmış kurallar birbirinden uzaklaşır: İki controller aynı şeyi biraz farklı kontrol eder ve sistem girişe göre farklı davranır — tek bir kural artık var olmadığı için bulunması zor bir hata. Framework'e bağımlılık, her büyük güncellemeyi, hatta bir geçişi tehlikeli hâle getirir, çünkü aslında dokunulmaması gereken iş mantığına dokunur. Yeni ekip üyelerinin uyum sağlaması uzun sürer, çünkü kurallar dağınıktır ve hiçbir yerde bir bütün olarak durmaz. Ve her değişiklik istenmeyen bir yan etki riski taşır, çünkü controller'da protokol, akış, kural ve kalıcılık yan yana durur.

Bu maliyetlerin hiçbiri başlangıçta hissedilmez — uzun ömürlülüğü belirleyen tüm maliyetler gibi birikirler. Onları tehlikeli kılan da tam olarak budur: Bugünün ucuz kısayolu, yarının pahalı değişikliğidir.

Aynı fark, sonuçlarıyla ölçüldüğünde:

BoyutController'da kurallarDomain'de kurallar
Test edilebilirlikyalnızca framework ve HTTP ilesaf unit test olarak
Erişilebilirlikyalnızca HTTP üzerindenHTTP, CLI, kuyruk, iş, test üzerinden
Framework bağımlılığıyüksekçekirdekte hiç yok
Kuralın yeridağınık, çoğu zaman kopyalanmıştek bir yerde
Değişiklik maliyetiartan, riskliyerel
Okunabilirlikprotokolle karışıkkural tek başına

Öneri: Sonradan büyüyen bedelden kaçınmak için küçük yapısal bedeli erken ödeyin — girişten bağımsız olarak kurallar tek bir yerde. Bedeli, hemen görünür bir faydası olmayan gerçek bir ön çalışmadır: Sistem küçük olduğu sürece ayrılmış yapı daha pahalıdır. Kısa ömürlü bir sistemde — bir prototipte, bir kampanya sayfasında, sonu öngörülebilen bir araçta — farklı karar veririz; orada kısayolun faturası hiçbir zaman gelmez ve yapı boşa harcanmış olurdu.

Test üzerindeki etkileri

Farkı hiçbir şey test kadar somut hâle getirmez. Saf bir iş kuralı — veritabanı, HTTP ve framework olmadan bir nesne — ucuz, hızlı ve kararlı şekilde test edilebilir: tam da en yüksek ROI'ye sahip test türü. Kuralı girdilerle çağırır ve sonucu kontrol edersiniz; milisaniyeler içinde, hiçbir kurulum olmadan.

Aynı kural controller'daysa ekonomi tersine döner. Onu kontrol etmek için framework'ü başlatmanız, bir isteği simüle etmeniz, çoğu zaman bir veritabanı hazırlamanız gerekir — özünde bunların hiçbirine ihtiyaç duymayan bir hesaplama için. Bu tür testler yavaş ve kırılgandır, bu yüzden daha seyrek yazılır ve pahalı çekirdek test edilmeden kalır. Ayrım bunu tersine çevirir: Bir hatanın pahalı olduğu yerleri tam da ucuz şekilde test edilebilir kılar.

Etki günlük işte kendini gösterir. Saf kural testlerinden oluşan bir test paketi saniyeler içinde çalışır ve bu yüzden sık çalıştırılır — her commit'ten önce, her pipeline'da. Her kural için framework'ü ayağa kaldıran bir paket dakikalar içinde çalışır ve bu yüzden kaçınılır. Hızlı testler kullanılır, yavaş testler atlanır; dolayısıyla ayrım yalnızca bir testin mümkün olup olmadığını değil, hata canlıya çıkmadan önce o testin hiç çalışmış olup olmayacağını da belirler.

Öneri: İş kurallarını, framework olmadan saf bir unit test olarak kontrol edilebilecekleri yere koyun. Bedeli, katmanlar arasındaki geçişi de eşlemek ve bu eşlemeyi de güvence altına almak zorunda olmanızdır. Altyapıyla ayrılmaz biçimde iç içe geçmiş mantıkta farklı karar veririz — örneğin özü veritabanı davranışının kendisi olan bir sorguda; orada bir entegrasyon testi, izole bir kural testinden daha dürüst bir mihenk taşıdır.

Framework bağımsızlığı

Framework bağımsızlığı kolayca yanlış anlaşılır. Framework'ten kaçınmak ya da onu her yerde kendi soyutlamalarınızın arkasına gizlemek anlamına gelmez — bu, pahalı ve karşılıksız bir erken soyutlama olurdu. Framework'ü güçlü olduğu yerde sonuna kadar kullanırsınız: routing, HTTP, istek biçiminin doğrulanması, kalıcılık için ORM. Bağımsız kalan yalnızca çekirdektir — iş kuralları framework'e bağlı değildir.

Pratik faydası ideolojik değil, somuttur. Aynı kurala birden fazla girişten erişilebilmelidir: bir HTTP isteğinden, bir komut satırı komutundan, bir kuyruk worker'ından, zamanlanmış bir işten, bir testten. Controller'daysa ona yalnızca HTTP erişir; domain'deyse hepsi erişir. Ve bir framework güncellemesi artık kuralları tehdit etmez, çünkü kurallar framework'ü tanımaz. Bu açıdan bakıldığında Laravel, Symfony, ASP.NET, Spring ve Rails'in hepsi aynıdır: kenardaki altyapı, hizmet ettikleri kurallardan daha kolay değiştirilebilir.

Bir örnek bunu somutlaştırır. Bir siparişin yalnızca ödeme, stok ve teslimat adresi birbiriyle uyumluysa tamamlanabileceği kuralının geçerli olduğunu varsayalım. Bu kural controller'da durduğu sürece ona yalnızca web formu üzerinden gelen yol erişir. Daha sonra komut satırı üzerinden bir içe aktarma, bir kuyruktan yeniden deneme ya da kendi arayüzü üzerinden bir iş ortağı eklenirse, bu yolların her biri kuralı yeniden yazmak zorunda kalır — ve o andan itibaren kopyalar birbirinden uzaklaşır. Aynı kural domain'deyse tüm yollar aynı tek kontrolü çağırır ve bir siparişin ne zaman tamamlanabileceğine dair tek bir doğru vardır.

Altyapı · framework, HTTP, ORM Use case'ler · orkestrasyon Domain Bağımlılık içe doğru işaret eder — domain dış katmanları tanımaz

Şema: Dış katmanlar domain'e bağlıdır, asla tersi değil — bu yüzden çekirdek framework'ten bağımsız kalır.

Öneri: Bağımlılıkları içe doğru işaret edecek şekilde tutun — domain hiçbir şeye bağlı değildir, framework ona bağlıdır. Bedeli sınırdaki eşleme emeğidir: Framework nesneleri ile domain nesneleri arasında çeviri yapılması gerekir. Sistemin bir bölümü özünde framework fonksiyonundan başka bir şey değilse — saf bir yönlendirme, ince bir görünüm — farklı karar veririz; orada framework'e bağımlılık bir günah değil, uygun bir sadeliktir.

Migrasyon stratejisi

Controller merkezli bir sistem büyük bir "Clean Architecture yeniden yazımı" ile düzeltilmez — bu, her big bang dönüşümüyle aynı hata olurdu. Mantığı adım adım, işlem işlem ayırırsınız.

Yolu bir kez öğrendiğinizde süreç mekaniktir. Şişkin bir controller alır, içinden bir iş kuralı seçer ve onu kendi testleri olan basit bir domain nesnesine taşırsınız. Davranış belirsizse, taşımadan önce onu karakterizasyon testleriyle sabitlersiniz. Ardından controller yalnızca ayrılmış kuralı çağırır. Geri kalanı, sırası gelene kadar dokunulmadan kalır. Böylece mantık parça parça içe doğru taşınır, her adım küçük ve geri alınabilirdir ve sistem baştan sona çalışmaya devam eder.

Nereden başlamalı? En iyisi en sık kopyalanmış ya da bozulduğunda en büyük hasarı veren kuraldan. Ayırmanın kazancı orada en büyüktür ve yeni yapıyı ilk kez kuran, orantısız derecede pahalı ilk adım kendini orada haklı çıkarır — sonraki her işlem bu yapıyı hazır bulur.

Öneri: Büyük bir dönüşüm yerine, testlerle güvence altına alınmış, tek tek kuralların adım adım ayrılmasıyla migrasyon yapın. Bedeli, iki stilin yan yana var olduğu bir geçiş dönemidir — bazı kurallar çoktan domain'de, diğerleri hâlâ controller'dadır. Tek seferde dönüştürülmesi geçiş döneminden daha ucuz olan çok küçük bir sistemde farklı karar veririz; orada risk sınırlı kaldığı için tek adımda düzeltme yapılabilir.

Sık yapılan hatalar

Bu konu etrafında tekrarlayan kalıplar:

  • Controller'da iş kuralları — kural protokole zincirlenmiştir ve yalnızca HTTP üzerinden erişilebilir.
  • Şişkin ORM modeli — kalıcılıkla iç içe geçen kurallar; biri değiştirilmeden diğeri değiştirilemez.
  • Aşırı düzeltme olarak anemik domain — yalnızca veri tutan nesneler, mantık ise yine controller'larda ve service'lerde son bulur; kurallar, yönettikleri verilere aittir.
  • Kopyalar birbirinden uzaklaşana kadar aynı kuralın birden fazla girişte kopyalanması.
  • Framework'ü her yerde soyutlamak — her ayrıntı için bir arayüz, pahalı ve karşılıksız.
  • Önemsiz kod için çok fazla katman — korunacak bir kuralın olmadığı yerde seremoni.
  • Controller'da orkestrasyon; böylece akış ve çeviri birbirine karışır.
  • Domain'de HTTP kavramları — durum kodları, request nesneleri — çekirdeği protokole bağlar.

Karar kontrol listesi

Bir ekibin bir controller'ı yazmadan ya da temizlemeden önce sorabileceği sorular. Bunlar hüküm değil, teşhis sorularıdır.

  • Bu kurala, onu kopyalamadan bir komut satırı komutundan ya da arka plan işinden erişilebilir mi? Hayırsa, fazla dışarıda duruyor.
  • Bu iş kuralını framework'ü başlatmadan ya da HTTP'yi simüle etmeden test edebilir miyim? Hayırsa, protokole bağlı.
  • Controller işle ilgili bir karar mı veriyor — yoksa yalnızca çeviri mi yapıyor? İçine yalnızca çeviri aittir.
  • Aynı kural tam olarak tek bir yerde mi tanımlı? Birden fazla yer, sapma (drift) demektir.
  • Domain içinde HTTP kavramları — durum kodu, request, response — görünüyor mu? Görünmemeli.
  • Framework'ü değiştirseydik iş kuralları değişir miydi? Değişmemeli.
  • Katman sayısı soruna uygun mu — yoksa önemsiz kod üzerine seremoni mi? Yapı ilkeyi değil, karmaşıklığı izler.

Sıkça Sorulan Sorular

Bu, küçük uygulamalar için aşırı mühendislik değil mi? Gerçek kuralları olmayan bir uygulama için — saf oluşturma, okuma, güncelleme, silme — evet. Orada controller üzerinden giden doğrudan yol uygun sadeliktir. Ayrım, test etmek, yeniden kullanmak ve yıllar boyunca değiştirmek istediğiniz iş kuralları ortaya çıktığı anda karşılığını verir. Mesele ilke değil, orantıdır.

Doğrulama controller'a mı, domain'e mi aittir? İkisine de, ancak iki farklı doğrulama olarak. İsteğin biçimini — alanlar mevcut mu, tipler doğru mu — kenarda, controller'da kontrol edersiniz. İşe dair kural — bu durum geçişine izin var mı, bu değişmez korunuyor mu — domain'e aittir. Fark şudur: Biri anlamsız girdilere karşı korur, diğeri iş kuralının ta kendisidir.

Peki mantık taşıyan ActiveRecord ya da ORM modelleri ne olacak? Şişkin model, şişkin controller ile aynı hatadır, yalnızca bir katman daha içeride: İş kuralları kalıcılıkla iç içe geçer. Kuralları zengin domain nesnelerinde tutabilir ve kalıcılığı ayrı yönetebilirsiniz. Pragmatik olarak küçük bir sistem ORM'e daha yakın kalabilir — mesele belirli bir kalıbı dayatmak değil, kuralları kaydetme işlemine zincirlememektir.

Anemik bir domain de kötü değil mi? Evet, bu diğer uç noktadır: Mantık başka yerdeyken yalnızca veri tutan nesneler. Kurallar mümkün olduğunca yönettikleri verilere aittir. Amaç mantığı nesnelerden sürmek değil, onu controller'dan çıkarmaktır — ve karar veren bir domain, yalnızca taşıyan bir domain'den iyidir.

Bu, framework'ü kullanmamak anlamına mı geliyor? Tam tersine. Framework'ü güçlü olduğu yerde sonuna kadar kullanırsınız — routing, HTTP, ORM, biçim doğrulaması. Bağımsız kalan yalnızca çekirdektir. Framework bağımsızlığı framework'ten uzaklaşmak değil, sistemin içinde bir sınırdır.

Transaction sınırı nereye aittir? Orkestrasyona, yani use case'e — kalıcılık hakkında hiçbir şey bilmemesi gereken domain'e değil, bir karar olarak controller'a da değil. Use case bir işlemin adımlarını kapsar; transaction orada açılır ve kapanır.

İleri okuma

Temelini Batunet Engineering Method oluşturur: iş mantığı domain'e, framework altyapı olarak, sınırlar bilinçli ve küçük adımlarla.


Bir framework on yıllık bir araçtır, bir iş kuralı ise daha uzun ömürlü bir varlık. İkisini karıştıran kişi kalıcı olanı değiştirilebilir olana bağlar — ve bunu ancak değiştirilebilir olanın değiştirilmesi gerektiğinde fark eder.

İlgili kavramlar ve teknolojiler

Bu alanda somut bir projeniz mi var?

Teknik rehberlerimiz yaklaşımımızı gösterir. Projenizin ihtiyaçlarını doğrudan şirket yönetimiyle değerlendirin.