Layered Architecture: Presentation, Business ve Data Layer'larının Gerçek Sorumlulukları
Layered Architecture'ın asıl vaadi closed layer'ların sağladığı izolasyondur, ama .NET'te ProjectReference varsayılan olarak transitive olduğu için Web, Business üzerinden Data'yı sessizce görür; DisableTransitiveProjectReferences bu kuralı derleyiciye taşır. N-layer ve N-tier farkı, closed ve open layer, sinkhole, DB-centric tasarım ve dependency yönünün sorunu.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un ilk hali tek bir proje olarak başlamıştı. Ekip büyüyünce kodu üç layer'a ayırmaya karar veriyor ve en klasik yapıyı kuruyor: Web, Business'a; Business, Data'ya referans veriyor. Web'in Data'yı doğrudan görmemesi, yani her isteğin Business'tan geçmesi hedefleniyor.
dotnet new sln -n Bookstore
dotnet new web -n Bookstore.Web -o Bookstore.Web
dotnet new classlib -n Bookstore.Business -o Bookstore.Business
dotnet new classlib -n Bookstore.Data -o Bookstore.Data
dotnet sln add Bookstore.Web Bookstore.Business Bookstore.Data
dotnet add Bookstore.Web reference Bookstore.Business
dotnet add Bookstore.Business reference Bookstore.Data
Data layer'ı kitap satırlarını tutuyor (gerçek projede burada Entity Framework Core (EF Core) DbContext'i olurdu), Business layer'ı üye indirimi ve stok kuralını uyguluyor:
namespace Bookstore.Data;
public sealed record BookRow(int Id, string Title, string AuthorName, decimal ListPrice, int Stock);
public sealed class BookRepository
{
private readonly List<BookRow> _rows =
[
new(1, "Dune", "Frank Herbert", 12.99m, 4),
new(2, "Emma", "Jane Austen", 7.50m, 0),
new(3, "The Hobbit", "J. R. R. Tolkien", 10.99m, 12)
];
public BookRow? FindById(int id) => _rows.FirstOrDefault(b => b.Id == id);
public IReadOnlyList<BookRow> All() => _rows;
}
using Bookstore.Data;
namespace Bookstore.Business;
public sealed record BookDetails(int Id, string Title, decimal Price, bool InStock);
public sealed class BookService(BookRepository books)
{
// Sinkhole: iş kuralı yok, isteği olduğu gibi Data'ya iletiyor
public BookRow? GetBook(int id) => books.FindById(id);
// Business layer'ın asıl işi: üye indirimi ve stok kuralı
public BookDetails? GetDetails(int id, bool isMember)
{
BookRow? row = books.FindById(id);
if (row is null)
return null;
decimal price = isMember ? Math.Round(row.ListPrice * 0.90m, 2) : row.ListPrice;
return new BookDetails(row.Id, row.Title, price, row.Stock > 0);
}
}
Web tarafında üç endpoint var. Üçüncüsü, admin ekranı için acelesi olan birinin yazdığı bir kısayol:
using Bookstore.Business;
using Bookstore.Data;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<BookRepository>();
builder.Services.AddScoped<BookService>();
WebApplication app = builder.Build();
app.MapGet("/books/{id:int}", (int id, bool member, BookService service) =>
service.GetDetails(id, member) is { } book ? Results.Ok(book) : Results.NotFound());
app.MapGet("/books/{id:int}/raw", (int id, BookService service) =>
service.GetBook(id) is { } row ? Results.Ok(row) : Results.NotFound());
// "Hızlı yol": Business'ı atlayıp doğrudan Data'ya gidiyor
app.MapGet("/admin/stock", (BookRepository books) =>
books.All().Select(b => new { b.Title, b.Stock }));
app.Run();
Bookstore.Web'in csproj'unda yalnız Bookstore.Business'a bir ProjectReference var. using Bookstore.Data; satırı ve BookRepository'yi doğrudan kullanan endpoint derlenir mi?
Closed Layer Kâğıtta Kaldı
Derleniyor, hem de uyarısız. Nedenini görmek için derlenmiş assembly'lerin hangi Bookstore assembly'lerine referans verdiğine bakan küçük bir araç yazalım:
using System.Reflection;
foreach (string path in args)
{
AssemblyName[] references = Assembly.LoadFile(Path.GetFullPath(path)).GetReferencedAssemblies();
string bookstore = string.Join(", ", references
.Where(r => r.Name!.StartsWith("Bookstore."))
.Select(r => r.Name)
.Order());
Console.WriteLine($"{Path.GetFileNameWithoutExtension(path)} -> {bookstore}");
}
Bookstore.Web -> Bookstore.Business, Bookstore.Data
Bookstore.Business -> Bookstore.Data
Web, Data'ya doğrudan bağlı. SDK-style (SDK: Software Development Kit) projelerde ProjectReference varsayılan olarak transitive: Web, Business'a referans verince Business'ın referans verdiği her şey de Web'in derleyicisine görünür hale geliyor. Yani çizdiğimiz "Web → Business → Data" diyagramı bir niyet beyanı, derleyici açısından hiçbir anlamı yok. İlk yazıda mimarinin bir structure olduğunu söylemiştik; burada diyagramdaki structure ile koddaki structure ilk günden ayrışmış durumda.
Bu yazı, bu ayrışmanın neden önemli olduğunu ve Layered Architecture'ın asıl vaadini nasıl koruyacağımızı anlatıyor. Önce kavramları netleştirelim.
N-Layer ve N-Tier
İki terim sık karıştırılır. Layer mantıksal bir ayrımdır: kodu sorumluluğa göre gruplar ve hangi grubun hangisini kullanabileceğine kural koyar. Tier fiziksel bir ayrımdır: kodun ayrı process'lerde ya da ayrı makinelerde çalışmasıdır. Microsoft'un "Common web application architectures" dokümanı bunu açıkça söyler: layer'lar mantıksal ayrımı temsil eder, uygulama mantığı ayrı sunuculara ya da process'lere dağıtıldığında bunlara tier denir ve N-layer bir uygulamanın tek bir tier'a deploy edilmesi çok yaygındır.
Bookstore tam olarak böyle: üç layer, tek bir deployment unit. Bookstore.Web, Bookstore.Business.dll ve Bookstore.Data.dll aynı process'te çalışıyor; veritabanı ise ayrı bir tier. Bu ayrımın pratik sonucu büyük: layer'lar arası çağrı bir method çağrısı, nanosaniyeler sürüyor; tier'lar arası çağrı bir network çağrısı, milisaniyeler sürüyor ve hata verebiliyor. Azure Architecture Center'ın N-tier sayfası da bu yüzden uyarıyor: fiziksel ayrım scalability ve resiliency kazandırır ama her ek network adımı latency ekler. Layer'ları "ileride ayrı sunuculara taşırız" diye tasarlamak, bugün ödenmeyen bir bedeli yarına bırakmaktır; o ayrım ayrı bir karar ve bu serinin monolith ve stil haritası yazılarının konusu.
Closed ve Open Layer'lar
Richards ve Ford'un Fundamentals of Software Architecture kitabı (FoSA, 2. baskı, 2025) layered architecture'ın kalbine iki kavram koyar. Closed layer: bir request bir layer'dan geçerken hemen altındaki layer'a gitmek zorundadır, onu atlayamaz. Open layer: üstteki layer bu layer'ı atlayıp bir alttakine gidebilir.

Closed layer'ların sağladığı şeyin adı layers of isolation: bir layer'daki değişiklik, layer'lar arasındaki contract'lar değişmediği sürece diğer layer'ları etkilemez. Data layer'ında SQL'i (Structured Query Language) elle yazmaktan EF Core'a geçmek, Business'ın Data'ya açtığı yüzey aynı kaldıkça Web'i hiç ilgilendirmez. Açılıştaki /admin/stock kısayolu bu izolasyonu deliyor: artık Data'daki BookRow'un bir property'sinin adı değişirse Web de değişmek zorunda.
Open layer bir hata değil, bir tasarım seçeneği. FoSA'nın örneği, ortak servisleri (e-posta, PDF üretimi, dosya saklama gibi) içeren bir layer'ı business ile persistence arasına koyup open işaretlemek: Business ona ihtiyaç duyduğunda kullanır, duymadığında atlayıp doğrudan Data'ya gider. 2000'lerin başındaki Fast-Lane Reader pattern'i de sadece okuma yapan ekranların business layer'ı atlayıp veriyi doğrudan okumasıydı. Önemli olan, hangi layer'ın open, hangisinin closed olduğunun bilinçli bir karar olması ve yazılması. FoSA'nın uyarısı açık: bu yazılmazsa ekip her layer'ı atlanabilir sayar ve sonuç sıkı bağlı, kırılgan bir sistem olur. Açılıştaki kısayol tam olarak böyle doğdu: kimse Data'yı open ilan etmedi, ama derleyici izin verdiği için open'a dönüştü.
Kuralı Derleyiciye Taşımak
Bir mimari kural yalnız dokümanda ve code review'da yaşıyorsa, er geç delinir. Kuralı derleyicinin kontrol edebileceği bir yere taşımak en ucuz korumadır. Web projesinde transitive project reference'ı kapatalım:
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Bookstore.Business\Bookstore.Business.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Program.cs(2,17): error CS0234: The type or namespace name 'Data' does not exist in the namespace 'Bookstore' (are you missing an assembly reference?)
Tek bir hata görüyorsun, ama bu bütün tablo değil. Bu örnekte derleyici, using satırındaki hatayı bulunca kodun gövdesindeki hataları raporlamadı. using Bookstore.Data; satırını silip tekrar derleyince asıl liste ortaya çıkıyor (silinen satır yüzünden satır numaraları bir yukarı kaydı):
Program.cs(12,5): error CS0012: The type 'BookRow' is defined in an assembly that is not referenced. You must add a reference to assembly 'Bookstore.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Program.cs(15,29): error CS0246: The type or namespace name 'BookRepository' could not be found (are you missing a using directive or an assembly reference?)
Program.cs(4,31): error CS0246: The type or namespace name 'BookRepository' could not be found (are you missing a using directive or an assembly reference?)
Üç hata, üç farklı mimari sorun. 15. satırdaki CS0246 beklediğimiz kısayol. 12. satırdaki CS0012 daha ilginç: Web, Data'yı hiç anmıyor ama Business'ın GetBook method'u Data'nın BookRow tipini döndürüyor. Business layer'ı, alttaki layer'ın tipini kendi public yüzeyine sızdırmış; closed layer'ı Web değil, Business delmiş. 4. satırdaki CS0246 ise composition root: DI (Dependency Injection) registration'ı yapmak için Web'in Data'daki class'ı tanıması gerekiyor. Microsoft'un dokümanı da bu durumu not eder ve en basit çözümün UI (User Interface) projesinin altyapı projesine referans vermesi olduğunu, ama bu tiplerin kullanımının composition root ile sınırlı tutulması gerektiğini söyler.
Biz burada daha sıkı bir yol seçelim: registration'ı Business'ın kendisine taşıyalım, sızan tipi Business'a ait bir tiple değiştirelim ve kısayolu Business üzerinden geçirelim. Business projesine Microsoft.Extensions.DependencyInjection.Abstractions paketi (10.0.14) eklendi:
using Bookstore.Data;
using Microsoft.Extensions.DependencyInjection;
namespace Bookstore.Business;
public static class ServiceCollectionExtensions
{
// Data layer'ın registration'ı da Business'ın içinde kalıyor
public static IServiceCollection AddBookstoreBusiness(this IServiceCollection services)
{
services.AddSingleton<BookRepository>();
services.AddScoped<BookService>();
return services;
}
}
using Bookstore.Data;
namespace Bookstore.Business;
public sealed record BookDetails(int Id, string Title, decimal Price, bool InStock);
public sealed record StockLevel(string Title, int Stock);
public sealed class BookService(BookRepository books)
{
public BookDetails? GetDetails(int id, bool isMember)
{
BookRow? row = books.FindById(id);
if (row is null)
return null;
decimal price = isMember ? Math.Round(row.ListPrice * 0.90m, 2) : row.ListPrice;
return new BookDetails(row.Id, row.Title, price, row.Stock > 0);
}
// Bilinçli pass-through: kural yok ama Web, Data'nın tiplerini görmüyor
public IReadOnlyList<StockLevel> GetStockLevels() =>
[.. books.All().Select(b => new StockLevel(b.Title, b.Stock))];
}
using Bookstore.Business;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddBookstoreBusiness();
WebApplication app = builder.Build();
app.MapGet("/books/{id:int}", (int id, bool member, BookService service) =>
service.GetDetails(id, member) is { } book ? Results.Ok(book) : Results.NotFound());
app.MapGet("/admin/stock", (BookService service) => service.GetStockLevels());
app.Run();
Derleme temiz, ve referans aracı artık diyagramla aynı şeyi söylüyor:
Bookstore.Web -> Bookstore.Business
Bookstore.Business -> Bookstore.Data
DisableTransitiveProjectReferences yalnız derleme zamanı görünürlüğünü kapatıyor; Bookstore.Data.dll yine Web'in output klasörüne kopyalanıyor ve uygulama çalışıyor:
curl "http://127.0.0.1:5205/books/1?member=true"
curl "http://127.0.0.1:5205/books/2?member=false"
curl "http://127.0.0.1:5205/admin/stock"
{"id":1,"title":"Dune","price":11.69,"inStock":true}
{"id":2,"title":"Emma","price":7.50,"inStock":false}
[{"title":"Dune","stock":4},{"title":"Emma","stock":0},{"title":"The Hobbit","stock":12}]
Bu tek property her şeyi çözmüyor. Biri Web'e Data'ya doğrudan bir ProjectReference eklerse kural yine delinir; buna karşı CI'da (Continuous Integration) çalışan bir architecture test gerekir. Bunu serinin son yazısında, fitness function'larla yapacağız. Ama "kazara" delinmeyi, yani en sık görülen durumu, sıfır maliyetle engelliyor.
Layer'ların Gerçek Sorumlulukları
Klasör adlarını koymak kolay; hangi kodun nereye ait olduğuna karar vermek zor. ASP.NET Core Web API (Application Programming Interface) serisinin "Katmanlı Yapı: Service Katmanı ve Repository Tartışması" yazısı bunu controller ve service seviyesinde tartışmıştı; burada mimari seviyeden bakalım.
Presentation layer'ın işi dış dünyayla konuşmak: HTTP (Hypertext Transfer Protocol) isteğini almak, model binding ve input validation, authentication, Business'ın dönüşünü JSON (JavaScript Object Notation) ve status code'a çevirmek. "Üyeye yüzde 10 indirim" kuralı burada yaşarsa, aynı kuralı mobil uygulamanın API'si ya da bir arka plan job'ı için tekrar yazmak zorunda kalırsın. Test sorusu basit: bu kod, sistem HTTP yerine bir message queue'dan çağrılsaydı da gerekli olur muydu? Cevap evetse Presentation'a ait değil.
Business layer'ın işi kurallar ve use case'ler: indirim, stok kontrolü, siparişin hangi durumdan hangisine geçebileceği, bir işlemin transaction sınırı. Açılıştaki GetDetails gerçek bir Business method'u; GetBook ise değildi.
Data layer'ın işi verinin nasıl saklandığı: SQL, EF Core mapping'i, connection yönetimi, cache. Business'a "kitabı getir" der gibi bir yüzey açar, "hangi tabloya hangi join'le" sorusunu kendine saklar.
Bu yapının adı technical partitioning: kod, teknik rolüne göre bölünüyor. Avantajı tanıdık olması; her developer nereye bakacağını bilir. Bedeli ise domain değişikliklerinde ortaya çıkar: "kitaba bir orijinal dil alanı ekle" isteği üç layer'ın üçüne de dokunur, çünkü bir iş kavramı üç teknik parçaya dağılmış durumda. Bunun alternatifi domain partitioning (kodu Catalog, Ordering gibi iş alanlarına göre bölmek) ve bir sonraki yazının konusu.
Anti-Pattern'ler: Sinkhole, DB-Centric Tasarım ve Utils
Architecture sinkhole. Açılıştaki GetBook, isteği hiçbir iş yapmadan Data'ya ileten bir method'du. FoSA buna architecture sinkhole anti-pattern'i der: request'ler layer'lardan geçer ama hiçbir layer bir şey yapmaz. Her layer'ı closed tutmanın kaçınılmaz bir yan etkisi, ve tek başına sorun değil. FoSA bir 80-20 kuralı önerir: request'lerin yüzde 20'si sinkhole ise kabul edilebilir; yüzde 80'i sinkhole ise layered architecture bu problem için yanlış stil olabilir. Son haldeki GetStockLevels da bilinçli bir pass-through: kural yok, ama Web'i Data'nın tiplerinden ayırıyor. Bookstore'da kaç endpoint'in böyle olduğunu saymak, stilin uygunluğu hakkında somut bir ölçü verir.
DB-centric (DB: database) tasarım. Data'daki BookRow bir tablo satırının şekli. İlk halde bu şekil Business'ın public API'sinden Web'e, oradan da JSON olarak dışarıya akıyordu; CS0012 hatası bunun derleyicideki izi. Bu yapıda veritabanı şeması bütün sistemin modeli olur: bir kolonun adını değiştirmek API contract'ını değiştirir. İlk yazıdaki sipariş numarası örneği gibi, şema sistem sınırını geçmiş olur. Son halde Business kendi tipini (BookDetails) dönüyor ve şema Data'nın içinde kalıyor.
"Her şey Utils'e". Hiçbir layer'a ait olmayan kod için açılan Bookstore.Common ya da Utils projesi zamanla tarih biçimlendirmeden e-posta göndermeye, string uzantılarından iş kurallarına kadar her şeyi toplar. Herkes ona bağlı olduğu için en çok değişen ve en riskli proje haline gelir. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisinin "Component Prensipleri" yazısındaki CCP (Common Closure Principle) ve CRP (Common Reuse Principle) burada iyi bir rehber: birlikte değişmeyen ve birlikte kullanılmayan şeyler aynı projede durmamalı. Shared bir layer gerekiyorsa adı, sorumluluğu ve open olduğu yazılmalı.
Dependency Yönünün Sorunu
Layered architecture'ın en çok eleştirilen yanı dependency yönü. Klasik yapıda compile-time dependency'ler yukarıdan aşağıya akar: Web → Business → Data. Bu, uygulamanın en değerli kodunu (iş kurallarını) en teknik detaya (veri erişimine) bağımlı kılar. Microsoft'un dokümanı da bunu dezavantaj olarak sayar: business logic layer, data access implementation'ına ve çoğu zaman bir veritabanının varlığına bağımlıdır, bu yüzden test etmek için genellikle bir test veritabanı gerekir.
Bookstore'da bunu BookService'in constructor'ında görüyorsun: BookRepository'yi doğrudan alıyor. Üye indirimi kuralını test etmek için gerçek bir repository kurmak zorundasın. Çözüm, Business'ın ihtiyaç duyduğu interface'i Business'ın içinde tanımlayıp Data'nın onu implement etmesi, yani dependency'yi tersine çevirmek. SOLID ve Design Principles serisinin "Dependency Inversion Principle" yazısında class seviyesinde gördüğümüz bu fikir, mimari seviyede uygulanınca Clean, Onion ve Hexagonal Architecture adlarını alıyor; bu kategorinin bir sonraki büyük serisi tam olarak bu.
Bu bir "layered kötüdür" hükmü değil. Dependency yönünü tersine çevirmek her proje için interface, mapping ve dolaylılık demek. Üç kişilik bir ekibin, iş kuralı az ve CRUD (Create, Read, Update, Delete) ağırlıklı bir uygulamada bu bedeli ödemesi gerekmeyebilir. Bookstore'un Business layer'ı büyüdükçe, özellikle sipariş ve ödeme kuralları karmaşıklaştıkça bu bedel ödenmeye değer hale gelecek.
Ne Zaman Layered, Ne Zaman Değil?
Layered architecture iyi bir başlangıç noktasıdır: herkes tanır, kurması ucuzdur, küçük ekiplerde ve iş kuralı ağırlığı düşük uygulamalarda yıllarca iyi çalışır. Henüz domain'in sınırları net değilken teknik bir ayrımla başlamak da makul. FoSA'nın değerlendirmesinde layered stilin güçlü yanları basitlik ve düşük maliyet, zayıf yanları ise scalability, elasticity ve deploy edilebilirlik.
Şu belirtiler stilin sınırına geldiğini gösterir: request'lerin büyük çoğunluğu sinkhole; her iş değişikliği bütün layer'lara dokunuyor ve aynı dosyalarda birden fazla ekip çakışıyor; bir parçayı (örneğin katalog aramasını) ayrı ölçeklemek istiyorsun ama her şey tek deployment; Business layer test edilemeyecek kadar Data'ya bağımlı. Bu belirtilerin her biri farklı bir yöne işaret ediyor: Clean/Hexagonal, domain partitioning ve modular monolith ya da ayrı servisler. Bir sonraki iki yazı bu yönlerin haritasını çıkaracak.
Sık Düşülen Tuzaklar
Diyagrama güvenip derleyiciye bakmamak. SDK-style projelerde project reference'lar transitive; "Web, Data'yı görmez" kuralı varsayılan olarak hiçbir şey tarafından korunmuyor. DisableTransitiveProjectReferences ile kuralı derleyiciye taşı ve assembly referanslarını ara ara kontrol et.
Alttaki layer'ın tiplerini public yüzeyde sızdırmak. Business'ın method'u Data'nın entity'sini dönüyorsa closed layer'ı Business deliyor demektir. Her layer kendi contract tiplerini açmalı.
Her layer'ı "nasılsa closed" diye sinkhole'larla doldurmak. Yüzde 80'i pass-through olan bir Business layer, bir mimari değil bürokrasi. Pass-through oranını say; yüksekse stili sorgula ya da bazı layer'ları bilinçli olarak open ilan et.
N-layer'ı N-tier sanmak. Layer'ları ayrı process'lere bölmek bir characteristic'in (scalability, security sınırı) gerektirdiği ayrı bir karar. Gerekçesiz yapılırsa her method çağrısı bir network çağrısına dönüşür.
Composition root'u bahane etmek. "DI için Web'in Data'ya referansı lazım" doğru olabilir, ama bu referans Program.cs dışına yayılmamalı. Registration'ı layer'ın kendi extension method'una taşımak, referansı tamamen kaldırmanın en basit yolu.
Küçük Bir Deneme
★☆☆ Kendi solution'ında Web projesine DisableTransitiveProjectReferences ekle ve derle. Kaç hata çıkıyor? Her biri hangi türde: kısayol mu, sızan tip mi, composition root mu?
★★☆ Açılıştaki referans aracını solution'ındaki bütün assembly'ler için çalıştır ve çıktıyı bir dependency listesine çevir. Diyagramındaki oklarla karşılaştır; diyagramda olmayan bir ok var mı?
★★★ BookService'in BookRepository yerine Business içinde tanımlı bir IBookReader interface'ine bağımlı olmasını sağla ve Data'nın onu implement etmesini yap. Project reference'ların yönü nasıl değişmek zorunda? Üye indirimi kuralını veritabanısız test eden bir unit test yaz.
Bu yazıda Bookstore'un içini layer'lara ayırdık; ama dışarıdan bakınca hâlâ tek bir uygulama, tek bir deployment. Bir sonraki yazıda bu "tek parça" halin kendisini konuşacağız: monolith'in türleri, Big Ball of Mud'ın nasıl oluştuğu ve neden Bookstore'un bugün için bir monolith olması gerektiği.