Veritabanı Odaklı N-Tier Mimariden Hexagonal Mimariye 3 Aşamalı Geçiş: Core Domain İzolasyonu
Blog'a Dön

Veritabanı Odaklı N-Tier Mimariden Hexagonal Mimariye 3 Aşamalı Geçiş: Core Domain İzolasyonu

Buğra Şıkel

Veritabanı Odaklı N-Tier Mimariden Hexagonal Mimariye 3 Aşamalı Geçiş: Core Domain İzolasyonu

Giriş

Production ortamında saniyede 8.500 işlem (8.5K TPS) karşılayan bir ödeme geçidi sistemini yönetiyorsanız, N-Tier (N-Katmanlı) mimarinin sınırlarına er ya da geç çarpmanız kaçınılmazdır. Tipik bir N-Tier tasarımında iş kuralları (Business Layer), veritabanı erişim katmanına (DAL) ve dolayısıyla Object-Relational Mapper (ORM) kütüphanesinin spesifikasyonlarına göbekten bağlıdır. 2018 yılında 65 milyon satırlık bir Transactions tablosunu SQL Server’dan PostgreSQL’e taşımak istediğimizde, iş mantığımızın arasına sızmış [Table], [Column] attribute’ları ve Entity Framework’e özel LINQ sorguları yüzünden bu taşıma işlemi tam 8 ay sürdü. Sorun veritabanında değil, mimarinin merkezine veritabanını koyan hatalı tasarımdaydı.

Hexagonal Mimari (Ports and Adapters), sistemi altyapı bağımlılıklarından kurtararak iş mantığını (Core Domain) merkeze alır. Ancak 10 yıllık, 400.000 satır koda sahip legacy bir N-Tier uygulamayı bir gecede Hexagonal yapıya dönüştürmek, sistemi geri döndürülemez bir kaosa sürükler. Müşteriye yansıyan 500 (Internal Server Error) hatalarını %0.01’in altında tutarak bu dönüşümü gerçekleştirmek cerrahi bir hassasiyet gerektirir.

Aşağıdaki rehber, veritabanı sızıntısı (database leakage) yaşayan legacy bir sistemi, %100 izole edilmiş bir Core Domain’e dönüştürmenin 3 aşamalı, production-tested yol haritasını içermektedir. Bu geçiş sonrası test yazım sürelerinin 4.5 günden 2.5 saate düştüğünü, framework güncellemelerinin ise haftalar yerine saatler içinde tamamlandığını göreceksiniz.

İçindekiler

  • Mevcut Durum Analizi: N-Tier Antipattern’i ve ORM Sızıntısı
  • Aşama 1: Bağımlılıkların Tersine Çevrilmesi (Dependency Inversion) ve Port Tanımları
  • Aşama 2: Core Domain İzolasyonu ve Adapter İmplementasyonu
  • Aşama 3: Trafik Taşıma, Shadow Read ve Geri Dönüş (Rollback) Stratejisi
  • Trade-off Analizi: Hexagonal Mimari Ne Zaman Tercih Edilmemeli?
  • Pratik Öneriler ve Production Notları
  • Sık Sorulan Sorular
  • Sonuç

Mevcut Durum Analizi: N-Tier Antipattern’i ve ORM Sızıntısı

Geleneksel N-Tier mimaride akış yukarıdan aşağıya doğrudur: Presentation -> Business -> Data Access -> Database. Bu yapıda Data Access Layer (DAL) sistemin en altında yer alır ve Business Layer (BLL) doğrudan DAL’a bağımlıdır. C#/.NET veya Java/Spring ekosistemlerinde bu durum genellikle OrderService sınıfının, veritabanını temsil eden OrderEntity sınıfını doğrudan döndürmesi ve değiştirmesiyle sonuçlanır.

Bağımlılık oklarının veritabanına doğru aktığı her sistemde, veritabanı şemasındaki bir değişiklik tüm iş mantığını derleme (compile) zamanında kırar.

Bir e-ticaret sepet hesaplama modülünde indirim kurallarını test etmek için in-memory veritabanı (örn. SQLite veya EF Core InMemory) ayağa kaldırmak zorunda kalıyorsanız, Core Domain’iniz izole değildir. Test suite’inin çalışması 12 saniyeden 8 dakikaya çıkmışsa, I/O bağımlılığınız iş mantığınızı boğuyor demektir. Hedefimiz, okların yönünü tersine çevirerek altyapıyı (veritabanı, message broker, cache) birer eklenti (plugin) haline getirmektir.

Aşama 1: Bağımlılıkların Tersine Çevrilmesi (Dependency Inversion) ve Port Tanımları

İlk adımda hiçbir mevcut kodu silmiyoruz. Sadece Business Layer’ın DAL ile iletişim kurduğu noktaları tespit edip araya sözleşmeler (Interface/Port) yerleştiriyoruz. Bu aşamada Entity sınıfları hala Domain içinde dolaşabilir, ancak DAL sınıflarının doğrudan instanceları yerine interfaceler kullanılmalıdır.

Sistemin kalbinde yer alacak Core Domain projesini oluşturun. Bu projenin referanslarına baktığınızda EF Core, Hibernate, Dapper, AWS SDK, RabbitMQ gibi HİÇBİR altyapı kütüphanesi bulunmamalıdır. Referans listesi sıfır olmalıdır.

  • Port’ların Tanımlanması: Core Domain içerisinde IOrderRepository, IPaymentGateway gibi arayüzler oluşturun.
  • Use Case’lerin Yeniden Yazılması: Mevcut Service sınıflarını (örn: OrderManager) yeni oluşturduğunuz Port’lara (Interface) bağımlı hale getirin.

Bu aşamanın sonunda mimari derlenir durumda kalmalı, ancak bağımlılık yönü (Dependency Rule) yavaş yavaş Domain’e doğru dönmeye başlamalıdır. P99 gecikmelerinde bu aşamada herhangi bir değişiklik beklenmez.

Aşama 2: Core Domain İzolasyonu ve Adapter İmplementasyonu

En kritik ve riskli aşama budur. Artık DAL katmanındaki OrderEntity (veritabanı tablosu) ile Core Domain’deki Order (İş modeli) sınıflarını birbirinden ayırmamız gerekiyor. ORM framework’üne ait olan [Key], [Column("order_date")] gibi attribute’lar Core Domain’e asla giremez.

Aşağıdaki C# kod örneği, izole edilmiş bir Domain modeli ile veritabanı Entity’si arasındaki geçişi (Adapter) göstermektedir:

// 1. CORE DOMAIN (Hiçbir framework bağımlılığı yok) 
namespace ECommerce.Domain.Orders
{
    public class Order
    {
        public Guid Id { get; private set; }
        public decimal TotalAmount { get; private set; }
        public OrderStatus Status { get; private set; }

        public Order(Guid id, decimal totalAmount)
        {
            if (totalAmount <= 0) throw new ArgumentException("Tutar 0'dan büyük olmalıdır.");
            Id = id;
            TotalAmount = totalAmount;
            Status = OrderStatus.Created;
        }

        public void Complete()
        {
            Status = OrderStatus.Completed;
        }
    }

    // Port (Secondary/Driven)
    public interface IOrderRepository
    {
        Task<Order> GetByIdAsync(Guid id, CancellationToken token);
        Task SaveAsync(Order order, CancellationToken token);
    }
}

// 2. INFRASTRUCTURE / ADAPTER (EF Core bağımlılığı burada yaşar)
namespace ECommerce.Infrastructure.Persistence
{
    internal class OrderEntity
    {
        public Guid Id { get; set; }
        public decimal TotalAmount { get; set; }
        public int StatusCode { get; set; }
        public DateTime CreatedAt { get; set; } 
    }

    public class OrderRepositoryAdapter : IOrderRepository
    {
        private readonly AppDbContext _context;

        public OrderRepositoryAdapter(AppDbContext context)
        {
            _context = context;
        }

        public async Task<Order> GetByIdAsync(Guid id, CancellationToken token)
        {
            var entity = await _context.Orders.AsNoTracking()
                               .FirstOrDefaultAsync(x => x.Id == id, token);
            if (entity == null) return null;

            // Entity'den Domain Model'e Mapping
            var order = new Order(entity.Id, entity.TotalAmount);
            // State restorasyonu (Reflection veya Internal constructor ile yapılabilir)
            return order;
        }

        public async Task SaveAsync(Order order, CancellationToken token)
        {
            // Domain Model'den Entity'e Mapping
            var entity = new OrderEntity
            {
                Id = order.Id,
                TotalAmount = order.TotalAmount,
                StatusCode = (int)order.Status,
                CreatedAt = DateTime.UtcNow
            };
            _context.Orders.Update(entity);
            await _context.SaveChangesAsync(token);
        }
    }
}

Bu yapıda OrderRepositoryAdapter sınıfı, veritabanı detaylarını (AppDbContext, Entity) Core Domain’den gizler. Domain sadece Order bilir. Bu sayede veritabanını MongoDB’ye geçirmek istediğinizde sadece yeni bir Adapter yazmanız yeterli olur; iş kurallarının bulunduğu Domain kodunun tek bir satırı bile değişmez.

Aşama 3: Trafik Taşıma, Shadow Read ve Geri Dönüş (Rollback) Stratejisi

Kod bazındaki yapısal dönüşümü tamamladıktan sonra, bu yeni mimariyi production ortamında %100 trafiğe açmak intihardır. Bellek tahsisi (Memory Allocation) tarafında Entity-Domain dönüşümleri (mapping) yüzünden Garbage Collector (GC) üzerinde ekstra baskı oluşacaktır. Migration sürecini Strangler Fig pattern ve Shadow Read tekniği ile kademeli olarak yürütmeliyiz.

Kademeli Geçiş Planı

  1. Hafta 1 (Dark Launch): Yeni Hexagonal yapı production’a deploy edilir ancak canlı trafik eski N-Tier servisleri üzerinden akmaya devam eder.
  2. Hafta 2 (Shadow Read / Gölge Okuma): Okuma (Read) istekleri hem eski N-Tier servisine hem de asenkron olarak yeni Hexagonal servise (fire-and-forget şeklinde) gönderilir. İki servisin döndürdüğü yanıtlar bir Comparator aracı ile karşılaştırılır ve uyumsuzluklar loglanır. Örneğin: Mismatch detected: N-Tier returned status 2, Hexagonal returned status 3 for OrderID: 5543.
  3. Hafta 3 (Dual Write): Yazma işlemleri eski yapı üzerinden yapılırken, yeni yapıya ait Adapter’lar üzerinden veritabanına paralel yazma simülasyonları yapılır. P99 gecikmeleri metriklenir.
  4. Hafta 4 (Cut-over): Feature Flag (örn: LaunchDarkly veya AWS AppConfig) üzerinden trafik %5, %20, %50 ve nihayetinde %100 oranında yeni mimariye yönlendirilir.

Risk Noktaları ve Rollback Stratejisi

Entity ile Domain modeli arasındaki sürekli map’leme işlemi, özellikle saniyede 5.000 üzeri okuma yapılan endpoint’lerde Gen 0 Heap allocations miktarını %15-20 oranında artırabilir. CPU utilization %75’in üzerine çıkar veya HTTP 5xx hata oranı %0.05 eşiğini geçerse, Kubernetes cluster üzerindeki konfigürasyon değişkeni USE_HEXAGONAL_FLOW=false yapılarak anında eski koda dönülmelidir (Rollback süresi hedefi: < 2 saniye).

Trade-off Analizi: Ne Zaman Hexagonal, Ne Zaman N-Tier?

Mimari kararların doğrusu veya yanlışı yoktur, sadece trade-off’ları vardır. Hexagonal mimari bir gümüş kurşun değildir.

Karşılaştırma Kriteri Geleneksel N-Tier Mimari Hexagonal (Ports & Adapters) Mimari
Geliştirme Hızı (Time to Market) İlk aşamada yüksektir. Basit CRUD operasyonları için MVC -> Service -> DB akışı dakikalar içinde yazılır. İlk aşamada düşüktür. Port, Adapter ve Domain sınıflarının ayrı ayrı oluşturulması boilerplate kod yaratır.
Test Edilebilirlik Zordur. DB veya HTTP Client mock’lamak I/O bağımlılıkları yüzünden karmaşıktır. Entegrasyon testine mecbur bırakır. Mükemmeldir. Core Domain tamamen saf (pure) kodlardan oluştuğu için unit testler milisaniyeler içinde koşar.
Performans (Latency / Memory) Entity nesnesi katmanlar arası doğrudan taşındığı için memory allocation düşüktür. Domain ve Entity arasındaki mapping (AutoMapper vs) sebebiyle P99 latency’de 5-10ms overhead yaratabilir. GC baskısı artar.
Bakım Maliyeti (Uzun Vadede) Proje büyüdükçe Spagetti koda dönüşme riski yüksektir. ORM değişimi projeyi felç edebilir. Sınırlar (Boundaries) net çizildiği için 100+ geliştiricili ekiplerde bile conflict yaratmadan scale olur.

Karar Kuralı: Sadece bir tabloyu okuyup ekrana basan, kompleks iş kuralı içermeyen (CRUD yoğun) mikroservisler için N-Tier veya basit katmanlı mimari kullanın. Ancak bir sepetin tutarını hesaplayan, stok düşen, kampanyaları uygulayan ve risk analizi yapan karmaşık (Core) domainler için kesinlikle Hexagonal Mimariyi tercih etmelisiniz.

Pratik Öneriler / Production Notları

  • AutoMapper Kullanımına Dikkat: Domain ile Entity arasındaki dönüşümlerde AutoMapper gibi reflection tabanlı kütüphaneler yerine, yüksek trafikli modüllerde manuel mapping (örn. extension metotlar) kullanın. Reflection, p99 latency metriklerinde saniyelik spike’lara (dalgalanmalara) neden olabilir.
  • Domain Event’leri: Aggregate Root sınıflarınız içinde state değiştiğinde (örn: order.Complete()) veritabanına doğrudan yazmak yerine Domain Event fırlatın. Altyapı katmanı bu event’leri alıp RabbitMQ veya Kafka’ya iletmekle (Adapter) yükümlü olmalıdır.
  • Monitoring: Geçiş sürecinde Prometheus veya Datadog üzerinde app_domain_mapping_duration_ms şeklinde özel metrikler oluşturun. Mapping işleminin 2ms sınırını aşmadığından emin olun.
  • Test Kapsamı: Migration başlamadan önce legacy N-Tier sistemin uçtan uca (E2E) test coverage’ını %80 seviyesine çıkarın. Refactoring sırasında sizi koruyacak tek güvenlik ağı bu testlerdir.

Sık Sorulan Sorular

Hexagonal mimari mikroservisler için zorunlu mudur?

Hayır. Monolitik uygulamalarda da Hexagonal mimari mükemmel çalışır. Hexagonal mimari sistemin deployment modeliyle (Monolit vs Mikroservis) değil, modüllerin iç tasarımı ve bağımlılık yönetimiyle ilgilenir.

Entity ve Domain modeli arasındaki mapping kod tekrarı değil midir?

Görünüşte evet, felsefede hayır. Entity veritabanı tablolarının şemasını, Domain modeli ise iş kurallarını temsil eder. Zamanla veritabanı şemasını normalize/denormalize etmek istediğinizde (örneğin iki tabloyu birleştirmek), Domain modeliniz bu I/O operasyonundan tamamen habersiz kalır. Bu ayrım kod tekrarı değil, bir izolasyon sigortasıdır.

Read operasyonları (CQRS) için de bu katmanlardan geçmeli miyiz?

Karmaşık iş kuralları içermeyen salt okuma (Query) işlemlerinde Domain modelini ayağa kaldırmak gereksiz bir yüktür. P99 120ms hedeflerini tutturmak için okuma tarafında doğrudan veritabanına giden ince (thin) bir adapter (örneğin Dapper kullanarak) yazmak ve veriyi doğrudan DTO olarak dönmek en pragmatik çözümdür.

Mevcut projemde 300’den fazla servis var, nereden başlamalıyım?

Şirketinizin temel para kazandığı veya en çok değişime uğrayan (Core) modülünden başlayın. Kullanıcı profil yönetimi gibi jenerik (Generic Subdomain) kısımları legacy N-Tier yapısında bırakabilirsiniz. Her yeri dönüştürmeye çalışmak ROI (Yatırımın Geri Dönüşü) açısından mantıklı değildir.

Sonuç

Veritabanı odaklı N-Tier mimariden Hexagonal mimariye geçiş, sistemin I/O (veritabanı, framework) prangalarından kurtularak gerçek iş mantığına odaklanmasını sağlayan radikal bir paradigma değişimidir. 3 aşamalı izolasyon, Port/Adapter entegrasyonu ve shadow read stratejileri ile bu geçişi sıfır kesintiyle tamamlayabilirsiniz. Domain modellerini anemic (kansız, sadece getter/setter içeren) yapıdan kurtarıp, iş kurallarının encapsulate edildiği zengin nesnelere dönüştürmek, kod tabanınızın yaşam süresini en az 5 yıl uzatacaktır.

Aksiyon Adımı: Yarın sabah mevcut projenizdeki en karmaşık iş mantığını barındıran Service sınıfını açın ve import/using satırlarına bakın. Eğer orada System.Data.SqlClient, Microsoft.EntityFrameworkCore veya javax.persistence görüyorsanız, ilk Port interface’inizi yazarak izolasyon yolculuğunuza bugün başlayın.

Bunları da beğenebilirsiniz

Contract Testing vs End-to-End: Mikroservislerde Entegrasyon Açmazı ve Test Piramidi
2 Haziran 2026

Contract Testing vs End-to-End: Mikroservislerde Entegrasyon Açmazı ve Test Piramidi

Mikroservis mimarisinde 45 dakikalık flaky E2E testlerinden, 15ms’lik Contract testlerine geçiş sürecini, Pact konfigürasyonlarını ve CI/CD trade-off’larını inceliyoruz.

Devamını Oku
Edge Cihazlarda YOLOv8 ile Gerçek Zamanlı Nesne Tespiti: Docker ve NVIDIA Jetson Üzerinde Performans Optimizasyonu
29 Ocak 2026

Edge Cihazlarda YOLOv8 ile Gerçek Zamanlı Nesne Tespiti: Docker ve NVIDIA Jetson Üzerinde Performans Optimizasyonu

Bu kapsamlı rehberde, YOLOv8 modelini kullanarak NVIDIA Jetson edge cihazlarda gerçek zamanlı nesne tespitini nasıl optimize edeceğinizi öğreneceksiniz. Docker ve TensorRT entegrasyonuyla performansı zirveye taşıyın.

Devamını Oku
ClickHouse Dağıtık Tablo Mimarisinde Data Skew: Sharding Key Seçimi ve Resharding Stratejileri
1 Nisan 2026

ClickHouse Dağıtık Tablo Mimarisinde Data Skew: Sharding Key Seçimi ve Resharding Stratejileri

ClickHouse kümelerinde performans darboğazlarına yol açan data skew (veri dengesizliği) problemini gidermek için doğru sharding key seçimi ve gelişmiş resharding tekniklerini keşfedin.

Devamını Oku
AI Asistan