Clean Architecture ve CQRS ile Ölçeklenebilir Backend Mimarisi
Giriş: Spaghetti Kod Kabusundan Kurtulma Vakti Hepimiz o projeyle en az bir kez karşılaşmışızdır: Tek bir Controller dosyasının 1500 satır olduğu, iş kurallarının (business logic) veritabanı sorgularının arasına sıkıştığı ve ufacık bir …

Giriş: Spaghetti Kod Kabusundan Kurtulma Vakti
Hepimiz o projeyle en az bir kez karşılaşmışızdır: Tek bir Controller dosyasının 1500 satır olduğu, iş kurallarının (business logic) veritabanı sorgularının arasına sıkıştığı ve ufacık bir değişikliğin projenin bambaşka bir yerini patlattığı o kaotik kod tabanları...
Junior veya orta seviye bir yazılımcı olarak iş hayatına adım attığınızda, uygulamanın "sadece çalışması" ilk başlarda yeterli gelebilir. Ancak proje büyüdükçe, ekibe yeni kişiler katıldıkça ve müşteri talepleri karmaşıklaştıkça kodun sürdürülebilir ve test edilebilir olması hayati bir hal alır.
İşte bu yazıda, .NET ekosisteminde fırtınalar estiren iki güçlü yaklaşımı inceleyeceğiz: Clean Architecture (Temiz Mimari) ve CQRS (Command Query Responsibility Segregation). Gelin, teorik karmaşaya boğulmadan pratik örneklerle bu mimariyi nasıl kurgulayacağımıza bakalım!
1. Clean Architecture Nedir ve Neden İhtiyacımız Var?
Uncle Bob (Robert C. Martin) tarafından popülerleştirilen Clean Architecture'ın temel amacı şudur: İş kurallarınızı (Core Domain), dış dünyadan (Veritabanı, UI, 3. Parti kütüphaneler) bağımsız kılmak.
Geçişi basitleştirmek için katmanları dıştan içe doğru 4 ana halkada düşünebiliriz:
1. Domain Katmanı: Uygulamanızın kalbidir. Entity'ler, Value Object'ler ve domain kuralları burada yer alır. Hiçbir dış kütüphaneye bağımlılığı yoktur.
2. Application Katmanı: İş senaryolarının (Use Cases) yönetildiği yerdir. DTO'lar, Interface'ler ve CQRS yapımız bu katmanda yaşar.
3. Infrastructure Katmanı: Veritabanı erişimi (Entity Framework, Dapper), e-posta gönderimi, dış servis entegrasyonları gibi teknik detaylar burada yer alır.
4. Presentation (API) Katmanı: Kullanıcıyla veya dış sistemlerle iletişim kurduğumuz katmandır (Controllers, Minimal APIs).
2. CQRS: Okuma ve Yazma Sorumluluklarını Ayırmak
CQRS (Command Query Responsibility Segregation), uygulamadaki okuma (Query) ve yazma (Command) işlemlerini birbirinden ayıran bir tasarım desenidir.
- Command (Komut): Sistemin durumunu değiştiren işlemlerdir (Ekleme, Güncelleme, Silme). Genellikle veri döndürmezler veya sadece işlem kimliği (Id) döndürürler.
- Query (Sorgu): Sistemin durumunu değiştirmeyen, sadece veri okuyan işlemlerdir. Yan etkisi (side-effect) yoktur.
Bu ayrım bize ne kazandırır? Okuma ve yazma senaryolarını bağımsız olarak ölçekleyebilir, okuma performansını optimize etmek için farklı veritabanı modelleri veya önbellek (Redis) stratejileri kullanabiliriz.
3. Pratik Örnek: MediatR ile CQRS Uygulaması
Sözü fazla uzatmadan koda geçelim. .NET projemizde CQRS desenini uygulamak için yaygın olarak MediatR kütüphanesini kullanırız.
Senaryomuz: Yeni bir ürün (Product) oluşturma işlemi (Command) ve Id'ye göre ürün getirme işlemi (Query).
Step 1: Command ve Handler Tanımlama
Uygulamanın Application katmanında yeni bir ürün eklemek için Command ve bunun işleyicisini (Handler) yazıyoruz:
using MediatR;
namespace CleanArchitecture.Application.Products.Commands.CreateProduct;
// Command: Dışarıdan alınacak parametreler ve dönüş tipi (Guid)
public record CreateProductCommand(string Name, decimal Price, int Stock) : IRequest<Guid>;
// Handler: Komutun çalıştıracağı iş mantığı
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
{
// Gerçek senaryoda burada IApplicationDbContext veya Repository inject edilir
public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
// 1. Domain nesnesini oluştur
var productId = Guid.NewGuid();
// 2. Veritabanına kaydetme simülasyonu
// await _context.Products.AddAsync(new Product(productId, request.Name, request.Price, request.Stock));
// await _context.SaveChangesAsync(cancellationToken);
return await Task.FromResult(productId);
}
}Step 2: Query ve Handler Tanımlama
Şimdi de veriyi okumak için bir Query yazalım:
using MediatR;
namespace CleanArchitecture.Application.Products.Queries.GetProductById;
public record ProductDto(Guid Id, string Name, decimal Price);
public record GetProductByIdQuery(Guid Id) : IRequest<ProductDto?>;
public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, ProductDto?>
{
public async Task<ProductDto?> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
{
// Veritabanından okuma simülasyonu (Sadece okuma olduğu için AsNoTracking kullanılabilir)
var product = new ProductDto(request.Id, "Test Laptop", 25000m);
return await Task.FromResult(product);
}
}Step 3: Controller Katmanında Kullanım
Presentation (API) katmanındaki Controller'ımız inanılmaz derecede temiz ve ince (thin) kalır. Controller sadece istekleri karşılar ve IMediator aracılığıyla ilgili Handler'a yönlendirir:
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace CleanArchitecture.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
{
var productId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetById), new { id = productId }, productId);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await _mediator.Send(new GetProductByIdQuery(id));
if (result is null)
return NotFound();
return Ok(result);
}
}Özet ve Tavsiyeler
Clean Architecture ve CQRS kombosu ilk bakışta dosya sayısını artırıyor gibi görünebilir ve küçük (CRUD) projeler için "Overengineering" (aşırı mühendislik) riski taşıyabilir. Ancak proje büyüdükçe bu mimarinin faydalarını altın tepside görürsünüz:
- Kolay Test Edilebilirlik: Handler'larınızı veritabanından bağımsız unit testler ile kolayca test edebilirsiniz.
- Geliştirici Paralelliği: Farklı yazılımcılar aynı proje üzerinde çatışma (conflict) yaşamadan farklı Command ve Query'ler yazabilir.
- Tek Sorumluluk İlkesi (SRP): Her sınıf sadece tek bir işi mükemmel bir şekilde yapar.
Unutmayın, her mimari her proje için şart değildir. Ancak orta ve büyük ölçekli bir projeye başlarken bu yapıyı kurgulamak, gelecekteki uykusuz gecelerinizi engellemenin en pratik yoludur.
Kodunuz temiz, mimariniz sağlam olsun! Sorularınızı yorumlarda paylaşmaktan çekinmeyin.