sazak@labs:~
oturum

etiketler

CQRSsoftware architectureClean Architecture.NETEnterprise PatternsSOLID

Software · Apr 12, 2026 · 11 dk okuma

Kurumsal .NET Mimarisinde Doğru Sınırlar: Clean Architecture, SOLID ve Enterprise Patterns

Büyük ölçekli .NET sistemlerinde sürdürülebilir mimari, katman sayısıyla değil, bağımlılıkların yönü ve değişikliklerin etkisiyle belirlenir. Clean Architecture, SOLID ve Enterprise Patterns üzerinden uygulanabilir bir dönüşüm yaklaşımı.

Kurumsal .NET Mimarisinde Doğru Sınırlar: Clean Architecture, SOLID ve Enterprise Patterns

Kurumsal .NET sistemlerinde sürdürülebilir mimarinin temel ölçütü, projenin kaç katmana ayrıldığı değil, bir iş kuralı değiştiğinde sistemin hangi parçalarının değişmek zorunda kaldığıdır. Clean Architecture, SOLID ve Enterprise Design Patterns bu değişim maliyetini kontrol etmek için kullanılan araçlardır. Ancak doğru sınırlar kurulmadan uygulandıklarında, çözmeye çalıştıkları karmaşıklığın üzerine yeni bir karmaşıklık ekleyebilirler.

Büyük ölçekli uygulamalarda mimari kararların etkisi zamanla belirginleşir. Başlangıçta birkaç endpoint ve database tablosuyla çalışan sistem, yeni entegrasyonlar, farklı transaction gereksinimleri ve sürekli değişen iş kurallarıyla büyür. Bu noktada mimarinin görevi yalnızca kodu düzenlemek değildir. Değişikliklerin yayılmasını sınırlamak, kritik davranışları test edilebilir tutmak ve ekiplerin birbirinin çalışma alanını sürekli bozmadan geliştirme yapabilmesini sağlamaktır.

Architecture ve Design farklı karar seviyeleridir

Software Architecture, sistemin temel yapı taşlarını, bu parçalar arasındaki ilişkileri ve değiştirilmesi maliyetli kararları kapsar. Design ise bu yapı içinde belirli bir davranışın nasıl gerçekleştirileceğine odaklanır. İkisi birbirinden tamamen bağımsız değildir; ancak kararların kapsamı farklıdır.

Bir sipariş sisteminde Application katmanının Infrastructure detaylarına bağımlı olmaması mimari bir karardır. Sipariş indiriminin Strategy pattern ile hesaplanması ise genellikle design kararıdır. İlki geniş bir kod tabanının bağımlılık yönünü belirlerken ikincisi belirli bir iş davranışının genişletilme biçimini belirler.

Bu ayrım önemlidir çünkü her design problemini architecture seviyesine taşımak gereksiz maliyet üretir. Tek bir fiyat hesaplama algoritmasını değiştirmek için yeni project, assembly ve dependency registration oluşturmak çoğu zaman doğru yatırım değildir.

Tersine, database erişiminin doğrudan HTTP endpoint içinde gerçekleşmesi yalnızca küçük bir kod düzenleme problemi sayılmamalıdır. İş kuralları transport mekanizmasına ve persistence detaylarına bağlanıyorsa değişiklik maliyeti sistem genelinde artacaktır.

İyi bir mimari karar, gelecekteki bütün ihtiyaçları tahmin etmeye çalışmaz. Bugün bilinen değişim nedenlerini birbirinden ayırır ve gelecekte geri alınması pahalı olacak bağımlılıkları kontrol eder.

Bu nedenle mimariyi değerlendirirken project sayısından önce dependency graph incelenmelidir. Hangi assembly hangisini referans alıyor? Domain modelinin çalışması için Entity Framework Core gerekiyor mu? Bir business rule testi database başlatmadan çalışabiliyor mu? Bu sorular klasör yapısından daha fazla bilgi verir.

N-Layer dönüşümünde asıl mesele dependency direction

Geleneksel N-Layer uygulamalarında Presentation, Business ve Data Access katmanları bulunur. Tipik dependency direction Presentation → Business → Data Access şeklindedir. Bu yapı sorumlulukları ayırır; fakat Business katmanı somut persistence implementasyonlarına bağımlıysa iş kuralları hâlâ altyapı kararlarından etkilenir.

Clean Architecture bu ilişkiyi değiştirir. Dependency Rule gereği source-code bağımlılıkları içeriye, yani iş kurallarını temsil eden katmanlara yönelir. Runtime sırasında dış sistemlerle iletişim kurulması bu kuralı değiştirmez. Kod bağımlılığı ile çalışma zamanındaki çağrı yönü aynı şey değildir.

Kurumsal bir .NET uygulamasında sınırlar genellikle dört temel alanla ifade edilir.

Domain, sistemin iş açısından anlamlı davranışlarını ve invariant olarak adlandırılan, her geçerli durumda korunması gereken kuralları içerir. Entity, Value Object, Domain Event ve uygun olduğunda Aggregate burada bulunur. Domain katmanı HTTP, EF Core, message broker veya UI framework bilgisine ihtiyaç duymamalıdır.

Application, use case koordinasyonundan sorumludur. Sipariş oluşturmak, ödeme talep etmek veya müşteri limitini güncellemek gibi işlemler burada yürütülür. Application, Domain davranışlarını kullanır ve dış sistemlerden ihtiyaç duyduğu yetenekleri interface üzerinden tanımlar. Transaction sınırının hangi use case etrafında kurulacağı da çoğunlukla bu seviyede belirlenir.

Infrastructure, database erişimi, message broker, dosya sistemi ve harici servis entegrasyonlarının somut implementasyonlarını içerir. EF Core DbContext, HTTP client ve persistence adapter gibi bileşenler bu sınırın doğal parçalarıdır.

Presentation ise HTTP endpoint, controller, request binding ve response üretimi gibi transport sorumluluklarını taşır. Presentation'ın görevi iş kuralını yeniden yazmak değil, Application davranışını erişilebilir hâle getirmektir.

Pratik bir dependency düzeninde Domain bağımsızdır; Application, Domain'e bağımlıdır; Infrastructure, Application ve Domain tarafından tanımlanan sözleşmeleri uygular; Presentation ise Application'a erişir. Host project, Dependency Injection registration sırasında Infrastructure implementasyonlarını sisteme bağlayabilir. Bu composition root ilişkisi, iş kurallarının Infrastructure'a bağımlı olduğu anlamına gelmez.

Dependency Inversion Principle burada belirleyicidir. Yüksek seviyeli iş politikaları düşük seviyeli implementasyonlara bağlı olmamalı; iki taraf da uygun abstraction üzerinden ilişki kurmalıdır.

Dependency Injection ise bu ilişkinin runtime sırasında kurulmasını sağlayan tekniktir. ASP.NET Core DI container bir interface ile implementation arasındaki bağlantıyı oluşturabilir. Ancak constructor'a interface eklemek tek başına Dependency Inversion sağlamaz. Interface yanlış katmanda tanımlanmışsa veya Infrastructure modelini dışarı sızdırıyorsa mimari bağımlılık hâlâ korunuyor olabilir.

Örneğin Application katmanındaki bir interface doğrudan DbSet veya IQueryable döndürüyorsa EF Core davranışları Application'a taşınabilir. IQueryable özellikle sorgunun nerede çalıştığını ve hangi provider tarafından yorumlandığını belirsizleştirir. Bu durumda soyutlama fiziksel olarak vardır, fakat davranışsal sınır yeterince güçlü değildir.

N-Layer sisteminden Clean Architecture'a geçerken ilk adım bütün solution yapısını yeniden oluşturmak olmamalıdır. Önce bağımlılıklar, transaction sınırları ve iş kurallarının bulunduğu yerler belirlenmelidir.

Ardından değişim sıklığı yüksek bir use case seçilerek Domain davranışı ayrıştırılabilir. Application contract tanımlanır, Infrastructure implementasyonu bu contract'a bağlanır ve Presentation doğrudan database erişiminden çıkarılır. Her adımda mevcut davranışı doğrulayan testler korunmalıdır.

Bu yaklaşım büyük bir yeniden yazım yerine kontrollü migration sağlar. Özellikle aktif kullanılan sistemlerde mimari dönüşümün teslimat akışını durdurmaması, hedef mimarinin teorik saflığından daha değerli olabilir.

SOLID ve Enterprise Patterns mekanizmayı değiştirmelidir

Clean Architecture hakkında yaygın açıklama, bağımlılık oklarının içeri bakmasının sistemi otomatik olarak sürdürülebilir yaptığıdır. Bu açıklama eksiktir. Eksik kalan mekanizma change coupling, yani bir değişikliğin başka bileşenlerde zorunlu değişiklik oluşturma derecesidir. Dependency direction doğru olsa bile aynı iş davranışı çok sayıda service, mapper, handler ve repository arasında dağılmışsa değişiklik maliyeti yüksek kalır.

SOLID ilkeleri bu problemi daha küçük ölçekte ele alır. Single Responsibility Principle, her class'ın yalnızca tek method yapması gerektiğini söylemez. Bir bileşenin değişmesi için aynı sorumluluk alanından kaynaklanan nedenlerin bulunmasını hedefler. Sipariş fiyatlandırması ile SMTP gönderiminin aynı class içinde olması bu nedenle sorunludur; iki davranış farklı nedenlerle değişir.

Open/Closed Principle, yeni davranışların mevcut ve kararlı kodu sürekli değiştirmeden eklenebilmesini amaçlar. Bunun için her ihtimali baştan interface hâline getirmek gerekmez. Gerçekten değişen fiyatlandırma algoritmalarında Strategy anlamlıdır; tek algoritmanın bulunduğu yerde Strategy interface oluşturmak gelecekteki belirsizliği bugünkü maliyete dönüştürebilir.

Liskov Substitution Principle, bir implementation'ın contract yerine geçtiğinde beklenen davranışı korumasını gerektirir. Örneğin farklı payment provider implementasyonlarından biri başarısız işlemlerde exception fırlatırken diğeri sessizce başarılı response dönüyorsa ortak interface güvenilir bir contract sunmaz.

Interface Segregation Principle, tüketicilerin ihtiyaç duymadıkları operation'lara bağımlı bırakılmamasını savunur. Bir raporlama bileşeninin yalnızca okuma ihtiyacı varken kapsamlı bir CRUD repository interface alması gereksiz bağımlılık yaratır.

Dependency Inversion Principle ise bu kararları bileşen sınırlarına taşır. Application'ın belirli bir SQL implementation'ını değil, ihtiyaç duyduğu persistence yeteneğini bilmesi gerekir.

Enterprise Patterns seçimi de aynı mantığa dayanmalıdır: Pattern, adı için değil çözdüğü problem için kullanılmalıdır.

Repository, Domain veya Application açısından anlamlı persistence operation'larını kapsüllemek için yararlıdır. Aggregate yüklemek ve kaydetmek gerektiğinde açık bir contract sağlayabilir. Ancak EF Core zaten repository benzeri davranışlar sunduğundan, bütün entity'ler için generic repository oluşturmak çoğu sistemde fazladan bir katman üretir.

Unit of Work, birden fazla persistence değişikliğinin tek transaction sınırında koordine edilmesini sağlar. EF Core DbContext bu davranışın önemli bölümünü zaten sağlar. Ayrı bir Unit of Work abstraction ancak transaction yönetimini uygulamanın açık bir contract'ı hâline getiriyorsa veya gerçek bir bağımsızlık ihtiyacı karşılıyorsa anlam kazanır.

Specification, tekrar eden ve iş açısından anlamlı filtreleme kurallarını kapsüllemek için kullanılabilir. Örneğin aktif müşteriler arasından kredi limiti uygun olanları seçen kural farklı use case'lerde tekrar ediyorsa Specification yararlı olabilir. Buna karşılık her LINQ ifadesini Specification nesnesine çevirmek sorguların okunabilirliğini azaltır.

Strategy, çalışma koşullarına göre değişen algoritmaları birbirinden ayırır. Factory ise nesne oluşturma süreci koşullu, karmaşık veya invariant korumaya bağlı olduğunda değer üretir. Factory kullanımı yalnızca constructor çağrısını farklı bir class'a taşımaktan ibaretse soyutlamanın getirisi sınırlıdır.

Burada uygulanabilir bir seçim ölçütü vardır: Pattern kaldırıldığında iş kuralı daha fazla yere mi dağılacak, yoksa kod daha mı anlaşılır olacak? İkinci durum geçerliyse pattern muhtemelen erken uygulanmıştır.

Sipariş onayı gerçek sınırları ortaya çıkarır

Bir kurumsal sipariş sisteminde sipariş onaylama işlemini düşünelim. HTTP endpoint bir sipariş numarası alır. Sistem siparişin mevcut durumunu kontrol eder, kredi limitini değerlendirir, stok ayırma talebi oluşturur ve onay sonucunu kaydeder.

N-Layer yapısında bu işlem tek bir OrderService içinde bulunabilir. Service doğrudan DbContext kullanır, stok servisine HTTP çağrısı yapar ve işlem tamamlandığında e-posta gönderir. Başlangıçta anlaşılır görünen bu düzen, stok entegrasyonu değiştiğinde veya sipariş kurallarına yeni koşullar eklendiğinde kırılganlaşır.

Clean Architecture dönüşümünde önce hangi davranışın Domain'e ait olduğu belirlenir. Bir siparişin iptal edilmiş durumdan onaylanamaması Domain invariant olabilir. Kredi limiti kontrolünün hangi veriye ihtiyaç duyduğu ise Domain modeli ve Application koordinasyonu arasında dikkatle ayrılmalıdır.

Order aggregate, geçerli state transition kurallarını korur. Application katmanındaki ConfirmOrderHandler siparişi yükler, gerekli kontrolleri koordine eder ve Domain davranışını çağırır. Handler'ın görevi sipariş durumunu doğrudan property assignment ile değiştirmek değil, Domain'in sunduğu operation üzerinden ilerlemektir.

Persistence için IOrderRepository gibi amaçlı bir contract kullanılabilir. Bu interface'in bütün CRUD operation'larını taşıması gerekmez. Aggregate yükleme ve kaydetme davranışları yeterli olabilir. EF Core implementation Infrastructure içinde kalır.

Stok sistemi harici bir HTTP API ise Application, IInventoryReservationGateway gibi bir port üzerinden bu yeteneği talep edebilir. Infrastructure adapter gerçek HTTP çağrısını gerçekleştirir. Burada port, Application'ın ihtiyaç duyduğu dış yeteneği tanımlayan contract'tır; adapter ise o contract'ın teknik implementasyonudur.

Fakat önemli bir transaction problemi ortaya çıkar. SQL database transaction'ı ile harici stok API çağrısı normal şartlarda tek bir atomik transaction oluşturmaz. Database commit başarılı olurken HTTP çağrısı başarısız olabilir veya karşı sistem işlemi tamamladığı hâlde response kaybolabilir.

Bu nedenle sipariş onayını tek bir synchronous method içinde yürütmek her zaman doğru değildir. İş süreci eventual consistency kabul ediyorsa sipariş PendingConfirmation durumuna alınabilir, Outbox pattern ile aynı database transaction'ında bir integration event kaydedilebilir ve ayrı bir worker stok ayırma sürecini tamamlayabilir.

Outbox kaydı ile sipariş state değişikliğinin aynı local transaction içinde commit edilmesi kritik noktadır. Bu yöntem event'in broker'a tam olarak bir kez teslim edileceğini garanti etmez. Worker yeniden deneme yapabileceğinden consumer tarafında idempotency gerekir. Aynı reservation identifier ile gelen tekrar taleplerin ikinci kez stok düşmemesi buna örnektir.

Burada CQRS değerlendirmesi de anlam kazanır. Command Query Responsibility Segregation, state değiştiren operation'larla veri okuyan operation'ların modellerini ayırır. Her command için handler oluşturmak CQRS'in tüm faydasını otomatik olarak sağlamaz.

Sipariş onaylama tarafında invariant koruyan Domain modeli gerekirken, sipariş listesi ekranı yalnızca tarih, müşteri ve durum alanlarını isteyebilir. Bu durumda read tarafında doğrudan projection kullanmak, bütün aggregate'i yüklemekten daha uygundur.

CQRS için ayrı database zorunlu değildir. Aynı database üzerinde farklı command ve query modelleriyle başlanabilir. Ayrı read store ancak sorgu yükü, veri şekli veya bağımsız ölçekleme gereksinimi bunu haklı çıkarıyorsa değerlendirilmelidir.

Read model'in asynchronous güncellendiği tasarımlarda kullanıcıların kısa süreli eski veri görebileceği kabul edilmelidir. Sipariş onayından hemen sonra kesin güncel durum göstermek gerekiyorsa read-after-write beklentisi ayrıca çözülmelidir.

Bu örnekte doğru mimari, yalnızca dört project oluşturmak değildir. Domain invariant'larının korunması, transaction sınırının açık olması, tekrar işleme davranışının tanımlanması ve dış sistem hatalarının yönetilmesi gerçek tasarımın kendisidir.

Kararı ADR ile kaydedin, abstraction'ı gerekçelendirin

Kurumsal mimaride en pahalı problemlerden biri, bir kararın neden alındığının zamanla kaybolmasıdır. Architecture Decision Record, yani ADR, önemli bir mimari kararın bağlamını, değerlendirilen alternatifleri, seçilen yaklaşımı ve beklenen sonuçlarını kaydeden kısa belgedir.

Örneğin sipariş sistemi için bir ADR şu kararı içerebilir: Sipariş onayı ile stok ayırma işlemi tek distributed transaction içinde yönetilmeyecek; local transaction ve Outbox kullanılacaktır. Gerekçe, stok servisinin bağımsız çalışması ve iki sistem arasında atomik commit garantisinin bulunmamasıdır.

Aynı ADR, bu kararın maliyetini de belirtmelidir. Eventual consistency kabul edilir, retry ve idempotency implementation'ı gerekir, operasyon ekibinin başarısız event'leri takip edebilmesi sağlanmalıdır. Alternatif olarak synchronous çağrının neden yeterli bulunmadığı açıklanmalıdır.

ADR bir onay töreni değil, kararın gelecekte yeniden değerlendirilebilmesi için tutulan mühendislik kaydıdır. Karar değiştiğinde eski belge sessizce değiştirilmek yerine yeni bir ADR ile geçersiz kılınabilir.

Benim tercih ettiğim yaklaşım, büyük .NET sistemlerini tek seferde ideal bir Clean Architecture şemasına dönüştürmek yerine, yüksek değişim maliyeti oluşturan use case'lerden başlayarak bağımlılıkları kademeli biçimde düzeltmektir. Öncelik Domain invariant'larını korumak, Application ile Infrastructure arasındaki bağımlılığı tersine çevirmek ve transaction davranışını açık hâle getirmek olmalıdır.

Bu tercih her sistemde geçerli değildir. Kısa ömürlü bir uygulamada, basit CRUD ağırlıklı bir internal tool'da veya iş kuralları henüz belirginleşmemiş küçük bir serviste dört ayrı project, repository interface'leri ve çok sayıda handler geliştirme hızını düşürebilir. Böyle durumlarda tek deployable içinde düzenli feature sınırları ve doğrudan EF Core kullanımı daha anlaşılır bir çözüm olabilir.

Ayrıca Clean Architecture tek başına servislerin ayrılması gerektiği anlamına gelmez. Modüler bir monolith, bağımlılık sınırlarını koruyarak microservice mimarisinin operasyonel maliyetlerinden kaçınabilir. Bağımsız deployment ihtiyacı doğmadan servisleri dağıtmak network failure, tracing ve consistency problemlerini gereksiz yere erkene çeker.

Kararı verirken ölçülebilir sinyallere bakılmalıdır: Bir iş kuralı değişikliği kaç modülü etkiliyor, testler hangi dış sistemleri başlatmak zorunda kalıyor, aynı davranış kaç yerde tekrar ediyor ve deployment bağımsızlığı gerçekten gerekli mi? Mimari soyutlamanın maliyeti, önlediği değişiklik maliyetinden büyükse sadeleştirilmelidir.

Sürdürülebilir .NET mimarisinin hedefi maksimum abstraction değildir. Doğru yerde kararlı contract'lar, değişimin yoğun olduğu yerde açık sınırlar ve bütün kritik kararlar için anlaşılır gerekçeler oluşturmaktır. Mimari kararın kalitesi, kullanılan pattern sayısıyla değil, sistem değiştiğinde koruduğu hareket alanıyla ortaya çıkar.

yorumlar

0

Henüz yorum yok. İlk notu sen bırak.

Yorum yazmak için giriş yap.

Gmail ile saniyeler içinde, ya da e-posta ile kaydol.

bağlı 26109:32:57