Kısım 1: DDD Dünyasına Merhaba ve Temel Kavramlar
Bölüm 1: Yazılım Geliştirmenin Kalbine Yolculuk: Neden DDD?
- 1.1. Kod ve İş Dünyası Arasındaki Uçurum: Projeler neden karmaşıklaşır?
- 1.2. 2025'in Gerçekliği: Hızla değişen iş ihtiyaçları (Örnek: 2025 Paris Olimpiyatları için geliştirilen anlık biletleme sisteminin zorlukları).
- 1.3. Domain-Driven Design (DDD) Nedir? Teknik bir jargondan fazlası.
- 1.4. DDD'nin İki Temel Direği: Stratejik ve Taktiksel Tasarım.
- 1.5. Bu Kitap Size Ne Vaat Ediyor? Sıfırdan uzmanlığa giden yol haritanız.
Bölüm 2: Her Şeyin Başladığı Yer: Domain (Alan Adı)
- 2.1. "Domain" Sadece Bir Kelime Değildir: İş probleminizin evreni.
- 2.2. Domain Uzmanları (Domain Experts): Projenizin gizli kahramanları.
- 2.3. Her Yerde Aynı Dili Konuşmak: Ubiquitous Language (Evrensel Dil)
- Örnek: 2025 yılında popüler olan bir "Sürdürülebilir Enerji Ticaret Platformu" geliştirdiğimizi düşünelim. "Enerji Sertifikası", "Karbon Kredisi", "Arz Talep Eşleşmesi" gibi terimlerin hem yazılımcılar hem de enerji analistleri için aynı anlama gelmesini nasıl sağlarız?
- 2.4. Problem Alanı (Problem Space) ve Çözüm Alanı (Solution Space): Doğru sorunu çözdüğümüzden emin olmak.
Bölüm 3: Stratejik Tasarım - Büyük Resmi Görmek
- 3.1. Bounded Context (Sınırlı Bağlam): Karmaşıklığı yönetilebilir parçalara ayırmak.
- Örnek: Bir e-ticaret sistemini düşünelim. "Satış", "Stok Yönetimi" ve "Müşteri İlişkileri" bağlamları nasıl ayrılır? Bir ürünün "Satış" bağlamındaki fiyatı ile "Stok Yönetimi" bağlamındaki maliyeti neden farklı kavramlardır?
- 3.2. Context Map (Bağlam Haritası): Bounded Context'ler arasındaki ilişkileri görselleştirmek.
- Partnership (Ortaklık): İki takımın kaderi bir.
- Shared Kernel (Paylaşılan Çekirdek): Riskli ama bazen gerekli bir evlilik.
- Customer-Supplier (Müşteri-Tedarikçi): Güç dengeleri.
- Conformist (Uyumlu): Büyük balığı takip etmek.
- Anticorruption Layer (Bozulmayı Önleyici Katman): Eski sistemlerle modern dünyayı birleştirmek. (Örnek: 2025'te hala kullanılan bir bankacılık ana sistemini (mainframe) yeni mikroservislerle nasıl konuştururuz?)
- Open Host Service (Açık Sunucu Hizmeti): Kendi API'nı yayınla.
- Published Language (Yayınlanmış Dil): Herkesin anlayacağı ortak bir format (JSON Schema, Avro vb.).
- 3.3. Core, Supporting ve Generic Subdomainler: Nereye odaklanmalıyız?
- Core Domain: Bizi biz yapan, rekabet avantajı sağlayan alan. (Örnek: Yapay zeka destekli kişiselleştirilmiş seyahat rotası oluşturan bir startup için rota optimizasyon algoritması).
- Supporting Subdomain: Core Domain'i destekleyen ama rekabet avantajı olmayan işler.
- Generic Subdomain: "Hazır alabileceğimiz" çözümler (Örn: Kimlik doğrulama, e-posta gönderimi).
Kısım 2: Taktiksel Tasarım - Kodun İçindeki Güzellik
Bölüm 4: Java 21 ile Tanışma: Modern Java'nın Gücü
- 4.1. Neden Java 21? Sanal Thread'ler (Virtual Threads), Record'lar ve Pattern Matching'in DDD için anlamı.
- 4.2. Geliştirme Ortamının Kurulumu: JDK 21, Spring Boot 3.x ve Maven/Gradle.
- 4.3. Merhaba DDD: İlk Spring Boot Projemiz.
Bölüm 5: DDD'nin Yapı Taşları (Building Blocks)
- 5.1. Entity (Varlık): Kimliği olan nesneler.
- Örnek: Bir Order (Sipariş) nesnesi. orderId onun kimliğidir. Durumu (sipariş alındı, kargolandı vb.) değişebilir ama kimliği asla.
- Java 21 ile Kodlama: Record'lar Entity olmak için uygun mu? Nerede ve nasıl kullanılır?
// OrderId bir Value Object
// Order ise bir Entity
public class Order {
private final OrderId id;
private OrderStatus status;
private List<OrderItem> items;
private CustomerId customerId;
// Constructor ve diğer metodlar...
public void ship() {
if (this.status == OrderStatus.PAID) {
this.status = OrderStatus.SHIPPED;
// Kargo ile ilgili bir domain event tetiklenir.
}
}
}
- 5.2. Value Object (Değer Nesnesi): Değiştirilemez (immutable) ve kimliği olmayan nesneler.
- Örnek: Address (Adres) veya Money (Para). İki tane "100 TL" nesnesi arasında fark yoktur, ikisi de aynı değeri temsil eder.
- Java 21 record ile Mükemmel Uyum:
// Para birimi ve miktarını bir arada tutan, değiştirilemez bir Değer Nesnesi.
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
public Money add(Money other) {
// Para birimlerinin aynı olduğu kontrol edilmeli.
return new Money(this.amount.add(other.amount), this.currency);
}
}
- 5.3. Aggregate (Küme): İş kurallarının ve tutarlılığın kalesi.
- Aggregate Root Nedir? Kümenin dış dünya ile tek iletişim noktası.
- Kural: Dışarıdan sadece Aggregate Root'a erişilir.
- Örnek: Order Aggregate'i. OrderItem'lar (Sipariş Kalemleri) Order olmadan var olamaz. Bir siparişe ürün eklemek için order.addItem() metodu çağrılır, doğrudan order.getItems().add() yapılmaz. Bu, "toplam sipariş tutarı 20.000 TL'yi geçemez" gibi kuralları korumamızı sağlar.
- 5.4. Repository (Depo): Aggregate'leri kalıcı hale getirme ve geri getirme sanatı.
- Veritabanı detaylarından soyutlama.
- findByOrderId, save gibi metodlar.
- Spring Data JPA ile Implementasyon:
// Sadece Order Aggregate'i için bir Repository.
// OrderItem için ayrı bir Repository olmaz!
@Repository
public interface OrderRepository extends JpaRepository<Order, OrderId> {
Optional<Order> findById(OrderId orderId);
void save(Order order);
}
- 5.5. Factory (Fabrika): Karmaşık nesne oluşturma mantığını gizlemek.
- 5.6. Service (Servis): Hiçbir Entity veya Value Object'e ait olmayan domain operasyonları.
Bölüm 6: Uygulama Mimarisi: Katmanları Doğru Ayarlamak
- 6.1. Klasik N-Tier Mimarinin Ötesi: Hexagonal Architecture (Ports & Adapters)
- 6.2. Katmanlar:
- Domain Layer: En içteki kalp. İş kuralları, Entity'ler, Aggregate'ler burada yaşar.
- Application Layer: Kullanıcı senaryolarını (use cases) yönetir. Gelen isteği alır, ilgili Aggregate'i Repository'den bulur ve üzerinde işlem yapar.
- Infrastructure Layer: Dış dünya ile konuşan katman. Veritabanı, REST API'ler, mesajlaşma kuyrukları.
- 6.3. Spring Boot ile Katmanlı Mimari Proje Yapısı:
Kısım 3: İleri Seviye DDD ve Modern Desenler
Bölüm 7: Domain Events (Alan Olayları): Sistemleri Ayrıştırmak
- 7.1. Olay Nedir? Geçmişte olan ve değiştirilemeyen bir gerçeklik. "Sipariş Kargolandı", "Kullanıcı Kayıt Oldu".
- 7.2. Neden Önemli? Bounded Context'ler arası gevşek bağlı (loosely coupled) iletişim.
- 7.3. 2025 Örneği: Bir kullanıcı, otonom aracını şarj istasyonuna getirdiğinde (VehicleArrivedAtStationEvent), faturalandırma (Billing) context'i bu olayı dinleyerek yeni bir fatura taslağı oluşturabilir.
- 7.4. Spring Boot ile Domain Event'leri Yayınlama ve Dinleme: @ApplicationEventPublisher ve @EventListener.
Bölüm 8: CQRS (Command Query Responsibility Segregation)
- 8.1. Yazma ve Okuma Modellerini Ayırmak: Performans ve ölçeklenebilirlik.
- 8.2. Command (Komut): Sistemin durumunu değiştiren operasyonlar (CreateOrderCommand).
- 8.3. Query (Sorgu): Sistemin durumunu değiştirmeyen, sadece veri okuyan operasyonlar (GetOrderDetailsQuery).
- 8.4. Spring Boot ve Axon Framework/MediatR kütüphaneleri ile basit bir CQRS implementasyonu.
Bölüm 9: Event Sourcing (Olay Kaynağı)
- 9.1. Anlık Durumu Değil, Olayları Saklamak: Bir Aggregate'in tüm yaşam döngüsünü kaydetmek.
- 9.2. Faydaları: Tam denetim izi (audit trail), geçmişteki bir ana dönebilme, hata ayıklama kolaylığı.
- 9.3. 2025 Örneği: Bir "Dijital Sağlık Platformu"nda hastanın verilerinin (Patient Aggregate) nasıl değiştiğini adım adım izlemek. "Teşhis Konuldu", "İlaç Eklendi", "Kan Değeri Güncellendi" gibi olayların saklanması, hem yasal bir zorunluluk hem de gelecekteki yapay zeka analizleri için bir hazinedir.
- 9.4. Event Store Nedir?
Kısım 4: DDD'yi Hayata Geçirmek: Pratik Uygulama
Bölüm 10: Vaka Analizi: "Yeşil Rota" - Akıllı Lojistik Platformu (2025)
- 10.1. Proje Vizyonu: Şehir içi teslimatları optimize etmek için elektrikli drone ve otonom yer araçlarını kullanan bir lojistik platformu. Karbon emisyonunu minimize etme hedefi.
- 10.2. Stratejik Tasarım:
- Bounded Context'leri Belirleme: FleetManagement (Filo Yönetimi), Shipment (Gönderi), Billing (Faturalandırma), Customer (Müşteri).
- Context Map Çizimi.
- 10.3. Taktiksel Tasarım: Shipment Context'ine Derinlemesine Bakış.
- Aggregate'ler: Shipment (Gönderi), Drone.
- Entity'ler, Value Object'ler.
- Domain Event'ler: ShipmentCreated, DroneDispatched, ShipmentDelivered.
- 10.4. Kodlama: Spring Boot, Java 21, JPA ve PostgreSQL kullanarak Shipment servisini adım adım geliştirme.
- 10.5. Test: DDD'de Testin Önemi: Birim testleri, entegrasyon testleri.
Bölüm 11: DDD ve Mikroservisler: Doğal Bir İttifak
- 11.1. Her Bounded Context Bir Mikroservis mi Olmalı? Yaygın bir yanılgı ve doğrusu.
- 11.2. Servisler Arası İletişim: Senkron (REST) ve Asenkron (RabbitMQ/Kafka) İletişim.
- 11.3. Dağıtık Sistemlerde Tutarlılık: Saga Pattern'ine giriş.
Bölüm 12: DDD Felsefesini Sürdürmek
- 12.1. Refactoring Towards Deeper Insight: Modelin zamanla evrimi.
- 12.2. Ekip Kültürü ve DDD: Yazılımcılar ve domain uzmanları nasıl birlikte çalışır?
- 12.3. Geleceğe Bakış: DDD'nin yapay zeka ve veri bilimi projelerindeki yeri.
Ekler
- Ek A: Faydalı Araçlar ve Kütüphaneler
- Ek B: Kavramlar Sözlüğü (Ubiquitous Language)
————————
Ön Söz
Yıl 2025. Yapay zeka artık bir bilim kurgu filmi konusu değil, günlük hayatımızın bir parçası. Otonom araçlar şehirlerimizde yollarını buluyor, sürdürülebilir enerji platformları gezegenimizin geleceğini şekillendiriyor ve iş dünyası, dün hayal bile edilemeyen bir hızla dönüşüyor. Peki, bu fırtınanın tam ortasında duran biz yazılım geliştiriciler, bu karmaşıklığın neresindeyiz?
Elinizde tuttuğunuz bu kitap, basit bir kodlama veya teknoloji kitabı değildir. Bu kitap, 2025'in getirdiği bu baş döndürücü karmaşıklığı bir düşman olarak değil, üzerine zarif ve kalıcı çözümler inşa edebileceğimiz bir zemin olarak görme davetidir.
Çoğumuz o hissi biliriz: Projeye büyük bir hevesle başlarız, ancak aylar geçtikçe kod tabanı bir "çamur yumağına" (Big Ball of Mud) dönüşür. İş birimlerinden gelen her yeni talep, mevcut yapıda bir çatlak daha oluşturur. Yazdığımız kod, hizmet etmesi gereken iş dünyasının gerçekliğinden giderek uzaklaşır. İşte bu kitap, bu kaçınılmaz kadere bir başkaldırıdır.
Bu başkaldırının adı: Domain-Driven Design (DDD).
DDD, bir dizi kuraldan veya katı bir metodolojiden çok daha fazlasıdır; o bir felsefedir. Yazılımın kalbine, onun varoluş sebebi olan iş alanını (domain) yerleştirme felsefesidir. DDD bize, kodu iş hedefleriyle aynı hizada tutmanın, işin dilini konuşan modeller yaratmanın ve en karmaşık problemleri bile yönetilebilir, anlaşılır parçalara ayırmanın yollarını öğretir.
Bu sayfalarda, DDD'nin teorik derinliklerinde kaybolmayacaksınız. Aksine, Java 21'in Sanal Thread'leri (Virtual Threads) ve Record'ları gibi modern güçlerini ve Spring Boot'un sağlam altyapısını birer pusula olarak kullanarak, DDD'nin hem stratejik hem de taktiksel desenlerini adım adım hayata geçireceğiz. Bir "Akıllı Lojistik Platformu" için rota optimizasyonunu veya bir "Dijital Sağlık" uygulamasının veri bütünlüğünü nasıl güvence altına alacağımızı kodlayarak öğreneceğiz.
Bu kitap, kariyerinin başındaki bir yazılımcı için sağlam bir temel, deneyimli bir mimar için ise modern dünyaya uyarlanmış yeni bir bakış açısı sunmak üzere tasarlandı. Amacım, size sadece ne yapmanız gerektiğini değil, daha da önemlisi, neden yaptığınızı anlatmak. Böylece sadece kod yazan değil, aynı zamanda değer üreten, işe yön veren ve yazdığı kodla gurur duyan bir yazılım zanaatkarı olmanıza yardımcı olmak.
Eğer siz de yazdığınız kodun bir anlam taşımasını, karmaşıklığı yönetmeyi ve yazılım geliştirme sanatında bir sonraki seviyeye geçmeyi hedefliyorsanız, doğru yerdesiniz.
Bu heyecan verici yolculuğa çıkmaya hazır mısınız?
Hadi başlayalım.
————————
1.1. Kod ve İş Dünyası Arasındaki Uçurum: Projeler Neden Karmaşıklaşır?
Hiç büyük bir hevesle başladığınız bir işin zamanla içinden çıkılmaz bir hale geldiğini hissettiniz mi? Belki odanızı toplarken "Şu eşyayı şuraya, bunu da buraya koyayım," diye başlarsınız. Ama bir süre sonra bir bakmışsınız, her yer daha da dağılmış ve neyi nereye koyacağınızı bilemez hale gelmişsinizdir. İşte yazılım projeleri de genellikle böyle bir yolculuk izler. Başlangıçta her şey net ve basittir, ancak zamanla bir canavara dönüşebilirler.
Peki, neden? Neden parlak fikirlerle başlayan projeler, aylar sonra hem yazılımcıları hem de iş sahiplerini yoran karmaşık sistemlere dönüşür?
Cevap, genellikle kodun kendisinde değil, kod ile gerçek dünya arasındaki giderek büyüyen uçurumda yatar.
Bu uçurumu yaratan birkaç temel sebep vardır:
1. Farklı Diller Konuşmak: İletişim Kopukluğu
En temel sorun, projenin iki ana kahramanının – iş uzmanları ve yazılım geliştiriciler – genellikle aynı dili konuşmamasıdır.
- İş Uzmanı Ne Der? "Müşterilerimiz için dinamik bir karbon ayak izi hesaplama modülü istiyoruz. 2025 Sürdürülebilirlik Regülasyonları'na göre, her teslimatın lojistik rotasındaki anlık enerji tüketimini ve kaynak türünü (elektrikli araç, biyoyakıt vb.) hesaba katmalı ve müşteriye anlık bildirimle bir 'yeşil puan' sunmalıyız."
- Yazılımcı Ne Anlar (veya Anlamak Zorunda Kalır)? "Tamam, bir shipment tablomuz var. Oraya bir carbon_footprint sütunu eklerim. Rota bilgisi için bir API'ye istek atıp dönen JSON'dan energy_consumption alanını alırım. Bunu bir formülle çarpıp o sütuna yazarım. Sonra da bir notification servisi tetiklerim."
Gördünüz mü? İlk bakışta bir sorun yok gibi. Ama "dinamik hesaplama", "regülasyonlar", "kaynak türü", "yeşil puan" gibi iş dünyasına ait kritik kavramlar, teknik dile çevrilirken anlamını ve derinliğini yitirir. Yazılımcı, bu kavramları kendi anladığı teknik karşılıklara (sütun, JSON alanı, servis) indirger. İşin ruhu kaybolur ve geriye sadece teknik bir uygulama kalır. Proje ilerledikçe bu küçük anlam kaymaları birikir ve dev bir uçuruma dönüşür.
2. Hedeflerin Değişmesi: Hareketli Bir Nişan Tahtası
2025 dünyası, değişimin kural olduğu bir yer. Dün popüler olan bir iş modeli, bugün yerini yenisine bırakabilir.
- Örnek: "Yeşil Rota" adını verdiğimiz akıllı lojistik platformumuzun projesine başladığımızı düşünelim. Başlangıçtaki hedefimiz, sadece elektrikli kamyonetlerin rotasını optimize etmekti.
- Üç Ay Sonra: Hükümet, şehir içinde drone ile teslimata yönelik yeni bir yasa çıkarır. Proje paydaşları hemen heyecanlanır: "Harika! Sistemimize drone'ları da ekleyelim! Ama drone'ların batarya ömrü ve ağırlık kapasitesi kamyonetlerden çok farklı."
- Altı Ay Sonra: Rakip bir firma, teslimatları otonom yeraltı kapsülleriyle yapmaya yönelik bir pilot proje duyurur. Yönetimden yeni bir talep gelir: "Bizim mimarimiz gelecekte yeraltı sistemlerini de destekleyebilecek kadar esnek olmalı!"
Yazılım ekibi, başlangıçta sadece kamyonetler için tasarladığı basit bir modele sürekli yeni ve bambaşka kurallar eklemeye çalışır. Her yeni talep, mevcut kodda bir "yama" olur. Zamanla kod, orijinal amacından o kadar uzaklaşır ki, artık kimse ona dokunmaya cesaret edemez. Proje, bir yama bohçasına dönüşür.
3. "Bunu Şimdilik Böyle Yapalım, Sonra Düzeltiriz": Teknik Borç
Bazen zaman baskısı, bazen de problemin tam anlaşılamaması nedeniyle ekipler "hızlı ve kirli" çözümlere yönelebilir. Bu, tıpkı kredi kartından para çekmeye benzer: O anki sorunu çözer ama gelecekte faiziyle birlikte geri ödemeniz gereken bir borç yaratır. Buna Teknik Borç (Technical Debt) diyoruz.
- Örnek: Bir geliştirici, müşteri adreslerini doğrulamak için karmaşık bir sistem kurmak yerine, "şimdilik" adresleri tek bir metin alanına (String address) yazmaya karar verir.
- Anlık Kazanım: 1-2 gün kazanç.
- Gelecekteki Maliyet: Bir yıl sonra, "Müşterileri şehirlere göre gruplayıp onlara özel kampanyalar yapalım" talebi geldiğinde, o tek metin alanının içinden şehri, ilçeyi, posta kodunu ayıklamak imkansız hale gelir. Bütün adres verisini baştan temizlemek ve sistemi yeniden yazmak için haftalar harcanır.
Bu üç ana neden – iletişim kopukluğu, değişen hedefler ve biriken teknik borç – bir araya geldiğinde, projeler kaçınılmaz olarak karmaşıklaşır. Kod, hizmet etmesi gereken iş dünyasından kopar ve kendi içinde, anlaşılması ve değiştirilmesi zor bir labirente dönüşür.
İşte Domain-Driven Design (DDD), tam olarak bu uçurumun üzerine bir köprü kurma vaadiyle ortaya çıkar. Bize, işin karmaşıklığını bir düşman olarak görmeyi değil, onu kucaklayıp yazılımın tam merkezine yerleştirmeyi öğretir.
————————
1.2. 2025'in Gerçekliği: Hızla Değişen İş İhtiyaçları
Bir önceki bölümde projelerin neden karmaşıklaştığını konuştuk. Şimdi ise madalyonun diğer yüzünü çevirelim ve bu karmaşıklığı körükleyen dış dünyaya, yani 2025'in iş ve teknoloji ortamına bakalım. Eğer 20 yıl önce bir yazılım geliştirseydiniz, iş kuralları muhtemelen yıllarca değişmeden kalırdı. Ama bugün? Bugün iş dünyası durağan bir göl değil, adeta azgın bir nehir gibi.
Bu nehrin akıntısını hızlandıran birkaç güçlü etken var:
- Her Şeyin Akıllanması (IoT ve Yapay Zeka): Artık sadece bilgisayarlar ve telefonlar internete bağlı değil. Akıllı saatimizden kahve makinemize, fabrikadaki bir robottan kiraladığımız bisiklete kadar her şey veri üretiyor ve veri tüketiyor. Yapay zeka (AI), bu devasa veriyi analiz ederek anlık kararlar alıyor, kişiselleştirilmiş deneyimler sunuyor ve daha önce insan müdahalesi gerektiren süreçleri otomatikleştiriyor. Bu durum, yazılımların artık sadece statik verileri işleyen sistemler olmaktan çıkıp, yaşayan, öğrenen ve adapte olan organizmalara dönüşmesini zorunlu kılıyor.
- Kişiselleştirme Beklentisi: "Tek beden herkese uyar" anlayışı artık tarihe karıştı. Tüketiciler, kullandıkları her hizmetin kendilerine özel olmasını bekliyor. İzlediğimiz filmlerden dinlediğimiz müziklere, alışveriş önerilerinden sağlık tavsiyelerine kadar her şey kişisel verilerimize göre şekilleniyor. Bu, yazılımların arkasındaki iş mantığının çok daha karmaşık ve dinamik olması gerektiği anlamına geliyor.
- Sürdürülebilirlik ve Regülasyonlar: İklim değişikliği ve sosyal sorumluluk bilinci, şirketleri iş yapış şekillerini değiştirmeye zorluyor. Karbon ayak izini azaltma, etik kaynak kullanımı ve şeffaflık gibi konular artık birer "tercih" değil, yasal birer "zorunluluk". Bu yeni kurallar, tedarik zincirinden üretime, lojistikten raporlamaya kadar tüm iş süreçlerini ve dolayısıyla bu süreçleri yöneten yazılımları doğrudan etkiliyor.
Bu gerçekliği daha somut hale getirmek için gelin hep birlikte bir örnek üzerinden düşünelim.
Örnek: 2025 Paris Olimpiyatları İçin Geliştirilen Anlık Biletleme Sisteminin Zorlukları
Hayal edin, 2025 Paris Olimpiyatları'nın resmi biletleme sistemini geliştiren ekibin bir parçasısınız. Göreviniz, milyonlarca insanın aynı anda kullanacağı, adil, güvenli ve modern bir sistem yaratmak. Kulağa heyecan verici geliyor, değil mi? Ama işin içine girince 2025'in gerçekleri yüzünüze bir bir çarpmaya başlıyor:
- Dinamik ve Adil Fiyatlandırma: Artık bilet fiyatları sabit değil. Sistem, bir etkinliğe olan anlık talebe, koltuğun konumuna, hatta hava durumuna göre fiyatları dinamik olarak ayarlamalı. Amaç karaborsayı önlemek ve biletlerin gerçek sporseverlere ulaşmasını sağlamak. Peki, "adil fiyat" nedir? Bunu belirleyen iş kuralları o kadar karmaşıktır ki, basit bir if-else bloğu ile çözülemez. Belki de bir yapay zeka modeli, anlık arz-talep dengesini analiz ederek fiyatı belirlemelidir.
- Kişiselleştirilmiş "Olimpiyat Deneyimi": Sistem sadece bilet satmamalı. Kullanıcının daha önce ilgi gösterdiği spor dallarına (örneğin, atletizm ve yüzme) göre ona özel paketler önermeli. Bir atletizm bileti alan kişiye, stadyuma en yakın metro istasyonundaki yoğunluğu anlık olarak bildirip alternatif bir rota çizmeli. Hatta kullanıcının favori sporcusunun yarışacağı diğer etkinlikleri de ajandasına eklemeyi teklif etmeli. Bu, "Biletleme" sisteminin "Ulaşım", "Etkinlik Takvimi" ve "Kullanıcı Profili" gibi bambaşka sistemlerle anlık ve akıllı bir şekilde konuşması demektir.
- Sürdürülebilirlik Puanı: Paris 2025, "en yeşil olimpiyat" olma hedefinde. Bu nedenle, biletleme sisteminin de bu hedefe hizmet etmesi isteniyor. Kullanıcı bir bilet aldığında, seçtiği ulaşım yöntemine (toplu taşıma, bisiklet, elektrikli araç) göre bir "yeşil puan" kazanabilir. Bu puanlar, onlara olimpiyat köyündeki mağazalarda indirim sağlayabilir. Bu basit görünen özellik bile, "Bilet" kavramının "Ulaşım" ve "Sadakat Programı" gibi bambaşka iş alanlarıyla (domain) iç içe geçmesini gerektirir.
- Güvenlik ve Kimlik Doğrulama: Biletlerin sahtesinin üretilmesini veya karaborsada satılmasını engellemek için her bilet, sahibinin dijital kimliğine (belki de biyometrik verisine) bağlanmalıdır. Stadyuma girişte biletin QR kodu okutulduğunda, sistem sadece biletin geçerliliğini değil, aynı zamanda bileti gösteren kişinin doğru kişi olup olmadığını da saniyeler içinde doğrulamalıdır.
Bu örnekte gördüğünüz gibi, "bir bilet satıp koltuk rezerve etmek" gibi basit bir fikir, 2025'in gerçekleriyle birleştiğinde inanılmaz derecede karmaşık bir iş ihtiyaçları yumağına dönüşür. İşte bu yumak, geleneksel yazılım geliştirme yaklaşımlarının yetersiz kaldığı yerdir. Eğer bu sistemi tek bir devasa veritabanı ve yüzlerce birbiriyle dolaşmış kod dosyasıyla yapmaya çalışırsak, projenin başarısızlığı en başından garantilenmiş olur.
İhtiyacımız olan şey, bu karmaşıklığı yönetmemizi sağlayacak, işin doğasını anlayan ve onu kodun kalbine yerleştiren bir yaklaşımdır. İşte bir sonraki bölümde tanışacağımız Domain-Driven Design, bize tam olarak bunu vaat ediyor.
————————
1.3. Domain-Driven Design (DDD) Nedir? Teknik Bir Jargondan Fazlası
Önceki bölümlerde, yazılım projelerini bir canavara dönüştüren karmaşıklıktan ve 2025 dünyasının bu karmaşıklığı nasıl körüklediğinden bahsettik. Kod ile iş dünyası arasında giderek büyüyen bir uçurum olduğunu gördük.
Peki, bu uçurumun üzerine köprüyü nasıl kuracağız? İşte bu sorunun cevabı Domain-Driven Design (DDD), yani Alan Adı Odaklı Tasarım'dır.
İlk duyduğunuzda kulağa bir başka sıkıcı teknik jargon gibi gelebilir. "Design Patterns", "SOLID Prensipleri", "Microservices" gibi diğer havalı terimlerin arasına eklenen yeni bir kelime... Ama durun. DDD, sadece teknik bir reçete veya bir dizi kural değildir. Ondan çok daha fazlasıdır.
DDD, bir felsefedir. Bir zihniyet değişimidir.
En basit tanımıyla DDD, karmaşık iş problemlerini çözmek için yazılımın merkezine işin kendisini, yani "alan adını" (domain) koyan bir yazılım geliştirme yaklaşımıdır.
Bu ne anlama geliyor? Gelin bir benzetme üzerinden gidelim.
Mimar ve Mühendis Benzetmesi
Hayal edin ki size bir gökdelen inşa etme görevi verildi.
- Geleneksel Yaklaşım (Teknoloji Odaklı): İşe en iyi betonu, en sağlam çeliği ve en hızlı vinçleri seçerek başlarsınız. "Harika, elimde çok güçlü malzemeler var!" dersiniz. Sonra bu malzemeleri bir şekilde bir araya getirerek bir yapı oluşturmaya çalışırsınız. Ancak bu yapının içinde kimin yaşayacağını, ofislerin nasıl bir yerleşime sahip olacağını veya binanın şehrin silüetine uyup uymayacağını ikinci plana atarsınız. Sonuç ne olur? Teknik olarak belki sağlamdır ama kullanışsız, ruhsuz ve amacına hizmet etmeyen bir beton yığını. Projelerimizin çoğu, en yeni framework'ü veya en popüler veritabanını seçerek başlar ve aynı bu hataya düşer.
- DDD Yaklaşımı (Domain Odaklı): İşe malzemeleri seçerek değil, sorular sorarak başlarsınız. Bu gökdeleni kim kullanacak? İçinde bir finans merkezi mi olacak, yoksa bir sanat galerisi mi? İnsanlar içinde nasıl hareket edecek? Binanın amacı ne? Bu soruların cevaplarını, yani binanın amacını (domain) anladıktan sonra, bu amaca en uygun mimari planı çizersiniz. Ancak bu plan bittikten sonra, "Bu planı hayata geçirmek için en uygun beton, çelik ve vinç hangisidir?" diye sorarsınız.
Gördüğünüz gibi, DDD odak noktasını "nasıl" yapacağımızdan (teknolojiden), "ne" yapacağımıza ve daha da önemlisi "neden" yapacağımıza (işin kendisine) çevirir.
DDD Sadece Bir "Şey" Değildir, Bir "Yapma Biçimi"dir
DDD'nin teknik bir jargondan fazlası olmasının sebepleri şunlardır:
- Önce İş, Sonra Kod: DDD, kodun bir amaç değil, bir araç olduğunu söyler. Asıl amaç, iş dünyasının karmaşıklığını anlamak ve bu anlayışı koda yansıtmaktır. Yazdığınız her bir kod satırının, "yeşil puan", "adil fiyat" veya "karbon ayak izi" gibi gerçek bir iş kavramına karşılık gelmesi gerekir.
- Ortak Dil Vurgusu: DDD'nin kalbinde Ubiquitous Language (Evrensel Dil) adını verdiğimiz bir fikir yatar. Bu, proje üzerinde çalışan yazılımcıların, iş analistlerinin, pazarlama uzmanlarının ve yöneticilerin aynı terminolojiyi kullanması demektir. Eğer iş uzmanı "Müşteri Segmentasyonu" diyorsa, kodun içindeki sınıfın adı da MusteriSegmentasyonu (veya İngilizce CustomerSegmentation) olmalıdır. UserGroup veya ClientType gibi "yaklaşık" çeviriler olmaz. Bu ortak dil, iletişimdeki anlam kaymalarını ortadan kaldırır ve o meşhur uçurumun oluşmasını engeller.
- Model Odaklılık: DDD, soyut iş kurallarını somut yazılım modellerine dönüştürme sanatıdır. Bu modeller, veritabanı tablolarının bir kopyası değildir. Onlar, işin nasıl çalıştığını, kurallarını, süreçlerini ve ilişkilerini temsil eden yaşayan, nefes alan yapılardır. Bir "Olimpiyat Bileti" modeli, sadece bir ID ve koltuk numarası içermez; aynı zamanda "transfer edilebilir mi?", "iade edilebilir mi?", "sahibi kim?" gibi iş kurallarını da kendi içinde barındırır.
Kısacası, DDD size "şu kütüphaneyi kullan" veya "kodunu tam olarak böyle yaz" demez. Bunun yerine size bir düşünce yapısı sunar:
"Dur ve önce işi anla. İşin uzmanlarıyla aynı dili konuş. İşin karmaşıklığını yansıtan bir model oluştur. Ancak ondan sonra bu modeli koda dönüştürmek için en iyi teknik araçları seç."
Bu felsefeyi benimsediğinizde, artık sadece kod yazan biri olmaktan çıkıp, karmaşık problemleri çözen bir problem çözücü ve bir yazılım zanaatkarı haline gelirsiniz. Kitabın geri kalanında, bu felsefeyi hayata geçirmemizi sağlayacak olan hem büyük resmi görmemizi sağlayan Stratejik Tasarım'ı hem de kodu şekillendiren Taktiksel Tasarım'ın somut araçlarını Java ve Spring Boot ile adım adım öğreneceğiz.
————————
1.4. DDD'nin İki Temel Direği: Stratejik ve Taktiksel Tasarım
Domain-Driven Design felsefesini bir savaş kazanma sanatı olarak düşünelim. Bir savaşı kazanmak için iki şeye ihtiyacınız vardır: İyi bir strateji ve etkili taktikler.
- Strateji, savaşın büyük resmidir. Hangi cephelerde savaşacağınızı, hangi tepeleri tutmanız gerektiğini, ordunuzu nasıl böleceğinizi ve düşmanın güçlü ve zayıf yönlerini nerede kullanacağınızı belirler. Stratejiniz olmadan, en iyi askerleriniz bile anlamsız ve koordinasyonsuz çatışmalarda gücünü kaybeder.
- Taktikler ise savaş meydanındaki uygulamalardır. Askerlerinizi nasıl konumlandıracağınız, hangi silahları kullanacakları, nasıl manevra yapacakları gibi detayları içerir. İyi taktikler olmadan, en parlak stratejiler bile kağıt üzerinde kalmaya mahkumdur.
DDD de tam olarak bu iki seviyede çalışır ve bu seviyelere Stratejik Tasarım ve Taktiksel Tasarım adını verir. Bu iki direk, DDD'nin temelini oluşturur ve birbirleri olmadan eksik kalırlar.
1. Stratejik Tasarım: Büyük Resmi Görmek (Makro Seviye)
Stratejik Tasarım, projenin "savaş haritasını" çizmektir. Kodun detaylarına dalmadan önce, sistemin genel yapısını, sınırlarını ve parçaları arasındaki ilişkileri anlamaya odaklanır. Amacı, en başta doğru savaşı seçtiğimizden ve enerjimizi doğru yere harcadığımızdan emin olmaktır.
Stratejik Tasarım şu gibi sorulara cevap arar:
- Problemimiz aslında tek bir büyük problem mi, yoksa birbiriyle ilişkili daha küçük problemlerden mi oluşuyor?
- Örnek: Bir e-ticaret sitesi, tek bir devasa "e-ticaret sistemi" midir? Yoksa içinde "Ürün Kataloğu", "Stok Yönetimi", "Sipariş" ve "Müşteri Memnuniyeti" gibi farklı "krallıklar" barındıran bir kıta mıdır? Stratejik tasarım, bu krallıkların sınırlarını çizer. DDD'de bu sınırlara Bounded Context (Sınırlı Bağlam) adını veririz.
- Hangi problem bizim için en kritik olanı? Hangisi bizi rakiplerimizden ayırıyor?
- Örnek: 2025 model bir online bankacılık uygulaması için, para transferi yapmak artık sıradan bir iştir (Generic). Ancak yapay zeka destekli, kişiye özel bir "finansal sağlık asistanı" sunmak, bankayı eşsiz kılacak olan asıl rekabet avantajıdır (Core Domain). Stratejik tasarım, enerjimizi bu "asistanı" mükemmel hale getirmeye harcamamızı, para transferi gibi standart işler için ise belki de hazır çözümler kullanmamızı söyler.
- Bu farklı parçalar (krallıklar) birbiriyle nasıl konuşacak?
- Örnek: "Sipariş" sistemi, bir ürünün stokta olup olmadığını "Stok Yönetimi" sistemine nasıl soracak? Birbirlerinin dilinden anlıyorlar mı? Yoksa araya bir "tercüman" mı koymalıyız? Bu ilişki haritasına da Context Map (Bağlam Haritası) denir.
Kısacası Stratejik Tasarım, projenin mimari vizyonunu oluşturur. Nereye odaklanacağımızı, karmaşıklığı nasıl böleceğimizi ve sistemin genel sağlığını nasıl koruyacağımızı belirler.
2. Taktiksel Tasarım: Mükemmel Parçalar İnşa Etmek (Mikro Seviye)
Stratejik olarak sınırlarımızı ve önceliklerimizi belirledikten sonra, sıra o sınırlar içindeki krallıkları, yani her bir Bounded Context'i inşa etmeye gelir. İşte burada Taktiksel Tasarım devreye girer.
Taktiksel Tasarım, kod seviyesindeki "en iyi uygulama" setidir. Amacı, yazdığımız kodun esnek, anlaşılır, test edilebilir ve en önemlisi, iş kurallarını açıkça yansıtan bir yapıda olmasını sağlamaktır.
Taktiksel Tasarım şu gibi sorulara cevap arar:
- İş kurallarını ve verileri kod içinde nasıl modelleyeceğiz?
- Bunun için Entity (Varlık), Value Object (Değer Nesnesi) gibi yapı taşlarını kullanırız. Örneğin, bir Musteri kimliği olduğu için bir Entity iken, "50 TL" gibi bir Para birimi sadece değerini temsil ettiği için bir Value Object'tir.
- Birbiriyle ilişkili nesnelerin tutarlılığını nasıl garantileyeceğiz?
- Bunun için Aggregate (Küme) kavramını kullanırız. Bir Siparis ve ona ait SiparisKalemleri bir bütündür. Siparişe bir kalem eklediğinizde, siparişin toplam tutarının da güncellenmesi gerekir. Bu tutarlılığı Aggregate sağlar.
- Bu nesneleri nasıl oluşturup saklayacağız?
- Nesne oluşturma mantığını Factory (Fabrika) ile, bu nesneleri veritabanına kaydetme/okuma işlerini ise Repository (Depo) ile yaparız.
Kısacası Taktiksel Tasarım, bize kaliteli ve anlamlı kod yazmanın somut araçlarını verir. Bu araçlar, Stratejik Tasarım sırasında çizdiğimiz haritadaki her bir bölgeyi en sağlam ve kullanışlı şekilde inşa etmemizi sağlar.
Sonuç: Birlikte Mükemmel Bir Bütün
- Sadece Stratejik Tasarım yaparsanız, elinizde güzel çizilmiş ama içi boş haritalar olur.
- Sadece Taktiksel Tasarım yaparsanız, elinizde teknik olarak mükemmel ama birbiriyle uyumsuz, yanlış problemi çözen ve büyük resmi göremeyen kod parçacıkları olur.
Gerçek başarı, bu iki direği birlikte kullanmaktan geçer. Önce Stratejik Tasarım ile "Ne yapıyoruz ve neden?" sorusunu cevaplarız. Ardından Taktiksel Tasarım ile "Bunu en iyi nasıl yaparız?" sorusuna odaklanırız. Bu kitap boyunca, bu iki direk arasında sürekli gidip gelerek DDD felsefesini bütüncül bir şekilde öğreneceğiz.
————————
1.5. Bu Kitap Size Ne Vaat Ediyor? Sıfırdan Uzmanlığa Giden Yol Haritanız
Şu ana kadar önemli temel kavramlardan bahsettik: Yazılım dünyasındaki karmaşıklık, hızla değişen iş ihtiyaçları ve tüm bunlara bir çözüm felsefesi olarak Domain-Driven Design. Belki kafanızda "Bounded Context", "Aggregate", "Ubiquitous Language" gibi yeni terimler uçuşmaya başladı ve tüm bunların nasıl bir araya geleceğini merak ediyorsunuz.
İşte bu bölüm, tam olarak o merakı gidermek için var. Bu kitap, elinizdeki bir pusula, bir rehberdir. Sizi teorinin sisli sularında yalnız bırakmayı değil, sıfır noktasından alıp uzmanlık seviyesine kadar adım adım, güvenli bir şekilde götürmeyi vaat ediyor.
Peki, bu yolculukta sizi neler bekliyor? İşte yol haritanız:
1. Sadece "Nasıl" Değil, "Neden" Diyerek Başlayacaksınız
Birçok teknik kitap, doğrudan kodla başlar. Bu kitap ise bir adım geri çekilip "Neden?" sorusunu sorarak başlıyor.
- Bir problemi çözmeye kod yazarak değil, onu anlayarak başlayacaksınız.
- DDD'nin arkasındaki felsefeyi, yani işin kalbini yazılımın merkezine koyma düşüncesini özümseyeceksiniz.
- Teknik jargona boğulmadan önce, Stratejik Tasarım'ın gücüyle büyük resmi görmeyi, problemleri doğru parçalara ayırmayı öğreneceksiniz.
2. Teoriyi Kodla Hayata Geçireceksiniz
Bu kitapta felsefe ve pratik, bir madalyonun iki yüzü gibidir. Öğrendiğiniz her teorik kavramı, modern ve güçlü araçlarla koda dökeceksiniz:
- Java 21 ve Spring Boot 3'ün en güncel özelliklerini kullanarak temiz, anlaşılır ve etkili kod yazacağız. Java'nın yeni record yapılarını Değer Nesneleri (Value Objects) için nasıl mükemmel bir şekilde kullanacağımızı veya Sanal Thread'lerin (Virtual Threads) sistemlerimizi nasıl daha verimli hale getirebileceğini göreceksiniz.
- DDD'nin taktiksel yapı taşlarını (Entity, Value Object, Aggregate, Repository, Factory) tek tek, somut kod örnekleriyle inşa edeceğiz. Sadece ne olduklarını değil, neden var olduklarını ve bir problemi nasıl çözdüklerini anlayacaksınız.
3. Gerçek Hayat Senaryolarıyla Tecrübe Kazanacaksınız
Kitabımız, sıkıcı ve soyut "Foo/Bar" örneklerinden uzak duruyor. Bunun yerine, 2025'in gerçekçi ve heyecan verici problemleriyle yüzleşeceksiniz:
- "Yeşil Rota" adını verdiğimiz akıllı lojistik platformu gibi kapsamlı bir vaka analizi üzerinden ilerleyeceğiz. Bu projeyi geliştirirken, bir fikrin stratejik analizden başlayıp çalışan bir mikroservise nasıl dönüştüğüne ilk elden tanıklık edeceksiniz.
- Paris Olimpiyatları biletleme sisteminden sürdürülebilir enerji ticaretine kadar, öğrendiğiniz her tekniğin günümüz dünyasında nasıl bir karşılığı olduğunu göreceksiniz.
4. Başlangıç Seviyesinden İleri Düzey Konulara Uzanacaksınız
Yolculuğumuz temel kavramlarla sınırlı kalmayacak. Kendinize güvendikçe, sizi modern yazılım mimarilerinin zirvesine taşıyacak ileri seviye konulara geçiş yapacağız:
- Sistemlerinizi birbirine gevşekçe bağlamanın ve daha esnek hale getirmenin anahtarı olan Domain Events (Alan Olayları) kavramını öğreneceksiniz.
- Okuma ve yazma operasyonlarını ayırarak inanılmaz bir performans ve ölçeklenebilirlik sağlayan CQRS (Command Query Responsibility Segregation) desenini uygulayacaksınız.
- Bir sistemin geçmişindeki her adımı kaydederek tam bir denetim ve analiz imkanı sunan Event Sourcing gibi güçlü mimari desenlere giriş yapacaksınız.
Bu Kitabın Sonunda Ne Olacaksınız?
Bu kitabın son sayfasına geldiğinizde, sadece yeni bir teknoloji öğrenmiş olmayacaksınız. Bir düşünce yapısını kazanmış olacaksınız.
- Karmaşıklıktan korkan değil, onu yönetebilen bir geliştirici olacaksınız.
- Sadece kendisine verilen görevi kodlayan değil, işin hedeflerini anlayan ve bu hedeflere yön veren bir problem çözücü olacaksınız.
- Daha sürdürülebilir, daha esnek ve daha anlamlı yazılımlar üreten bir yazılım zanaatkarı haline geleceksiniz.
Bu yol haritası, sizin için bir sözdür. Eğer bu yolculuğa çıkmaya kararlıysanız, sayfayı çevirin ve yazılım geliştirme sanatına bakış açınızı sonsuza dek değiştirecek bu maceraya ilk adımı atın.
————————
Bölüm 2: Her Şeyin Başladığı Yer: Domain (Alan Adı)
2.1. "Domain" Sadece Bir Kelime Değildir: İş Probleminizin Evreni
Domain-Driven Design'daki "Domain" kelimesini ilk duyduğunuzda aklınıza ne geliyor? Belki bir internet sitesinin ".com" ile biten adresi? Veya bir kralın hükmettiği topraklar? Bu çağrışımlar aslında o kadar da yanlış sayılmaz, çünkü hepsi bir sınır ve uzmanlık alanı fikrini içerir.
DDD bağlamında Domain (Alan Adı), en basit tanımıyla, bir yazılım geliştirerek çözmeye çalıştığınız problemin ait olduğu iş dünyası veya konu alanıdır.
Bu, hala biraz soyut geliyor olabilir. Gelin, daha somut bir benzetme yapalım.
Mutfak Benzetmesi
Evinizin mutfağını düşünün. Mutfak, sadece dört duvar ve bir kapıdan ibaret değildir. O, kendi kuralları, kendi dili ve kendi süreçleri olan küçük bir evrendir.
- Nesneleri Vardır: Ocak, fırın, buzdolabı, bıçak, kesme tahtası.
- Kuralları Vardır: Çiğ tavuğun kesildiği tahtada salata doğranmaz (hijyen kuralı). Kekin kabarması için fırının kapağı ilk 20 dakika açılmaz (pişirme kuralı). Bıçaklar bulaşık makinesinde yıkanmaz, körelir (bakım kuralı).
- Süreçleri Vardır: Menemen yapmak için önce soğan ve biber doğranır, sonra domates eklenir, en son yumurta kırılır. Bu adımların sırası önemlidir.
- Dili Vardır: "Sotelemek", "blanş etmek", "karamelize etmek", "benmari usulü eritmek" gibi terimler, bir aşçı için çok özel anlamlar taşır. Bir aşçıya "eti biraz ısıt" demezsiniz, "eti mühürle" dersiniz.
İşte bu mutfak, bir Domain'dir. Bir "Yemek Pişirme Domain'i". Eğer bu mutfak için bir "Akıllı Mutfak Asistanı" yazılımı geliştiriyor olsaydınız, çözmeniz gereken problem alanı, yani domain'iniz bu mutfağın ta kendisi olurdu. Yazılımınız, "sotelemek" ile "haşlamak" arasındaki farkı bilmek, fırının kapağının ne zaman açılmaması gerektiğini anlamak ve bir şefin dilini konuşmak zorunda kalırdı.
Domain, Kodunuzun "Neden" Var Olduğudur
Yazılım dünyasında "Domain", çözmeye çalıştığımız problemin evrenidir. Bu evren, bir dizi veritabanı tablosundan veya bir grup API'dan çok daha fazlasıdır. İçinde yaşayan, kendine özgü kuralları, süreçleri ve dili olan bir dünyadır.
Gelin 2025'ten birkaç örnekle bunu daha da netleştirelim:
- Havayolu Şirketi: Bu şirketin domain'i "Hava Taşımacılığı"dır. Bu evrenin içinde "uçuş planlama", "bilet satışı", "yolcu bagaj yönetimi", "sadakat programı (mil hesaplama)" ve "mürettebat zaman çizelgesi" gibi konular vardır. "Bir uçuşa kapasitesinden fazla bilet satılabilir mi (overbooking)?", "Aktarmalı bir uçuşta bagajlar nasıl transfer edilir?" gibi yüzlerce karmaşık kural bu domain'in bir parçasıdır.
- Bir Hastane: Domain'i "Sağlık Hizmetleri"dir. Bu evrende "hasta kayıt", "randevu yönetimi", "elektronik sağlık kayıtları (EHR)", "laboratuvar sonuçları" ve "reçeteleme" gibi konular bulunur. "Bir ilacın başka bir ilaçla birlikte verilip verilemeyeceği (ilaç etkileşimi)" veya "bir hastanın teşhis bilgilerinin gizliliği (mahremiyet kuralları)" bu domain'in en kritik kurallarındandır.
- "Yeşil Rota" Akıllı Lojistik Platformu: Kitabımızda sık sık kullanacağımız bu örneğin domain'i "Akıllı Şehir Lojistiği"dir. Bu evrende "paket", "drone", "otonom yer aracı", "şarj istasyonu", "teslimat rotası optimizasyonu" ve "karbon ayak izi hesaplama" gibi kavramlar yaşar. "Bir drone'un maksimum uçuş menzili nedir?", "Hangi paket hangi araca yüklenmeli?", "Bir teslimatın tahmini varış süresi trafik durumuna göre nasıl anlık olarak güncellenir?" gibi sorular bu domain'in kalbindedir.
Neden Bu Kadar Önemli?
Çünkü etkili bir yazılım geliştirmek, teknolojiyi iyi bilmekten önce, domain'i derinlemesine anlamayı gerektirir. Eğer bir "Hava Taşımacılığı" domain'i için yazılım geliştiriyorsanız ama "overbooking" kavramının iş için ne anlama geldiğini bilmiyorsanız, yazdığınız kod en iyi ihtimalle eksik, en kötü ihtimalle hatalı olacaktır.
Domain-Driven Design, bize tam olarak bunu söyler: Koda bir satır bile başlamadan önce, çözmeye çalıştığınız problemin evrenine, yani domain'e odaklanın. Onun kurallarını, dilini ve karmaşıklığını anlamaya çalışın. Çünkü yazdığınız kod, ancak ve ancak hizmet ettiği bu evreni doğru bir şekilde yansıttığı zaman değerli ve kalıcı olur.
————————
2.2. Domain Uzmanları (Domain Experts): Projenizin Gizli Kahramanları
Bir önceki bölümde mutfak benzetmesini kullanmıştık. Eğer bir "Akıllı Mutfak Asistanı" yazılımı geliştiriyorsanız, o mutfak sizin "domain"inizdir. Peki, "kekin kabarması için fırın kapağının ilk 20 dakika açılmaması gerektiği" kuralını size kim söyler? Veya "sotelemek" ile "mühürlemek" arasındaki o ince farkı kim anlatır?
Elbette, o mutfakta yıllarını geçirmiş, her bir malzemenin ruhunu, her bir tekniğin sırrını bilen usta bir aşçı.
İşte DDD dünyasında, bu usta aşçıya Domain Expert (Domain Uzmanları) deriz.
Domain Uzmanı, üzerinde çalıştığınız iş alanında (domain) derin bilgi ve tecrübeye sahip olan kişidir. Bu kişi bir yazılımcı olmak zorunda değildir; hatta çoğu zaman değildir. Onlar, geliştirdiğimiz yazılımın hizmet edeceği işi fiilen yapan, o işin tüm inceliklerini, zorluklarını ve gizli kurallarını bilen kişilerdir. Onlar, projenizin yaşayan hafızası ve bilgi kaynağıdır.
Yazılım geliştiriciler olarak bizler "nasıl" yapılacağını biliriz, ama Domain Uzmanları "ne" yapılacağını ve "neden" yapılması gerektiğini bilir. Onlar olmadan, en parlak teknik becerilerimiz bile yanlış problemi çözen, kullanışsız bir kod yığını üretmekten öteye gidemez.
Domain Uzmanı Kim Olabilir? 2025'ten Örnekler
Domain Uzmanı, havalı bir unvana sahip olmak zorunda değildir. Onları genellikle işin tam merkezinde, elleri "kirli" bir şekilde bulursunuz.
- "Yeşil Rota" Lojistik Platformu Projemizde: Domain uzmanımız, 20 yıllık bir filo operasyonları yöneticisi olabilir. Bu kişi, bir drone'un bataryasının soğuk havada daha çabuk bittiğini, şehir merkezindeki hangi saatlerde trafiğin kilitlendiğini veya bir teslimatın "acil" olarak işaretlenmesinin ne gibi zincirleme reaksiyonlara yol açtığını size yaşanmış tecrübeleriyle anlatır. Bu bilgi, hiçbir dokümanda yazmaz.
- Bir Hastane Otomasyonu Projesinde: Domain uzmanımız, acil serviste 25 yıldır çalışan bir başhemşire olabilir. Hangi hastanın öncelikli olduğunu belirleyen "triage" sisteminin yazılı olmayan kurallarını, doktorların bir hasta epikrizinde hangi bilgiyi en önce görmek istediğini veya farklı laboratuvar sonuçlarının bir araya geldiğinde ne anlama geldiğini en iyi o bilir.
- Bir Fintek (Finansal Teknoloji) Uygulamasında: 2025'in karmaşık "Sürdürülebilirlik Kredisi" mekanizmasını modellemeye çalıştığımızı düşünelim. Domain uzmanımız, bu alanda uzmanlaşmış bir finansal analist veya bir banka portföy yöneticisi olabilir. Bize, bir şirketin "yeşil yatırım" notunun nasıl hesaplandığını, hangi regülasyonların bu notu etkilediğini ve bu kredinin geri ödeme koşullarındaki esnekliklerin neler olduğunu anlatır.
Gördüğünüz gibi, bu insanlar projenin "kullanıcıları" veya "müşterileri" olmaktan çok daha fazlasıdır. Onlar, projenin başarısı için kilit rol oynayan takım arkadaşlarıdır.
Neden Onlar "Gizli" Kahramanlar?
Çünkü geleneksel proje yönetiminde, bu değerli insanların bilgisi genellikle göz ardı edilir. Süreç genellikle şöyledir:
- Bir iş analisti, domain uzmanıyla birkaç toplantı yapar.
- Anladıklarını uzun bir dokümana döker.
- Bu doküman yazılım ekibine "gereksinim" olarak iletilir.
- Yazılım ekibi, bu dokümanı okuyup yorumlayarak kod yazar.
Bu süreç, "kulaktan kulağa" oyununa benzer. Her adımda, bilginin özü, ruhu ve kritik detayları kaybolur. Orijinal mesaj, son kişiye ulaştığında bambaşka bir şeye dönüşür.
Domain-Driven Design ise bu duvarı yıkar. DDD, yazılım geliştiricileri ile domain uzmanlarının doğrudan ve sürekli bir iş birliği içinde çalışmasını savunur. Geliştiriciler, o sıkıcı dokümanları okumak yerine, doğrudan filonun başındaki operasyon yöneticisiyle veya hastanedeki başhemşireyle konuşur. Birlikte beyaz tahtanın başına geçer, iş süreçlerini çizer ve en önemlisi, bir sonraki bölümde göreceğimiz ortak bir dil oluştururlar.
Unutmayın, projenizin en büyük riski yanlış bir teknoloji seçmek değil, çözdüğünüz problemi yanlış anlamaktır. Domain uzmanları, bu riski ortadan kaldıran sigortanızdır. Onlar projenizin gizli kahramanlarıdır ve onlara hak ettikleri değeri vermek, DDD felsefesinin ilk ve en önemli adımıdır.
————————
2.3. Her Yerde Aynı Dili Konuşmak: Ubiquitous Language (Evrensel Dil)
Daha önceki "kulaktan kulağa" oyununu hatırlıyor musunuz? İş uzmanının söylediği bir kavramın, analiz dokümanından geçip yazılımcının koduna gelene kadar nasıl tanınmaz hale geldiğini... İşte Ubiquitous Language (Evrensel Dil), bu oyunun oynanmasını en başından engelleyen kuralın adıdır.
Ubiquitous Language, en basit tanımıyla, bir proje ekibinin (yazılımcılar, domain uzmanları, test mühendisleri, yöneticiler) tamamı tarafından paylaşılan, üzerinde anlaşılmış, tek ve ortak bir terminolojidir. Bu dil, işin kendisinden, yani domain'den türetilir ve projenin her zerresine işler.
Bu dil, sadece toplantılarda konuşulan bir jargon değildir. O, yaşayan bir dildir ve şuralarda karşınıza çıkar:
- Ekip içi konuşmalarda
- Analiz dokümanlarında ve diyagramlarda
- Kullanıcı arayüzündeki metinlerde
- Veritabanı şemalarında
- Ve en önemlisi, kodun kendisinde! Sınıf isimlerinde, metot isimlerinde, değişkenlerde...
Mutfak benzetmemize dönersek; eğer usta aşçı (domain uzmanı) bir işlem için "mühürleme" diyorsa, yazılım ekibi de kendi arasında "eti seal edelim" diye konuşur ve koddaki metodun adı sealMeat() olur. cookMeat(), heatBriefly() veya brownTheSurface() gibi "yaklaşık" çeviriler kesinlikle yasaktır. Tek bir doğru vardır, o da domain uzmanının kullandığı terimdir.
————————
Örnek: "Sürdürülebilir Enerji Ticaret Platformu" (2025)
Şimdi bu fikri, sorduğunuz güncel ve harika örnek üzerinden somutlaştıralım. 2025 yılında, şirketlerin karbon salınımlarını dengelemek için yenilenebilir enerji sertifikaları alıp sattığı bir platform geliştiriyoruz. Ekibimizde enerji piyasası analistleri (domain uzmanları) ve yazılımcılar var.
Problem: Eğer ortak bir dilimiz olmasaydı, şöyle bir kaos yaşanırdı:
- Analist: "Bir 'Karbon Kredisi'nin 'arz talep eşleşmesi' gerçekleştiğinde, sahibine bir 'Enerji Sertifikası' üretilir."
- Yazılımcı A (Kendi Dünyasında): "Tamam, CarbonCredit objesi match olduğunda, yeni bir Certificate nesnesi yaratacağım."
- Yazılımcı B (Farklı Bir Yorum): "Anladım, SupplyDemand tablosunda status'ü 'matched' yapınca, EnergyCertificate tablosuna yeni bir kayıt (record) ekleyeceğim."
- Test Mühendisi: "Test senaryom şu: Bir Offer (teklif), bir Request (talep) ile eşleşince, CertificateData oluşuyor mu?"
Gördüğünüz gibi, herkes aynı şeyden bahsettiğini sanıyor ama aslında herkes farklı bir dilde konuşuyor: Karbon Kredisi, CarbonCredit, SupplyDemand, Offer... Bu durum, hatalara, yanlış anlaşılmalara ve bakımı imkansız bir koda davetiye çıkarmaktır.
————————
Ubiquitous Language ile Çözüm
DDD yaklaşımında, proje başlar başlamaz ekip bir araya gelir ve bu kavramsal karmaşayı çözer.
- Dilin Oluşturulması: Yazılımcılar ve analistler bir araya gelir. "Bu platformda alınıp satılan şeye ne diyeceğiz? Karbon Kredisi mi? Yeşil Tahvil mi? Salınım Hakkı mı?" diye tartışırlar. Analistler, sektör standardının "Karbon Kredisi" (Carbon Credit) olduğunu söyler. Artık herkes için o şeyin tek bir adı vardır.
- Bir kredinin piyasaya sürülmesine "Arz" (Supply) mı, "Teklif" (Offer) mı diyeceğiz?
- Bir krediyi alma isteğine "Talep" (Demand) mi, "İstek" (Request) mi diyeceğiz?
- Bu ikisinin bir araya gelmesi işlemi "Eşleşme" (Match) mi, "İşlem" (Transaction) mi olacak?
Ekip, tüm bu kilit kavramlar üzerinde anlaşır ve bir sözlük oluşturur. İşte bizim Ubiquitous Language'imiz doğdu!
- Dilin Koda Yansıması: Artık bu dil, kodun her yerine acımasızca uygulanır.
- // Sınıf adı, domain'deki kavramla birebir aynı: CarbonCredit.
- // Bu bir Aggregate Root olabilir.
- public class CarbonCredit {
- private final CarbonCreditId id;
- private final OwnerId owner;
- private CreditStatus status;
- // ... constructor ve diğer alanlar
- }
- // Servis metodu, iş sürecini birebir yansıtır.
- // "create" veya "generate" değil, domain uzmanının dediği gibi "issue".
- public class CertificationService {
- public EnergyCertificate issueCertificateForMatch(Match successfulMatch) {
- // ... Mantık ...
- return new EnergyCertificate(...);
- }
- }
- // Arz ve talebi bir araya getiren süreci yöneten servis.
- // Metodun adı, işin kendisini anlatıyor.
- public class MatchingService {
- // "process" veya "execute" gibi genel bir fiil yerine,
- // domain'e özgü "Arz Talep Eşleşmesi Bul" gibi bir isim.
- public Optional<Match> findMatchFor(Supply supply, Demand demand) {
- // İki tarafın koşullarının uyup uymadığını kontrol eden iş kuralları...
- if (supply.canBeMatchedWith(demand)) {
- return Optional.of(new Match(supply, demand));
- }
- return Optional.empty();
- }
- }
Sonuç: Bu yaklaşım sayesinde, altı ay sonra projeye yeni katılan bir yazılımcı koda baktığında, sadece teknik bir yapı görmez. Enerji analistinin iş süreçlerini, işin mantığını ve kurallarını doğrudan kodun içinde okuyabilir. Kod, yaşayan bir dokümana dönüşür ve o meşhur kod ile iş dünyası arasındaki uçurum ortadan kalkar. İşte Ubiquitous Language'in gücü budur.
————————
2.4. Problem Alanı (Problem Space) ve Çözüm Alanı (Solution Space): Doğru Sorunu Çözdüğümüzden Emin Olmak
Bir an için yazılım dünyasından uzaklaşıp kendinizi bir doktor olarak hayal edin. Bir hasta, "Doktor Bey, başım çok ağrıyor!" diyerek odanıza giriyor.
Bu noktada ne yaparsınız?
- Seçenek A (Çözüme Atlamak): "Anladım, baş ağrısı... Alın size en güçlü ağrı kesiciden bir kutu. Günde üç defa kullanın."
- Seçenek B (Problemi Anlamak): "Bir saniye... Bu ağrı ne zamandır var? Başınızın neresinde hissediyorsunuz? Gözlerinizde bir bulanıklık veya mide bulantısı eşlik ediyor mu? Son zamanlarda farklı bir şey yiyip içtiniz mi? Tansiyonunuzu bir ölçelim."
Seçenek A, doğrudan Çözüm Alanı'na (Solution Space) atlamaktır. Elinizdeki araçları (ağrı kesici) kullanarak semptomu (baş ağrısı) hemen gidermeye çalışırsınız. Belki hasta geçici olarak rahatlar. Ama ya asıl sorun yüksek tansiyon, göz bozukluğu veya daha ciddi bir şey ise? O zaman sadece ana problemi maskelemiş ve hastayı daha büyük bir tehlikeye atmış olursunuz.
Seçenek B ise önce Problem Alanı'nda (Problem Space) kalmaktır. Hastanın şikayetlerinin, belirtilerinin ve yaşam tarzının oluşturduğu o karmaşık dünyayı anlamaya çalışırsınız. Teşhisi, yani asıl problemi, doğru koymadan tedaviye, yani çözüme geçmezsiniz.
İşte yazılım geliştirmede de durum tam olarak budur. Çoğu zaman ekipler, iş birimlerinden gelen "Başım ağrıyor!" (yani, "Raporlarımız yavaş çalışıyor!") gibi bir şikayet karşısında hemen Çözüm Alanı'na atlarlar: "Hemen sunucuyu büyütelim, veritabanına indeks ekleyelim, önbellekleme (caching) mekanizması kuralım!"
DDD ise bize şunu fısıldar: "Dur! Önce Problem Alanı'nı anla."
- Problem Alanı (Problem Space): Bu, "işin olduğu yerdir". İşin hedefleri, kullanıcıların ihtiyaçları, karşılaşılan zorluklar, mevcut süreçler, kurallar ve kısıtlamalar bu alandadır. Henüz teknoloji, kod veya veritabanı yoktur. Sadece anlaşılması gereken bir "iş problemi evreni" vardır. Burası "Ne?" ve "Neden?" sorularının sorulduğu yerdir.
- Çözüm Alanı (Solution Space): Bu, "bizim bir şeyler yaptığımız yerdir". Problem Alanı'nda anladığımız ihtiyaçları karşılamak için tasarladığımız ve geliştirdiğimiz yazılımın dünyasıdır. Kodlarımız, sınıflarımız, algoritmalarımız, mimarimiz, kullandığımız framework'ler ve teknolojiler bu alandadır. Burası "Nasıl?" sorusuna cevap verdiğimiz yerdir.
DDD'nin temel prensibi şudur: Anlamlı bir Çözüm Alanı yaratabilmek için, önce Problem Alanı'nda yeterince ve kaliteli zaman geçirmek zorundasınız.
Örnek: "Yeşil Rota" Lojistik Platformu
Gelin bu ayrımı projemiz üzerinden netleştirelim.
Senaryo: "Yeşil Rota" platformunun yöneticisi ekibe geliyor ve "Teslimatlarımız sürekli gecikiyor, müşteriler şikayetçi. Bunu çözmemiz lazım!" diyor.
Kötü Yaklaşım (Doğrudan Çözüm Alanı'na Atlamak):
Ekip hemen toplanır.
- Yazılımcı A: "Rota optimizasyon algoritmamız yavaş kalıyor. Daha performanslı bir kütüphane bulup onu entegre edelim."
- Yazılımcı B: "Sorun algoritmada değil, araçların GPS verisi geç geliyor. Veri akışını saniyede bir yerine 100 milisaniyede bire indirelim."
- Yazılımcı C: "Bence en iyisi, her araca daha güçlü bir işlemci takalım."
Bunların hepsi birer çözüm önerisidir. Ama doğru problemi mi çözüyorlar? Bilmiyoruz.
İyi Yaklaşım (Önce Problem Alanı'nda Kalmak):
Ekip, teknoloji konuşmadan önce domain uzmanı olan filo yöneticisiyle masaya oturur. "Neden?" diye sorarlar.
- Soru: "Gecikmeler tam olarak neden kaynaklanıyor? Her zaman mı oluyor?"
- Cevap (Domain Uzmanı): "Hayır, özellikle öğleden sonra 15:00-17:00 arası okul çıkış saatlerinde ve ayın son haftası maaşlar yatınca AVM çevresindeki yollarda oluyor. Algoritmanız o anki anlık trafiği değil, sadece haritadaki standart yol hızını hesaba katıyor gibi."
- Soru: "Peki başka bir sebep var mı?"
- Cevap (Domain Uzmanı): "Evet. Drone'larımızın bataryaları yeni nesil 'hızlı şarj' istasyonlarında 10 dakikada doluyor. Ama sistemimiz hala eski istasyonlara göre 30 dakikalık şarj molaları planlıyor ve bu da rotada gereksiz bekleme yaratıyor."
Şimdi tablo tamamen değişti. Gördüğünüz gibi asıl problem, ne algoritmanın yavaşlığı ne de GPS verisinin sıklığı. Asıl problem, işin gerçekliğindeki iki önemli detayın modelde eksik olması: anlık trafik verisinin kullanılmaması ve farklı şarj istasyonu tiplerinin tanınmaması.
Problem Alanı'nı anladıktan sonra, artık Çözüm Alanı'na güvenle geçebiliriz. Çözümümüz artık daha nettir:
- Sisteme anlık trafik verisi sağlayan bir dış servisle entegrasyon yapılacak (TrafficData-Adapter adında bir modül).
- Station modelimize StationType (örneğin, FAST_CHARGE, STANDARD_CHARGE) adında bir özellik eklenecek ve rota planlama algoritması bu tipe göre farklı mola süreleri hesaplayacak.
Neden Bu Ayrım Hayatidir?
Çünkü yanlış problemi çözmek için harcanan her bir satır kod, sadece boşa gitmiş bir emekten daha fazlasıdır; gelecekte doğru çözümü geliştirmenizi engelleyecek bir teknik borçtur. Problem Alanı ve Çözüm Alanı ayrımını yapmak, ekibinizin enerjisini doğru yere odaklamasını sağlar ve projenin en başında yanlış bir yola sapmasını engeller. Bu, başarılı bir projenin temelidir.
————————
Bölüm 3: Stratejik Tasarım - Büyük Resmi Görmek
3.1. Bounded Context (Sınırlı Bağlam): Karmaşıklığı Yönetilebilir Parçalara Ayırmak
Şimdiye kadar "Domain"in, yani iş probleminin evreninin ne kadar karmaşık olabileceğinden bahsettik. Bir e-ticaret sitesi, bir hastane veya bir lojistik platformu... Bunların hepsi içinde yüzlerce kural ve süreç barındıran devasa sistemlerdir. Eğer bu devasa sistemi tek bir parça olarak modellemeye ve kodlamaya çalışırsak, en başta bahsettiğimiz o "çamur yumağına" (Big Ball of Mud) dönüşmesi kaçınılmazdır.
Peki, bir fili nasıl yersiniz? Elbette, lokma lokma.
Bounded Context (Sınırlı Bağlam), işte bu "lokmalara" ayırma sanatıdır. Bir Bounded Context, büyük ve karmaşık bir domain'in içinde, belirli bir modelin tutarlı ve anlamlı olduğu mantıksal bir sınır çizgisidir. Bu sınırın içinde, her kelimenin, her kavramın tek ve net bir anlamı vardır.
Dilbilgisi Benzetmesi
Bu kavramı anlamak için harika bir benzetme vardır: Dil. "Yüz" kelimesini düşünün. Bu kelimenin anlamı nedir?
- Bir dermatologla konuşuyorsanız, "yüz", cildin bir parçasıdır. Sivilceler, lekeler, gözenekler gibi kavramlarla ilişkilidir.
- Bir yüzme antrenörüyle konuşuyorsanız, "yüz", suyun içinde yapılan bir eylemdir. Kulaç, nefes, stil gibi kavramlarla ilişkilidir.
- Bir bankacıyla konuşuyorsanız, "yüz", bir para biriminin değeridir. Dolar, Euro, faiz gibi kavramlarla ilişkilidir.
Tek bir kelime ("yüz"), farklı bağlamlarda (dermatoloji, yüzme, bankacılık) tamamen farklı anlamlara gelir. Her bir bağlam, kelimeye kendi anlamını yükler ve kendi kurallarını belirler. İşte bu bağlamların her biri, birer Bounded Context'tir.
Bir dermatoloğun, hastasının cildinden bahsederken "kulaç tekniğini" düşünmesine gerek yoktur. Aynı şekilde, bizim de yazılımımızda her şeyin her şeyi bilmesine gerek yoktur. Bounded Context, bu "ilgi alanlarının ayrımını" yaparak karmaşıklığı yönetmemizi sağlar.
————————
Örnek: E-Ticaret Sistemi
Gelin bu fikri, sorduğunuz e-ticaret sistemi üzerinden inceleyelim. Geleneksel bir yaklaşımda, sistemdeki her şeyi yöneten dev bir Product (Ürün) sınıfı yaratmaya çalışırdık:
// KÖTÜ YAKLAŞIM: Her şeyi bilen devasa bir sınıf
public class Product {
private ProductId id;
private String name;
private String description;
private BigDecimal price; // Satış Fiyatı
private BigDecimal cost; // Stok Maliyeti
private int stockQuantity; // Stok Miktarı
private String supplierInfo; // Tedarikçi Bilgisi
private List<CustomerReview> reviews; // Müşteri Yorumları
private List<Campaign> activeCampaigns; // Aktif Kampanyalar
// ... ve onlarca başka alan...
}
Bu Product sınıfı, tam bir baş belasıdır. Satış departmanından gelen bir değişiklik (örn: "ürünlere hediye paketi seçeneği ekle") ile depo departmanından gelen bir değişiklik (örn: "ürünün depodaki raf adresini ekle") aynı sınıf üzerinde çakışmaya başlar. Kod kırılgan hale gelir ve anlaşılması imkansızlaşır.
DDD Yaklaşımı (Bounded Context'ler ile Ayırma):
DDD bize der ki: "Dur! Senin tek bir 'Ürün' kavramın yok. Farklı bağlamlarda farklı anlamlara gelen birden çok 'Ürün' modelin var."
Haritamızı çizelim ve sınırlarımızı belirleyelim:
- Satış Bağlamı (Sales Context):
- Amacı Nedir? Müşteriye ürünü göstermek ve satmak.
- "Ürün" Bu Bağlamda Ne Anlama Gelir? Müşterinin gördüğü şeydir. Fiyatı, adı, açıklaması, fotoğrafları, müşteri yorumları ve hangi kampanyalara dahil olduğu önemlidir.
- Kimi İlgilendirmez? Bu bağlam, ürünün depoda hangi rafta durduğunu veya tedarikçiden kaça alındığını zerre kadar umursamaz. Bu bilgiler, satış anında gereksiz birer gürültüdür.
// Satış Bağlamı'ndaki Product modeli
public class Product {
private final ProductId id;
private final String name;
private final String description;
private final Money price; // Fiyat burada çok önemli!
private final List<CustomerReview> reviews;
// Bu ürüne uygulanabilecek kampanyaları bulma gibi iş mantığı...
public List<Campaign> findAvailableCampaigns() { /*...*/ }
}
- Stok Yönetimi Bağlamı (Inventory Context):
- Amacı Nedir? Ürünlerin fiziksel olarak depolanmasını, sayımını ve takibini yapmak.
- "Ürün" Bu Bağlamda Ne Anlama Gelir? Takip edilmesi gereken fiziksel bir nesnedir. Stok kodu (SKU), depodaki adedi, ağırlığı, boyutları, hangi tedarikçiden geldiği ve maliyeti önemlidir.
- Kimi İlgilendirmez? Bu bağlam, ürünün müşteri yorumlarını veya pazarlama kampanyalarını bilmek zorunda değildir. Ürünün fiyatı bile burada önemli değildir; önemli olan onun maliyetidir.
// Stok Yönetimi Bağlamı'ndaki Item (Eşya/Stok Kalemi) modeli
// Dikkat: Burada ismine "Product" demek yerine "Item" diyerek
// dilin bağlama göre değiştiğini daha da vurgulayabiliriz.
public class Item {
private final Sku sku; // Stok kodu burada kimlik olabilir.
private final String name;
private int quantityInStock;
private final Money cost; // Maliyet burada kritik!
private final LocationInWarehouse location;
// Stoktan düşme gibi iş mantığı...
public void decreaseStock(int amount) { /*...*/ }
}
- Müşteri İlişkileri Bağlamı (Support Context):
- Amacı Nedir? Satış sonrası destek sağlamak, iadeleri ve şikayetleri yönetmek.
- "Ürün" Bu Bağlamda Ne Anlama Gelir? Bir şikayete veya iade talebine konu olan şeydir. Garanti durumu, seri numarası, satın alan müşteri bilgisi ve ilgili destek talepleri önemlidir.
- Kimi İlgilendirmez? Bu bağlamın, ürünün depodaki maliyetini veya o anki satış kampanyalarını bilmesine gerek yoktur.
Neden "Fiyat" ve "Maliyet" Farklı Kavramlardır?
Çünkü farklı bağlamlarda farklı iş hedeflerine hizmet ederler:
- Fiyat (Price): "Satış Bağlamı"nın bir parçasıdır. Şirketin müşteriden almayı umduğu parayı temsil eder. Pazarlama stratejileri, rakip fiyatları, kampanyalar gibi faktörlere göre belirlenir. Dinamiktir.
- Maliyet (Cost): "Stok Yönetimi Bağlamı"nın bir parçasıdır. Şirketin o ürünü elde etmek için tedarikçiye ödediği parayı temsil eder. Kârlılık hesabı ve envanterin finansal değerini belirlemek için kullanılır. Genellikle daha statiktir.
Bounded Context kullanarak, bu iki kavramın birbirine karışmasını engelleriz. Her model, sadece kendi bağlamı içinde anlamlı olan verileri ve davranışları barındırır. Bu sayede, devasa ve kırılgan bir Product sınıfı yerine, her biri kendi işini mükemmel yapan, küçük, odaklanmış ve yönetilebilir modeller oluştururuz. İşte bu, karmaşıklığı yönetmenin ilk ve en önemli adımıdır.
Harika, Stratejik Tasarım'ın ikinci büyük aracına geçiyoruz. Önceki bölümde sınırlarımızı, yani Bounded Context'lerimizi çizdik. Artık e-ticaret sistemimizde bir "Satış", bir "Stok Yönetimi" ve bir "Müşteri İlişkileri" bağlamı olduğunu biliyoruz.
Peki, bu ayrı ayrı çizdiğimiz "krallıklar" birbirleriyle nasıl konuşacak? Birbirlerini tamamen görmezden mi gelecekler? Savaş mı yapacaklar, yoksa ticaret mi? İşte Context Map (Bağlam Haritası), bu krallıklar arasındaki diplomatik ilişkileri tanımlayan, projenizin politik haritasıdır.
Bir Context Map, Bounded Context'ler arasındaki teknik entegrasyondan çok, ekipler arasındaki sosyal ve organizasyonel ilişkiyi belgeler. Bu harita, bize hangi entegrasyon desenini seçeceğimizi ve olası iletişim sorunlarını nerede beklememiz gerektiğini söyler.
Gelin bu diplomatik ilişki türlerini tek tek inceleyelim.
————————
Partnership (Ortaklık) 🤝
- İlişki Türü: İki takımın kaderi bir. Birlikte başarılı olurlar ya da birlikte batarlar.
- Açıklama: İki farklı Bounded Context üzerinde çalışan iki takım düşünün. Birinin başarısız olması, diğerinin de projesini anlamsız kılıyorsa, bu iki takım bir ortaklık içindedir. Ekipler, entegrasyon sorunlarını çözmek için sürekli iletişim halinde olmalı ve planlarını birlikte yapmalıdırlar. Hiçbir takım, diğerine danışmadan kritik bir değişiklik yapamaz.
- Benzetme: Üç bacaklı yarışa katılan iki yarışmacı gibidirler. Adımlarını senkronize atmak zorundadırlar, yoksa ikisi de yere düşer.
- Ne Zaman Kullanılır? Birbirine çok sıkı bağlı iş süreçlerini geliştiren ekipler arasında. Örneğin, bir "Uçuş Planlama" context'i ile "Mürettebat Atama" context'i. Biri olmadan diğeri işe yaramaz.
————————
Shared Kernel (Paylaşılan Çekirdek) noyau
- İlişki Türü: Riskli ama bazen gerekli bir evlilik.
- Açıklama: Bu, iki veya daha fazla takımın, domain modelinin küçük bir alt kümesini (birkaç sınıf, bir veritabanı tablosu vb.) ortaklaşa paylaştığı ve yönettiği durumdur. Bu paylaşılan kod parçası, yani "çekirdek", her iki takımın da dokunabildiği ortak bir alandır.
- Neden Riskli? Çünkü bir takımın bu ortak kodda yaptığı bir değişiklik, diğer takımın haberi olmadan onların sistemini bozabilir. Bu nedenle, paylaşılan çekirdeğe dokunulmadan önce tüm paydaş takımların onayı alınmalıdır. Çok fazla koordinasyon gerektirir.
- Ne Zaman Kullanılır? Gerçekten projenin temelini oluşturan, çok stabil ve değişmesi beklenmeyen, her context için birebir aynı anlama gelen kritik bir model olduğunda.
————————
Customer-Supplier (Müşteri-Tedarikçi) 🛒
- İlişki Türü: Güç dengeleri.
- Açıklama: Bu, en yaygın ilişki türlerinden biridir. Bir takım (Supplier/Tedarikçi), diğer takımın (Customer/Müşteri) ihtiyaç duyduğu bir hizmeti veya veriyi sağlar. Tedarikçi takım "yukarı akış" (upstream), müşteri takım ise "aşağı akış" (downstream) olarak adlandırılır. Güç dengesi burada kritiktir. Müşteri takımın ihtiyaçları, tedarikçi takımın önceliklerini belirler.
- Benzetme: Bir restoranda mutfak (tedarikçi) ile garson (müşteri) arasındaki ilişki gibidir. Garson, salona gelen siparişleri mutfağa iletir ve mutfak da bu siparişleri hazırlayıp garsona verir.
- Ne Zaman Kullanılır? Bir servisin, başka bir servisin sağladığı veriye veya işlevselliğe bağımlı olduğu çoğu senaryoda.
————————
Conformist (Uyumlu) follower
- İlişki Türü: Büyük balığı takip etmek.
- Açıklama: Bu ilişkide, müşteri (aşağı akış) takımın, tedarikçi (yukarı akış) takım üzerinde hiçbir gücü veya söz hakkı yoktur. Tedarikçi takım, modeli ve API'yı kendi bildiği gibi geliştirir. Müşteri takımın tek seçeneği, bu modele harfiyen uymaktır. Kendi dünyasında farklı bir model yaratmaya çalışmaz, tedarikçinin modelini olduğu gibi kabul eder.
- Neden Yapılır? Tedarikçi takım çok büyük, çok güçlü veya belki de dışarıdan alınan hazır bir sistem (Örn: SAP, Salesforce) olduğunda bu ilişki kaçınılmazdır. Savaşmanın bir anlamı yoktur.
- Ne Zaman Kullanılır? Çok büyük, köklü bir sistemle veya üzerinde pazarlık gücünüzün olmadığı bir üçüncü parti servisle entegre olurken.
————————
Anticorruption Layer (Bozulmayı Önleyici Katman) 🛡️
- İlişki Türü: Tercüman ve diplomat.
- Açıklama: Bu desen, Conformist modelinin bir alternatifidir. Müşteri takım, tedarikçinin karmaşık, eski veya "kirli" modeline uymak yerine, kendi Bounded Context'inin girişine bir "tercüman" koyar. Bu katman, dış dünyadan gelen garip modelleri alır ve kendi temiz, modern, ideal dünyasına uygun bir modele çevirir. Bu sayede, dış sistemdeki karmaşıklığın kendi sistemini "bozmasını" veya "kirletmesini" engeller.
- 2025 Bankacılık Örneği: Bankanızın, 1980'lerden kalma bir Mainframe (ana sistem) üzerinde çalışan ve müşteri verilerini EBCDIC formatında, karmaşık kodlarla tutan bir "Çekirdek Bankacılık Sistemi" olduğunu düşünelim. Yeni geliştirdiğiniz, Java 21 ile yazılmış pırıl pırıl "Mobil Bankacılık" mikroservisinizin bu canavarla konuşması gerekiyor. Mobil servisinizi, Mainframe'in garip veri yapılarıyla kirletmek yerine, araya bir Anticorruption Layer (ACL) koyarsınız.
- ACL, Mainframe'den gelen "MSTR_HSP_NO=90123" gibi bir veriyi alır.
- Bunu, kendi modern Customer modelinizdeki customer.setAccountNumber("90123") gibi anlamlı bir işleme çevirir.
- Böylece Mobil Bankacılık servisi, sanki modern bir sistemle konuşuyormuş gibi mutlu mesut yaşar. Mainframe'in varlığından bile haberi olmaz.
————————
Open Host Service (Açık Sunucu Hizmeti) & Published Language (Yayınlanmış Dil) 🌐
Bu ikisi genellikle birlikte kullanılır.
- Open Host Service (OHS): Bir Bounded Context'in, kendi içindeki servisleri dış dünyanın kullanımına iyi belgelenmiş bir protokol (genellikle REST API) ile açmasıdır. "Benimle konuşmak isteyenler için resmi kapım budur" deme şeklidir.
- Published Language (PL): Bu resmi kapıdan (OHS) konuşulacak olan dildir. Herkesin anlayabileceği, iyi tanımlanmış, ortak bir formattır. Genellikle JSON Schema, Avro veya Protocol Buffers gibi teknolojilerle bu dilin yapısı (schema'sı) net bir şekilde yayınlanır.
Bu ikili, farklı takımların birbirlerinin iç işleyişini bilmeden, sadece kamuya açık API ve veri formatları üzerinden güvenli bir şekilde entegre olmasını sağlar. Bu, modern mikroservis mimarilerinin temelini oluşturur.
Harika bir noktadayız. Stratejik Tasarım ile projemizin haritasını çizdik ve bu haritadaki ülkeler (Bounded Context'ler) arasındaki diplomatik ilişkileri (Context Map) belirledik. Şimdi sormamız gereken en önemli stratejik soru şu: "Elimizdeki kısıtlı zamanı, parayı ve en iyi mühendislerimizi hangi ülkeyi geliştirmek için harcamalıyız?"
İşte bu sorunun cevabı, domain'imizi üç ana kategoriye ayırmaktan geçer.
————————
3.3. Core, Supporting ve Generic Subdomainler: Nereye Odaklanmalıyız?
Her işin her parçası aynı derecede önemli değildir. Bir şirketin para kazanmasını, rakiplerinden ayrışmasını ve müşteriler tarafından tercih edilmesini sağlayan bir "kalbi" vardır. Geri kalan her şey bu kalbin atmasını sağlamak için oradadır. DDD, bu önceliklendirmeyi yapabilmek için bize üç temel sınıflandırma sunar.
Gelin bunu bir restoran benzetmesi üzerinden anlayalım. Şehrin en popüler yeni nesil restoranını açtığınızı hayal edin.
————————
Core Domain (Çekirdek Alan) ❤️🔥
- Nedir? Bu, işinizin kalbidir. Sizi siz yapan, müşterilerin kapınızda kuyruk olmasını sağlayan, size rekabet avantajı kazandıran ve işinizin asıl para kazandığı yerdir. Burası, en yaratıcı, en yenilikçi olmanız gereken alandır.
- Restoran Benzetmesi: Sizi meşhur eden şey nedir? Belki de sadece sizin bildiğiniz gizli bir tarifle hazırlanan, 48 saat kısık ateşte pişen özel bir et yemeği. Ya da moleküler gastronomi teknikleriyle hazırladığınız inanılmaz tatlılar. İşte bu, sizin Core Domain'inizdir. En iyi şeflerinizi, en kaliteli malzemelerinizi ve tüm enerjinizi bu yemeği mükemmelleştirmek için harcarsınız. Bu tarifi asla dışarıdan satın almazsınız, çünkü sizi eşsiz kılan şey odur.
- 2025 Startup Örneği: Yapay zeka destekli, kişiselleştirilmiş seyahat rotası oluşturan bir startupsanız, sizin Core Domain'iniz, kullanıcının ilgi alanlarına, bütçesine ve anlık hava durumuna göre en optimal rotayı bulan o eşsiz rota optimizasyon algoritmasıdır. En parlak mühendislerinizi bu algoritma üzerinde çalıştırmalısınız.
————————
Supporting Subdomain (Destekleyici Alt Alan) 💪
- Nedir? Bu alanlar, Core Domain'in başarılı olması için mutlaka gereklidir, ancak tek başlarına bir rekabet avantajı sunmazlar. Genellikle şirketinize özel iş süreçleri içerirler ama standart çözümlerle de tam olarak karşılanamazlar.
- Restoran Benzetmesi: Harika yemeğinizi (Core) servis edebilmek için bir garson ve sipariş sistemine ihtiyacınız var. Bu sistem size özel olmalı; masaları, siparişleri, mutfağa iletimi yönetmeli. Bu işi kötü yaparsanız, harika yemeğiniz müşteriye soğuk gider ve her şey mahvolur. Yani bu sistem kritik derecede gereklidir. Ama dünyanın en iyi garson sipariş sistemine sahip olmanız, tek başına müşteri çekmenizi sağlamaz. Kimse "Hadi o restorana gidelim, sipariş sistemleri harikaymış!" demez. Bu nedenle bu işi "yeterince iyi" yapmanız kafidir. Bu, sizin Supporting Subdomain'inizdir.
- 2025 Startup Örneği: Rota algoritmanızın (Core) çalışabilmesi için, seyahat blogger'larından ve otellerden size özel içerikler ve anlaşmalar toplayan bir içerik yönetim sistemine (CMS) ihtiyacınız var. Bu sistem size özel olmalı ama onu dünyanın en karmaşık sistemi yapmanıza gerek yok. Core Domain'inizi destekleyecek kadar iyi çalışması yeterlidir.
————————
Generic Subdomain (Genel Alt Alan) ⚙️
- Nedir? Bunlar, her işletmenin ihtiyaç duyduğu, çözümü zaten icat edilmiş olan standart problemlerdir. Tekerleği yeniden icat etmenin hiçbir anlamı olmadığı alanlardır.
- Restoran Benzetmesi: Restoranınızın çalışanlarının maaşını ödemek için bir muhasebe ve bordro sistemine ihtiyacınız var. Ama kendi muhasebe yazılımınızı mı yazarsınız? Asla! Piyasadan hazır bir paket satın alır veya bir muhasebeciyle anlaşırsınız. Aynı şekilde, restoranın web sitesindeki "bize ulaşın" formu için kendi e-posta sunucunuzu kurmazsınız; hazır bir e-posta hizmeti (Gmail, Outlook vb.) kullanırsınız. Bunlar sizin Generic Subdomain'lerinizdir.
- 2025 Startup Örneği: Kullanıcıların sisteme giriş yapabilmesi için bir kimlik doğrulama (authentication) sistemine ihtiyacınız var. Kendi şifreleme, parola sıfırlama mekanizmanızı sıfırdan yazmak yerine, Auth0, Okta gibi hazır bir servis kullanır veya Spring Security gibi standart bir kütüphaneden faydalanırsınız. E-posta bildirimleri için Amazon SES veya SendGrid kullanırsınız. Bunlar, çözümü "satın almanız" veya "ödünç almanız" gereken standart problemlerdir.
Sonuç: Nereye Odaklanmalıyız?
Bu sınıflandırmayı yapmak, bize stratejik bir güç verir:
- Core Domain'e Yatırım Yap: En iyi yeteneklerinizi, en çok zamanınızı ve en çok parayı buraya harcayın. Sıfırdan, size özel, en iyi çözümü burada geliştirin.
- Supporting Subdomain'i "Yeterince İyi" Geliştir: Bu alanları kurum içinde geliştirin ama mükemmeliyetçi olmayın. Amaç, Core Domain'i aksatmayacak, işi görecek bir çözüm üretmektir.
- Generic Subdomain'i Satın Al veya Ödünç Al: Bu alanlar için asla sıfırdan kod yazmayın. Piyasadan hazır bir çözüm (SaaS) satın alın veya açık kaynaklı bir kütüphane kullanın.
Bu odaklanma, kısıtlı kaynaklarınızı en değerli olduğunuz alanda kullanmanızı sağlayarak projenizin ve işinizin başarı şansını dramatik bir şekilde artırır.
Stratejik Tasarım ile projemizin büyük resmini ve mimari vizyonunu oluşturduk. Artık ellerimizi kirletme ve bu vizyonu koda dökme zamanı geldi. Taktiksel Tasarım, bize tam olarak bunu nasıl yapacağımızı gösteren, kod seviyesindeki desenler ve yapı taşlarıdır.
Bu yolculukta kullanacağımız araç ise modern, güçlü ve DDD felsefesiyle harika bir uyum içinde çalışan Java 21.
————————
4.1. Neden Java 21? Sanal Thread'ler (Virtual Threads), Record'lar ve Pattern Matching'in DDD için Anlamı
DDD, belirli bir programlama diline veya framework'e bağımlı değildir; bir felsefedir. Ancak bazı diller ve teknolojiler, bu felsefeyi hayata geçirmeyi diğerlerinden çok daha kolay ve zarif hale getirir. 2025 itibarıyla Java'nın en son Uzun Süreli Destek (LTS) sürümü olan Java 21, DDD'nin taktiksel desenlerini uygulamak için bize adeta ısmarlama üretilmiş gibi hissettiren üç muhteşem özellik sunar.
1. record'lar: Mükemmel Değer Nesneleri (Value Objects) İçin Yaratılmış
DDD'nin en temel yapı taşlarından biri Değer Nesneleri'dir (Value Object). Bir kimliği olmayan, sadece taşıdığı değerlerle anlam kazanan ve en önemlisi değiştirilemez (immutable) olan bu nesneler, iş kurallarının bütünlüğü için hayatidir. Money (Para), Address (Adres) veya Weight (Ağırlık) gibi kavramlar Değer Nesneleridir.
Java 21'den Önce: Değiştirilemez bir Money sınıfı yazmak için şunlar gerekirdi:
- final alanlar (fields).
- Tüm alanları alan bir constructor.
- Sadece getter metodları (setter yok).
- equals(), hashCode() ve toString() metodlarını doğru bir şekilde override etmek.
Bu, basit bir kavram için bile bir sürü standart (boilerplate) kod demekti.
Java 21 record ile:
// Bitti! Hepsi bu kadar.
public record Money(BigDecimal amount, Currency currency) { }
Java 21'deki record anahtar kelimesi, bizim için tüm bu işleri otomatik olarak yapar. Tek bir satırda, DDD'nin Değer Nesnesi tanımına mükemmel uyan, değiştirilemez (immutable), veriyi temiz bir şekilde taşıyan bir sınıf yaratmış oluruz. Bu, kodumuzu inanılmaz derecede temizler ve doğrudan iş mantığına odaklanmamızı sağlar.
————————
2. Pattern Matching: İş Kurallarını Daha Anlaşılır Yazmak
Domain modellerimiz genellikle farklı durumlar veya türler içerebilir. Örneğin, bir Shipment (Gönderi) Hazırlanıyor, Yolda, Teslim Edildi veya İade Edildi gibi farklı durumlarda olabilir ve her durumun kendine özgü bir iş kuralı olabilir.
Java 21'den Önce: Bu tür durumları yönetmek için genellikle bir sürü if-else if bloğu veya instanceof kontrolü kullanırdık. Bu, kodun okunabilirliğini azaltır ve yeni bir durum eklendiğinde hata yapma olasılığını artırırdı.
Java 21 Pattern Matching ile: switch ifadeleri artık çok daha akıllı.
// Shipment bir "sealed interface" ve her durum bunu implemente ediyor.
// Bu sayede switch, tüm olası durumları kontrol etmemizi zorunlu kılıyor.
public Money calculateShippingFee(Shipment shipment) {
return switch (shipment) {
case PreparingShipment ps -> calculateBaseFee();
// Değişkenin tipini kontrol edip doğrudan kullanabiliyoruz.
case OnTheWayShipment otws -> calculateDistanceBasedFee(otws.getRoute());
case DeliveredShipment ds -> Money.ZERO;
case ReturnedShipment rs -> calculateReturnFee(rs.getReason());
};
}
Bu yapı, sadece daha temiz ve okunaklı olmakla kalmaz, aynı zamanda daha güvenlidir. Shipment için yeni bir durum (örneğin LostShipment) eklediğimizde, derleyici bu switch bloğunun eksik olduğunu fark eder ve bizi uyararak potansiyel bir hatayı en başından engeller. Bu, iş kurallarımızı koda dökerken bize müthiş bir güvenlik ağı sağlar.
————————
3. Sanal Thread'ler (Virtual Threads): Eş Zamanlı İşlemleri Basitleştirmek
Modern DDD sistemleri genellikle olay güdümlüdür (event-driven). Bir Sipariş Onaylandı olayı, aynı anda hem "Stok" sistemine hem "Faturalandırma" sistemine hem de "Bildirim" sistemine haber göndermeyi tetikleyebilir. Bu tür binlerce eş zamanlı isteği yönetmek, geleneksel Java thread modeliyle karmaşık ve kaynak tüketen bir işti.
Java 21 Sanal Thread'ler ile: Java, artık işletim sisteminin pahalı thread'lerine doğrudan bağlı olmayan, çok hafif, "sanal" thread'ler yaratabiliyor. Bu ne anlama geliyor?
Artık binlerce farklı görevi (örneğin, binlerce domain event'ini işlemek) sanki her biri kendi thread'indeymiş gibi basit ve sıralı bir kodla yazabiliriz. Arka plandaki karmaşık thread havuzu yönetimini Java'nın kendisine bırakırız.
Bu, özellikle reaktif programlama gibi karmaşık paradigmaları öğrenmeye gerek kalmadan, yüksek performanslı ve ölçeklenebilir uygulamalar yazmamızı sağlar. Kodumuz basit ve anlaşılır kalırken, sistemimiz 2025'in getirdiği yüksek kullanıcı yükünü rahatlıkla karşılayabilir.
Teoriden pratiğe geçişimizin ilk adımı olan geliştirme ortamını hazırlamaya başlayalım. Bu bölümde, DDD yolculuğumuzda bize eşlik edecek olan temel araçları kurup, ilk projemizin temelini atacağız. Amacımız, herkesin kolayca takip edebileceği, temiz ve standart bir başlangıç noktası oluşturmak.
————————
4.2. Geliştirme Ortamının Kurulumu: JDK 21, Spring Boot 3.x ve Maven/Gradle
Taktiksel Tasarım'ın güzelliğini koda dökebilmek için öncelikle atölyemizi, yani geliştirme ortamımızı hazırlamamız gerekiyor. Bu süreç, marangozun tezgâhını düzenlemesi gibidir; doğru araçlar elinizin altında olduğunda, ortaya harika eserler çıkarmanız çok daha kolaylaşır.
Bu kitap boyunca kullanacağımız temel araç setimiz şunlardan oluşacak:
- Java Development Kit (JDK) 21: Kodlarımızı derleyip çalıştıracak olan Java'nın kendisi.
- Maven veya Gradle: Projemizin bağımlılıklarını (kullandığı kütüphaneleri) yöneten ve projemizi paketleyen yapılandırma araçları.
- IntelliJ IDEA: Kodlarımızı yazacağımız modern ve güçlü bir Entegre Geliştirme Ortamı (IDE).
Hadi adım adım bu kurulumları yapalım.
Adım 1: Java'nın Kalbi - JDK 21 Kurulumu
Java 21, record'lar ve Sanal Thread'ler gibi modern özellikleriyle DDD için harika bir seçim. Bilgisayarınıza Java'yı kurmak için popüler ve ücretsiz dağıtımlardan birini tercih edebilirsiniz. Adoptium Temurin iyi bir başlangıç noktasıdır.
- İndirme: Web tarayıcınızdan adoptium.net adresine gidin.
- Sürüm Seçimi: Ana sayfada size en son LTS (Uzun Süreli Destek) sürümü olan "Temurin 21 (LTS)" önerilecektir. İşletim sisteminize (Windows, macOS, Linux) uygun olanı seçip indirin.
- Kurulum: İndirdiğiniz yükleyiciyi çalıştırın ve kurulum adımlarını takip edin. Kurulum sırasında "Add to PATH" (PATH'e Ekle) veya "Set JAVA_HOME variable" (JAVA_HOME değişkenini ayarla) gibi bir seçenek görürseniz, mutlaka işaretleyin. Bu, Java'nın komut satırından kolayca erişilebilir olmasını sağlar.
- Doğrulama: Kurulum bittikten sonra bir komut istemcisi (Windows'ta cmd veya PowerShell, macOS/Linux'ta Terminal) açın ve şu komutu yazın:
- java --version
- Eğer ekranda openjdk 21... veya benzeri bir çıktı görüyorsanız, tebrikler! Java'yı başarıyla kurdunuz.
Adım 2: Proje Yöneticileri - Maven ve Gradle
Bir proje geliştirirken onlarca farklı kütüphaneye ihtiyaç duyarız (Spring Boot, veritabanı sürücüsü, test kütüphaneleri vb.). Bu kütüphaneleri elle indirip projemize eklemek tam bir kabustur. İşte Maven ve Gradle, bu işi bizim için otomatikleştiren harika yardımcılardır. Projemizin "ihtiyaç listesini" bir dosyaya yazarız, o da bizim için tüm kütüphaneleri bulur, indirir ve yönetir.
Bu kitapta her iki araca da uygun örnekler bulacaksınız. Genellikle IntelliJ IDEA gibi IDE'ler bu araçları kendi içlerinde barındırır, bu yüzden ayrıca kurmanıza gerek kalmayabilir. Ancak sisteminizde genel olarak bulunmaları her zaman iyidir.
- Maven için: maven.apache.org adresinden indirip kurabilirsiniz. Doğrulamak için mvn -v komutunu kullanın.
- Gradle için: gradle.org adresinden indirip kurabilirsiniz. Doğrulamak için gradle -v komutunu kullanın.
Şimdilik bu adımı atlayabilirsiniz, çünkü bir sonraki adımda kullanacağımız Spring Initializr ve IntelliJ IDEA bu işi bizim için büyük ölçüde halledecek.
Adım 3: Atölyemiz - IntelliJ IDEA Kurulumu
Kodlarımızı yazmak için konforlu ve akıllı bir editöre ihtiyacımız var. JetBrains tarafından geliştirilen IntelliJ IDEA, Java dünyasında adeta bir standart haline gelmiştir.
- İndirme: jetbrains.com/idea/download/ adresine gidin.
- Sürüm Seçimi: Karşınıza iki sürüm çıkacak: Ultimate (ücretli, 30 gün deneme sürümü var) ve Community (ücretsiz). Spring Boot geliştirmesi için Community sürümü başlangıçta tamamen yeterlidir. Community sürümünü indirip kurun.
- İlk Ayarlar: Programı ilk açtığınızda size tema (koyu/açık) gibi birkaç ayar soracaktır. Zevkinize göre seçip devam edebilirsiniz.
Adım 4: Projemizin Temelini Atmak - Spring Initializr
Sıfırdan bir Spring Boot projesi yapılandırmak yerine, Spring ekibinin bize sunduğu harika bir araç olan Spring Initializr'ı kullanacağız. Bu araç, bize istediğimiz özelliklerde, yapılandırılmış, tertemiz bir proje paketi hazırlar.
- Web tarayıcınızda start.spring.io adresine gidin.
- Karşınıza çıkan proje oluşturma formunu aşağıdaki gibi doldurun:
- Project: Maven (veya isterseniz Gradle seçebilirsiniz, biz Maven ile devam edeceğiz)
- Language: Java
- Spring Boot: Varsayılan olarak gelen en güncel stabil 3.x sürümünü (örn: 3.4.x) seçili bırakın.
- Project Metadata:
- Group: com.yazilimokulu (Genellikle şirketinizin veya kişisel web sitenizin tersten yazılmış halidir)
- Artifact: ddd-kitap-projesi (Projenizin adı)
- Name: ddd-kitap-projesi
- Description: DDD Kitabı için Örnek Spring Boot Projesi
- Package name: com.yazilimokulu.dddkitapprojesi (Otomatik oluşacaktır)
- Packaging: Jar
- Java: 21 (Mutlaka 21'i seçtiğinizden emin olun!)
- Bağımlılıkları (Dependencies) Ekleme: Sağ taraftaki "ADD DEPENDENCIES..." butonuna tıklayın. Şimdilik sadece iki temel bağımlılık ekleyeceğiz:
- Spring Web: Web uygulamaları ve RESTful servisler geliştirmemizi sağlar.
- Lombok: recordların yaptığı gibi, getter, setter, constructor gibi standart kodları bizim için otomatik üreterek sınıflarımızı temiz tutan sihirli bir kütüphane.
- Proje Oluşturma: Her şey hazır olduğunda, sayfanın altındaki "GENERATE" butonuna tıklayın. Tarayıcınız, ddd-kitap-projesi.zip adında bir zip dosyası indirecektir.
Bu zip dosyası, ilk projenizin temelidir. Onu bilgisayarınızda uygun bir yere çıkarın. Artık atölyemiz hazır ve ilk projemizin iskeleti elimizde. Bir sonraki adımda, bu iskeleti IntelliJ IDEA'da açıp ilk kodumuzu yazmaya başlayacağız.
Atölyemizi kurduk, projemizin iskeletini start.spring.io'dan oluşturduk. Şimdi o iskelete can verme ve ekranda "Merhaba!" dediğini görme zamanı. Bu adım, her şeyin doğru çalıştığını teyit etmemizi sağlayacak ve bize DDD'nin taktiksel yapı taşlarını inşa edeceğimiz sağlam bir zemin verecek.
————————
4.3. Merhaba DDD: İlk Spring Boot Projemiz
Önceki adımda oluşturup indirdiğimiz ddd-kitap-projesi.zip dosyasını hatırlayın. Şimdi o paketlenmiş iskeleti atölye tezgâhımıza, yani IntelliJ IDEA'ya alıp inceleyeceğiz.
Adım 1: Projeyi IntelliJ IDEA'da Açmak
- Öncelikle indirdiğiniz ddd-kitap-projesi.zip dosyasını bilgisayarınızda kolayca ulaşabileceğiniz bir klasöre çıkarın (unzip yapın).
- IntelliJ IDEA'yı açın. Karşılama ekranında "Open" seçeneğine tıklayın. (Eğer başka bir proje açıksa, File > Open menüsünü kullanabilirsiniz.)
- Dosya gezgini açıldığında, az önce zip'ten çıkardığınız ddd-kitap-projesi klasörünü bulun, seçin ve "Open" butonuna tıklayın.
- IntelliJ IDEA projeyi analiz edecek ve bir Maven projesi olduğunu anlayacaktır. Sağ alt köşede, projenin bağımlılıklarının (dependencies) indirildiğini gösteren bir ilerleme çubuğu görebilirsiniz. Bu işlemin bitmesini bekleyin. Bu, Maven'ın pom.xml dosyasındaki ihtiyaç listemizi okuyup kütüphaneleri internetten indirmesidir.
Adım 2: Proje Yapısını Anlamak
Sol taraftaki Proje panelinde, Spring Initializr'ın bizim için oluşturduğu standart klasör yapısını göreceksiniz. Hızla tanıyalım:
- pom.xml: Projemizin kalbi. Tüm bağımlılıklarımızın (Spring Web, Lombok vb.) listelendiği, projenin kimlik kartı niteliğindeki dosyadır.
- src/main/java/com/yazilimokulu/dddkitapprojesi: İşte burası bizim evimiz. Tüm Java kodlarımızı bu paketin içine yazacağız.
- DddKitapProjesiApplication.java: IntelliJ IDEA'nın, içinde @SpringBootApplication anotasyonu olan bu sınıfı bulup projeyi başlatacağı ana giriş noktasıdır.
- src/main/resources: Yapılandırma dosyalarımızın bulunduğu yer.
- application.properties: Uygulamamızın veritabanı bağlantısı, sunucu portu gibi ayarlarını yapacağımız dosya.
- src/test/java: Kodlarımızın doğru çalıştığını kontrol etmek için yazacağımız testlerin bulunacağı klasör.
Adım 3: Uygulamayı İlk Kez Çalıştırmak
Her şeyin yolunda olup olmadığını görmek için uygulamayı bir kez çalıştıralım.
- Proje panelinden src/main/java/... altındaki DddKitapProjesiApplication.java sınıfını bulun ve çift tıklayarak açın.
- public static void main(String[] args) metodunun yanında, satır numarasının olduğu yerde yeşil bir "Play" (▶) ikonu göreceksiniz.
- Bu ikona tıklayın ve "Run 'DddKitapProjesiApplication'" seçeneğini seçin.
IntelliJ IDEA, ekranın alt kısmında "Run" panelini açacak. Burada rengarenk bir "Spring" yazısı ve bir dizi log mesajı göreceksiniz. Eğer en son satırlarda Tomcat started on port(s): 8080 gibi bir ifade görüyorsanız, bu harika bir haber! Bu, Spring Boot uygulamanızın başarıyla ayağa kalktığı ve 8080 portunda gelen istekleri dinlemeye başladığı anlamına gelir.
Adım 4: Ekrana "Merhaba DDD!" Yazdırmak
Uygulamamız çalışıyor ama henüz hiçbir iş yapmıyor. Şimdi, web tarayıcısından ulaştığımızda bize cevap veren basit bir servis (endpoint) yazalım.
- com.yazilimokulu.dddkitapprojesi paketine sağ tıklayın ve New > Package seçeneğini seçin. Paket adı olarak web yazın. Bu, web ile ilgili sınıflarımızı düzenli tutmamızı sağlar.
- Yeni oluşturduğunuz web paketine sağ tıklayın ve New > Java Class seçeneğini seçin. Sınıf adı olarak GreetingController yazıp Enter'a basın.
- Oluşturulan sınıfın içeriğini aşağıdaki gibi düzenleyin:
// Bu sınıfın bir web isteklerini karşılayan bir kontrolcü olduğunu Spring'e söylüyoruz.
@RestController
public class GreetingController {
// Web tarayıcısından http://localhost:8080/merhaba adresine bir GET isteği geldiğinde
// bu metodun çalışmasını sağlıyoruz.
@GetMapping("/merhaba")
public String sayHello() {
return "Merhaba DDD! İlk projemiz başarıyla çalışıyor!";
}
}
Not: @RestController ve @GetMapping kelimeleri kırmızı görünüyorsa, imleci üzerlerine getirip Alt + Enter (veya macOS'ta Option + Enter) tuşlarına basarak "Import class" seçeneğini seçin. IntelliJ IDEA, gerekli import ifadelerini dosyanın en üstüne ekleyecektir.
Adım 5: Sonucu Görmek
- Uygulamayı yeniden başlatın. "Run" panelindeki yeşil "Rerun" ( yeniden çalıştır) ikonuna (▶️) tıklayarak bunu yapabilirsiniz.
- Web tarayıcınızı açın ve adres çubuğuna şunu yazın:
- http://localhost:8080/merhaba
- Enter'a bastığınızda, ekranda gururla şu yazıyı görmelisiniz:
- Merhaba DDD! İlk projemiz başarıyla çalışıyor!
Tebrikler! Sadece bir geliştirme ortamı kurmakla kalmadınız, aynı zamanda çalışan, web isteklerine cevap veren bir Spring Boot uygulaması inşa ettiniz. Bu basit ama sağlam temel, kitabın geri kalanında DDD'nin Taktiksel Tasarım yapı taşlarını (Entity, Value Object, Aggregate vb.) inşa edeceğimiz tuvalimiz olacak. Artık kodun içindeki güzellikleri keşfetmeye tamamen hazırız.
Taktiksel Tasarım'ın en derinlerine inmeye başlıyoruz. Projemizin temelini attığımıza göre, şimdi iş mantığımızı ve kurallarımızı temsil edecek olan somut yapı taşlarını (building blocks) inşa etme zamanı. Bu yapı taşlarının ilki ve belki de en önemlisi Entity.
————————
Bölüm 5: DDD'nin Yapı Taşları (Building Blocks)
5.1. Entity (Varlık): Kimliği Olan Nesneler
Günlük hayatta etrafımızdaki nesneleri düşünelim. Bazı nesneleri özellikleriyle tanımlarız, bazılarını ise kim olduklarıyla. Örneğin, cebinizdeki 50 TL'lik banknot. Onu diğer 50 TL'lik banknotlardan ayıran şey seri numarasıdır, ama genellikle bizim için önemli olan değeri, yani "50 TL" olmasıdır. Ancak konu sizin kimliğiniz olduğunda, durum değişir. Adınız, saç renginiz, adresiniz zamanla değişebilir, ama sizi siz yapan, T.C. Kimlik Numaranız gibi eşsiz bir kimlik vardır. Bu kimlik, hayatınız boyunca aynı kalır.
DDD dünyasında Entity (Varlık), işte tam olarak bu ikinci tür nesnedir.
Bir Entity, özelliklerinden veya niteliklerinden bağımsız olarak, var olduğu süre boyunca sahip olduğu kesintisiz bir kimlik (identity) ile tanımlanan bir domain nesnesidir.
Entity'nin iki temel özelliği vardır:
- Eşsiz Bir Kimliği Vardır: Bu kimlik, nesne yaratıldığı andan itibaren sabittir ve onu aynı tipteki diğer tüm nesnelerden ayırır. Veritabanındaki bir primary key (birincil anahtar) gibi düşünebilirsiniz, ama aslında ondan daha fazlasıdır; iş mantığının bir parçasıdır.
- Yaşam Döngüsü Boyunca Durumu Değişebilir (Mutable): Bir Entity'nin kimliği sabitken, nitelikleri zamanla değişebilir. Tıpkı sizin adresinizin veya saç renginizin değişmesi gibi.
Örnek: Order (Sipariş) Nesnesi
Bir e-ticaret sitesindeki bir siparişi düşünelim. Bir müşteri sipariş verdiğinde, sistem bu sipariş için eşsiz bir Sipariş Numarası (orderId) üretir. Bu, o siparişin kimliğidir.
- Yaşam Döngüsü:
- Sipariş ilk oluşturulduğunda durumu PENDING (Beklemede) olabilir.
- Müşteri ödemeyi yapınca durumu PAID (Ödendi) olur.
- Depodan kargoya verilince durumu SHIPPED (Kargolandı) olur.
- Müşteriye ulaşınca DELIVERED (Teslim Edildi) olur.
Gördüğünüz gibi, siparişin durumu, içindeki ürünler, teslimat adresi gibi birçok özelliği zamanla değişebilir. Ama Sipariş Numarası asla değişmez. İki yıl sonra bile o numarayı sisteme girdiğimizde, aynı siparişe ulaşırız. İşte bu yüzden Order, klasik bir Entity'dir.
————————
Java 21 ile Kodlama: record'lar Entity Olmak İçin Uygun mu?
Bu çok önemli ve yerinde bir soru. Java 21'in record yapılarının Değer Nesneleri (Value Objects) için mükemmel olduğundan bahsetmiştik. Peki ya Entity'ler?
Cevap genellikle hayır'dır.
Çünkü record'ların temel felsefesi değiştirilemez (immutability) olmalarıdır. Bir record yaratıldığında, alanlarının değerleri bir daha asla değiştirilemez. Ancak bir Entity'nin tanımı gereği yaşam döngüsü boyunca durumu değişebilmelidir. Order örneğindeki gibi, status alanının PAID'den SHIPPED'e değişmesi gerekir. Bu, record'un doğasıyla çelişir.
Bu yüzden Entity'lerimizi modellemek için geleneksel class yapılarını kullanmaya devam edeceğiz.
Örnek Order Sınıfının İncelenmesi
Şimdi, verdiğiniz harika kod örneğini parçalarına ayıralım ve bir Entity'nin anatomisini inceleyelim:
// OrderId bir Value Object'tir, siparişin kimliğini taşır.
// Order ise bir Entity'dir, çünkü bir kimliği ve değişen bir durumu vardır.
public class Order {
// 1. Kimlik: final olarak tanımlanmış, asla değişmeyecek olan kimlik.
private final OrderId id;
// 2. Değişken Durum (Mutable State): Bu alanların değeri zamanla değişebilir.
private OrderStatus status;
private List<OrderItem> items;
private CustomerId customerId;
private Address shippingAddress;
// Constructor: Bir siparişin ilk haliyle nasıl yaratılacağını tanımlar.
// Genellikle kimlik ve zorunlu başlangıç değerlerini alır.
public Order(OrderId id, CustomerId customerId, Address shippingAddress) {
this.id = id;
this.customerId = customerId;
this.shippingAddress = shippingAddress;
this.status = OrderStatus.PENDING; // Her sipariş "Beklemede" olarak başlar.
this.items = new ArrayList<>();
}
// 3. Davranış (Behavior): Entity'nin asıl gücü buradadır.
// Bu metodlar, iş kurallarını uygular ve nesnenin durumunu kontrollü bir şekilde değiştirir.
// "setter" metodlarından (örn: setStatus()) farklı olarak, bu metodlar iş dilinde bir anlam taşır.
public void markAsPaid() {
if (this.status == OrderStatus.PENDING) {
this.status = OrderStatus.PAID;
} else {
// Zaten ödenmiş veya kargolanmış bir siparişi tekrar ödenmiş yapamazsınız.
// Bu bir iş kuralıdır!
throw new IllegalStateException("Order can only be marked as paid if it is pending.");
}
}
public void ship() {
// "Kargola" işlemi, sadece "Ödendi" durumundaki bir sipariş için geçerlidir.
// Bu da bir başka iş kuralıdır.
if (this.status == OrderStatus.PAID) {
this.status = OrderStatus.SHIPPED;
// Kargo ile ilgili bir domain event tetiklenir.
// Örneğin: OrderShippedEvent
} else {
throw new IllegalStateException("Order cannot be shipped unless it has been paid.");
}
}
// get'ler ve diğer metodlar...
public OrderId getId() {
return id;
}
public OrderStatus getStatus() {
return status;
}
}
Bu örnek, bir Entity'nin sadece veri tutan bir torba olmadığını, aynı zamanda kendi kurallarını ve davranışlarını da içinde barındıran canlı bir nesne olduğunu mükemmel bir şekilde göstermektedir. Durumu, order.setStatus(OrderStatus.SHIPPED) gibi dışarıdan keyfi bir şekilde değil, order.ship() gibi anlamlı bir iş operasyonuyla, kurallar kontrol edilerek değiştirilir. İşte Taktiksel Tasarım'ın güzelliği burada başlar.
Entity'nin ne olduğunu ve kimliğiyle nasıl var olduğunu anladık. Şimdi madalyonun diğer yüzüne, yani Taktiksel Tasarım'ın ikinci temel yapı taşı olan Value Object'e bakalım.
————————
5.2. Value Object (Değer Nesnesi): Değiştirilemez (Immutable) ve Kimliği Olmayan Nesneler
Bir önceki bölümde Entity'yi anlatırken T.C. Kimlik Numaranızdan bahsetmiştik; sizi siz yapan, eşsiz bir kimlik. Şimdi ise yine cüzdanınızdaki 50 TL'lik banknota dönelim. Bu banknotu markette harcayıp yerine başka bir 50 TL'lik banknot aldığınızda, sizin için bir şey değişir mi? Hayır. Çünkü sizin için önemli olan o kağıt parçasının seri numarası (kimliği) değil, temsil ettiği değerdir. "50 TL" değeridir.
DDD dünyasında Value Object (Değer Nesnesi), tam olarak bu tür bir kavramdır.
Bir Value Object, onu tanımlayan niteliklerin bir araya gelmesiyle anlam kazanan, kendine ait bir kimliği olmayan ve durumu değiştirilemez (immutable) olan bir nesnedir.
Value Object'lerin temel özellikleri şunlardır:
- Kimliği Yoktur, Değeri Vardır: Onları, içerdikleri niteliklerin kombinasyonu tanımlar. İki Value Object'in içindeki tüm nitelikler aynıysa, bu iki nesne birbirine eşittir ve birbirinin yerine kullanılabilir.
- Değiştirilemez (Immutable): Bir Value Object yaratıldıktan sonra içeriği asla değiştirilemez. Eğer bir değişiklik yapmanız gerekiyorsa, mevcut nesneyi değiştirmez, onun yerine istediğiniz değişikliği içeren yeni bir nesne yaratırsınız.
- Kendi Kendini Doğrular (Self-validating): Bir Value Object, yaratıldığı anda kendi kurallarını (invariants) kontrol etmeli ve geçersiz bir durumda olmasına asla izin vermemelidir. Örneğin, negatif bir tutara sahip bir Money nesnesi veya geçersiz bir e-posta formatına sahip bir EmailAddress nesnesi yaratılamamalıdır.
Örnek: Address (Adres) ve Money (Para)
- Address: Bir adres; şehir, ilçe, sokak ve kapı numarası gibi bilgilerin bir bütünüdür. İki adresin tüm bu bilgileri aynıysa, o iki adres aynıdır. Bir adresin sokağını "değiştirmek" istemeyiz; bunun yerine o sokak bilgisiyle yeni bir Address nesnesi yaratırız.
- Money: Para, miktar ve para birimi'nin birleşimidir. "100" ve "TL" bir araya geldiğinde bir anlam ifade eder. İki tane "100 TL" nesnesi arasında fark yoktur, ikisi de aynı değeri temsil eder.
Bu yaklaşım, kodumuzdaki belirsizlikleri ortadan kaldırır. Artık bir metodun String tipinde bir email alması yerine, EmailAddress tipinde bir email almasını sağlayabiliriz. Bu sayede o metoda gelen e-postanın formatının kesinlikle doğru olduğundan emin oluruz, çünkü geçersiz bir EmailAddress nesnesi zaten en başta yaratılamaz!
————————
Java 21 record ile Mükemmel Uyum
Entity'leri record ile yapmanın neden kötü bir fikir olduğunu konuşmuştuk. Ancak Value Object'ler için durum tam tersi. Java 21'in record yapısı, sanki DDD'deki Value Object tanımını okuyup ona göre tasarlanmış gibidir.
- record'lar doğaları gereği değiştirilemezdir (immutable).
- equals() ve hashCode() metodlarını, tüm alanları karşılaştıracak şekilde otomatik olarak üretirler. Bu, tam da Value Object'lerin "yapısal eşitlik" tanımına uyar.
- Az ve öz sözdizimi (syntax) sayesinde kodumuzu inanılmaz derecede temizlerler.
Örnek Money Sınıfının İncelenmesi
Şimdi, verdiğiniz mükemmel Money örneğini inceleyerek bir Value Object'in anatomisini görelim:
// Para birimi ve miktarını bir arada tutan, değiştirilemez bir Değer Nesnesi.
// "record" anahtar kelimesi, bunun bir Value Object olduğunu bas bas bağırır.
public record Money(BigDecimal amount, Currency currency) {
// 1. Kendi Kendini Doğrulama (Self-validation):
// Bu, Java record'larındaki "compact constructor" özelliğidir.
// Nesne yaratılırken, ana constructor çağrılmadan hemen önce çalışır.
// Burada, bir paranın negatif bir miktara sahip olamayacağı kuralını (invariant) uyguluyoruz.
public Money {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be null or negative");
}
if (currency == null) {
throw new IllegalArgumentException("Currency cannot be null");
}
}
// 2. Değiştirilemezlik ile Davranış:
// Bu metod, mevcut Money nesnesini DEĞİŞTİRMEZ.
// Bunun yerine, toplama işleminin sonucunu içeren YENİ bir Money nesnesi yaratır ve döndürür.
public Money add(Money other) {
// Farklı para birimlerinin toplanmasını engelleyen bir iş kuralı.
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add money with different currencies");
}
// "this.amount = this.amount.add(other.amount)" gibi bir şey ASLA YAPILMAZ.
// Çünkü record'un alanları final'dır ve değiştirilemez.
return new Money(this.amount.add(other.amount), this.currency);
}
}
Bu yapının bize kazandırdıkları:
- Güvenlik: Artık sistemimizin hiçbir yerinde negatif değere sahip bir Money nesnesi olamayacağından %100 eminiz.
- Anlaşılırlık: public void makePayment(BigDecimal price, String currency) gibi bir metot imzası yerine, public void makePayment(Money amount) gibi çok daha anlamlı ve güvenli bir imzamız olur.
- Yan Etkilerin (Side Effects) Önlenmesi: Bir Money nesnesini bir metoda parametre olarak geçtiğimizde, o metodun nesnenin içini gizlice değiştireceğinden asla korkmamıza gerek kalmaz, çünkü bu teknik olarak imkansızdır.
Entity'ler sistemimizin iskeletiyse, Value Object'ler de bu iskeleti oluşturan sağlam, güvenilir ve tutarlı hücrelerdir. Bu ikiliyi doğru kullanmak, temiz ve sürdürülebilir bir domain modelinin temelini oluşturur.
Taktiksel Tasarım'ın en önemli ve güçlü konseptlerinden birine geldik. Entity ve Value Object gibi yapı taşlarını öğrendik. Peki bu taşları nasıl bir araya getirip anlamlı, tutarlı ve sağlam yapılar inşa edeceğiz? İşte bu sorunun cevabı Aggregate (Küme).
————————
5.3. Aggregate (Küme): İş Kurallarının ve Tutarlılığın Kalesi
Bir an için modern bir araba düşünün. Arabanın içinde motor, şanzıman, tekerlekler, fren sistemi gibi yüzlerce parça bulunur. Bu parçaların hepsi birbiriyle uyum içinde çalışmak zorundadır. Gaza bastığınızda motorun devri artmalı, şanzıman doğru vitese geçmeli ve tekerlekler dönmelidir. Frene bastığınızda ise fren balataları diskleri sıkmalıdır.
Şimdi düşünün, arabanın motorunu kontrol etmek için motorun içindeki her bir pistonu, her bir valfi ayrı ayrı mı yönetirsiniz? Hayır. Sizin için tek bir iletişim noktası vardır: gaz pedalı. Gaza basarsınız ve "araba" bir bütün olarak hızlanır. Fren yapmak için fren balatalarını elle sıkmazsınız; fren pedalına basarsınız ve "araba" bir bütün olarak yavaşlar.
DDD'de Aggregate (Küme), tıpkı bu araba gibi, birbiriyle ilişkili ve birlikte bir anlam ifade eden nesnelerin oluşturduğu bir gruptur. Bu grubun amacı, bir işlem sırasında değişmesi gereken tüm nesneleri bir araya toplayarak iş kurallarının (invariants) ve veri tutarlılığının her zaman korunmasını sağlamaktır.
Aggregate Root Nedir? Kümenin Dış Dünya ile Tek İletişim Noktası
Arabamızdaki gaz pedalı veya fren pedalı ne işe yarıyordu? Motorun veya fren sisteminin karmaşıklığını bizden gizleyip, bize tek ve basit bir arayüz sunuyordu.
Aggregate Root (Küme Kökü), kümenin içindeki nesnelerden biridir ve o kümenin dış dünya ile olan tek iletişim kapısıdır. Tıpkı bir kalenin sadece dışarıya açılan ana kapısı gibi. Kalenin içindeki herhangi bir yere ulaşmak istiyorsanız, o kapıdan geçmek zorundasınız.
Bu bize şu iki altın kuralı verir:
- Kural 1: Dışarıdaki bir nesne, sadece ve sadece Aggregate Root'a referans tutabilir. Kümenin içindeki diğer nesnelere doğrudan erişim kesinlikle yasaktır.
- Kural 2: Küme içindeki herhangi bir nesneyi değiştirmek istiyorsanız, komutu doğrudan o nesneye değil, Aggregate Root'a vermelisiniz. Root, bu komutun kümenin tutarlılığını bozup bozmadığını kontrol ettikten sonra değişikliği uygular.
Bu kurallar, Aggregate'i adeta iş kurallarının ve tutarlılığın korunduğu bir kale veya işlem sınırı (transaction boundary) haline getirir.
————————
Örnek: Order (Sipariş) Aggregate'i
E-ticaret sistemimize geri dönelim. Bir Order (Sipariş) Entitysi ve ona bağlı OrderItem (Sipariş Kalemi) Entityleri vardır.
- Bir OrderItem, tek başına var olamaz. Mutlaka bir Order'a aittir.
- Bir Order'a yeni bir OrderItem eklendiğinde, Order'ın toplam tutarı (totalAmount) güncellenmelidir.
- Bir Order'ın durumu "Kargolandı" (SHIPPED) ise, o siparişe yeni bir ürün eklenememelidir.
Gördüğünüz gibi, Order ve OrderItem'lar arasında çok sıkı bir tutarlılık ilişkisi var. İşte bu yüzden bu ikisi birlikte bir Aggregate oluşturur.
Peki bu kümenin Aggregate Root'u hangisidir? Elbette, her şeyin sahibi olan Order!
Kötü Yaklaşım (Doğrudan Erişim):
Eğer kurallara uymasaydık ve OrderItem listesine doğrudan erişim izni verseydik ne olurdu?
// SAKIN BÖYLE YAPMAYIN!
Order myOrder = orderRepository.findById(orderId);
List<OrderItem> items = myOrder.getItems(); // Listenin referansını dışarıya sızdırdık!
items.add(new OrderItem(...)); // TUTARLILIK BOZULDU! Siparişin toplam tutarı güncellenmedi!
Bu kod, Aggregate'in en temel amacını yok eder. İş kuralını (toplam tutarın güncellenmesi) atlamış olduk.
Doğru Yaklaşım (Aggregate Root Üzerinden Erişim):
Aggregate Root olan Order sınıfı, bu tür tehlikeli işlemleri engellemek için kendi davranışlarını (metotlarını) sunar.
// Order, Aggregate Root'umuz
public class Order {
private final OrderId id;
private final List<OrderItem> items;
private Money totalAmount;
private OrderStatus status;
public Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.items = new ArrayList<>();
this.totalAmount = Money.ZERO; // Başlangıç değeri
this.status = OrderStatus.PENDING;
}
// DAVRANIŞ: Bu metot, kalenin ana kapısıdır.
// Dış dünya sadece bu metodu çağırabilir.
public void addItem(Product product, int quantity) {
// KURAL 1: Kargolanmış bir siparişe ürün eklenemez.
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("Cannot add item to an order that is not pending.");
}
// KURAL 2: Toplam sipariş tutarı 20.000 TL'yi geçemez.
Money newItemPrice = product.getPrice().multiply(quantity);
if (this.totalAmount.add(newItemPrice).isGreaterThan(new Money(20000, "TRY"))) {
throw new BusinessRuleException("Order total amount cannot exceed 20,000 TRY.");
}
// Tüm kurallar geçerliyse, değişikliği uygula.
// Hem item listesi hem de toplam tutar aynı anda, tutarlı bir şekilde güncellenir.
this.items.add(new OrderItem(product.getId(), newItemPrice, quantity));
this.totalAmount = this.totalAmount.add(newItemPrice);
}
// ÖNEMLİ: Dışarıya listenin kendisini değil, değiştirilemez bir kopyasını veriyoruz.
// Bu sayede kimse myOrder.getItems().add() yapamaz!
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
// ... diğer metodlar ...
}
Sonuç:
Artık bir siparişe ürün eklemek isteyen kod, bunu sadece tek bir güvenli yolla yapabilir:
Order myOrder = orderRepository.findById(orderId);
Product newProduct = productRepository.findById(productId);
myOrder.addItem(newProduct, 2); // Tüm iş kuralları ve tutarlılık içeride korunur.
orderRepository.save(myOrder); // Tüm aggregate tek bir birim olarak kaydedilir.
Aggregate, Taktiksel Tasarım'ın en güçlü desenidir. Karmaşıklığı sınırlar, iş kurallarını tek bir sorumluya emanet eder ve verinizin her zaman tutarlı kalmasını garanti altına alır. O, sizin iş mantığınızın koruyucu kalesidir.
Şimdi de Taktiksel Tasarım'ın en kritik köprülerinden birine geldik. Aggregate'lerimizi, yani iş kurallarının koruyucu kalelerini tasarladık. Bu nesneler şu an sadece bellekte (memory) yaşıyor. Uygulamayı kapattığımızda hepsi yok olacak. Peki, bu değerli Aggregate'lerimizi kalıcı olarak nasıl saklayacağız ve ihtiyaç duyduğumuzda onlara nasıl geri ulaşacağız?
Bu sorunun cevabı Repository (Depo) deseninde saklı.
————————
5.4. Repository (Depo): Aggregate'leri Kalıcı Hale Getirme ve Geri Getirme Sanatı
Domain modelimizi (Entity, Value Object, Aggregate) tasarlarken, veritabanı, SQL sorguları, INSERT, UPDATE gibi teknik detayları hiç düşündük mü? Hayır. Çünkü DDD'nin amacı, önce iş problemini temiz bir şekilde modellemektir. İşte Repository, bu temiz domain modelimiz ile veritabanı gibi altyapı detayları arasına çekilmiş kalın bir duvardır.
Repository, domain nesnelerimize sanki bellekte yaşayan bir koleksiyonmuş gibi davranmamızı sağlayan bir arayüzdür. Arka planda verilerin bir veritabanında mı, bir dosyada mı, yoksa bir bulut servisinde mi saklandığını tamamen gizler. Bu sayede domain katmanımız, altyapı karmaşasından kirlenmemiş, saf ve temiz kalır.
Repository'nin temel görevleri ve kuralları şunlardır:
- Altyapıyı Soyutlar: Domain katmanı, JdbcTemplate, EntityManager veya SQL sorguları gibi detayları asla bilmez. Sadece "Bu Order'ı kaydet" (save) veya "Bana şu kimliğe sahip Order'ı bul" (findById) gibi basit komutlar verir.
- Aggregate'lerle Çalışır: Bu en önemli kuraldır. Her Aggregate için sadece bir Repository olur. Order Aggregate'imiz için bir OrderRepository'miz olur. Ama OrderItem için ayrı bir OrderItemRepository olmaz! Neden? Çünkü OrderItem'ın yaşam döngüsü Order'a bağlıdır. Bir OrderItem'ı kaydetmek veya silmek istiyorsak, bunu her zaman Aggregate Root olan Order üzerinden yapmalıyız.
- Bir Koleksiyonu Taklit Eder: Repository arayüzü, add, remove, findById gibi basit koleksiyon metodlarına benzer isimler kullanır. Bu, geliştiricinin veritabanını değil, nesnelerden oluşan bir koleksiyonu yönettiği hissini güçlendirir.
————————
Spring Data JPA ile Implementasyon: Sihrin Gerçekleştiği Yer
Teoride Repository, domain katmanında tanımlanan bir arayüzdür ve altyapı katmanında implemente edilir. Ancak Spring Data JPA, bu deseni o kadar zarif bir şekilde uygular ki, bizim neredeyse hiç implementasyon kodu yazmamıza gerek kalmaz.
Şimdi verdiğiniz mükemmel örneği inceleyelim:
// 1. @Repository anotasyonu: Bu arayüzün bir Spring bileşeni olduğunu ve
// veritabanı istisnalarını (exceptions) Spring'in kendi hiyerarşisine
// çevirmesi gerektiğini belirtir.
@Repository
// 2. JpaRepository'yi extend etmek: İşte sihir burada başlıyor.
// JpaRepository<Order, OrderId> diyerek Spring'e şunu söylüyoruz:
// "Bu, 'Order' Aggregate'i için bir Repository'dir.
// Ve bu Aggregate'in kimlik (ID) tipi 'OrderId'dir."
public interface OrderRepository extends JpaRepository<Order, OrderId> {
// Spring Data JPA, JpaRepository'yi extend ettiğimiz için
// save(), findById(), delete(), findAll() gibi temel CRUD (Create, Read, Update, Delete)
// metodlarını bizim için OTOMATİK OLARAK implemente eder.
// Bizim bunları tekrar yazmamıza gerek yoktur!
// Not: Normalde JpaRepository'de bu metodlar zaten olduğu için
// aşağıdaki iki satırı yazmak zorunda bile değiliz.
// Ancak kodun okunabilirliğini artırmak için veya daha karmaşık
// sorgular eklemek istediğimizde bu şekilde belirtebiliriz.
// "Bu kimliğe sahip Order nesnesini bul ve bir Optional içinde geri dön."
// Optional, nesnenin bulunamama ihtimaline karşı kodumuzu daha güvenli hale getirir.
Optional<Order> findById(OrderId orderId);
// "Bu Order Aggregate'ini (içindeki tüm OrderItem'larla birlikte) veritabanına kaydet."
// Eğer Order yeni ise INSERT, mevcut ise UPDATE işlemini arka planda kendisi yapar.
void save(Order order);
}
Nasıl Kullanılır?
Artık bir Application Service (Uygulama Servisi) içinde bu Repository'yi kullanarak Aggregate'lerimizi yönetebiliriz:
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository; // Başka bir Aggregate'in Repository'si
// Constructor Injection ile Repository'yi servise dahil ediyoruz.
public OrderApplicationService(OrderRepository orderRepository, ProductRepository productRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
public void addProductToOrder(OrderId orderId, ProductId productId, int quantity) {
// 1. Aggregate'i Geri Getirme:
// Repository'yi kullanarak, işlem yapmak istediğimiz Order'ı buluyoruz.
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
// 2. Domain Mantığını Çalıştırma:
// Veritabanı ile ilgili hiçbir şey düşünmeden, sadece domain nesnesiyle konuşuyoruz.
order.addItem(product, quantity);
// 3. Aggregate'i Kalıcı Hale Getirme:
// Değişiklik yapılmış olan Order Aggregate'ini tek bir hamlede kaydediyoruz.
// Spring Data JPA, arka planda hem Order hem de OrderItem tablolarını güncelleyecektir.
orderRepository.save(order);
}
}
Gördüğünüz gibi, Repository deseni ve Spring Data JPA sayesinde servis katmanımız, veritabanı detaylarından tamamen soyutlanmış, temiz ve sadece iş mantığına odaklanmış bir hale gelir. Bu, DDD'nin en büyük kazanımlarından biridir.
Taktiksel Tasarım'ın bir diğer önemli yapı taşı olan Factory (Fabrika) desenine geldik. Şimdiye kadar Entity ve Aggregate'lerimizi genellikle new Order(...) gibi constructor'lar (yapıcı metotlar) ile oluşturduk. Peki ya bir nesneyi oluşturma süreci, sadece birkaç parametreyi alanlara atamaktan çok daha karmaşıksa?
————————
5.5. Factory (Fabrika): Karmaşık Nesne Oluşturma Mantığını Gizlemek
Bir araba fabrikasını gözünüzün önüne getirin. Bir araba, sadece şasi, motor ve tekerleklerin bir araya getirilmesiyle mi oluşur? Hayır. Üretim süreci yüzlerce adımdan oluşur: şasinin preslenmesi, parçaların birleştirilmesi, boyama, motorun montajı, elektronik sistemlerin kurulması... Bu karmaşık süreci arabanın constructor'ına yüklemek, onu devasa ve anlaşılmaz bir hale getirirdi.
Bunun yerine ne yaparız? Tüm bu karmaşık montaj sürecini, adına fabrika dediğimiz bir tesisin duvarları arkasına gizleriz. Biz fabrikaya sadece "bana kırmızı, spor model bir araba üret" deriz. Fabrika, tüm o karmaşık adımları kendi içinde halleder ve bize anahtarı teslim eder.
DDD'de Factory (Fabrika), tam olarak bu işi yapar. Bir Entity veya Aggregate'in oluşturulma sürecinin karmaşıklığını, bu işe adanmış ayrı bir nesnenin veya metodun arkasına gizleyen bir desendir. Amacı, Aggregate veya Entity'nin kendisini, nasıl yaratıldığına dair karmaşık detaylardan arındırmak ve bu süreci merkezileştirmektir.
Ne Zaman Bir Factory'ye İhtiyaç Duyarız?
Constructor'lar basit nesne yaratma işlemleri için harikadır. Ancak aşağıdaki durumlarda bir Factory kullanmak hayat kurtarır:
- Oluşturma Süreci Karmaşık Olduğunda: Nesneyi yaratmak için veritabanından başka veriler çekmek, başka servisleri çağırmak veya karmaşık algoritmalar çalıştırmak gerekiyorsa.
- Farklı "Tariflere" Göre Nesne Yaratılıyorsa: Aynı nesnenin farklı başlangıç koşulları veya türleri varsa. Örneğin, "Standart Müşteri", "Gold Müşteri", "Platin Müşteri" gibi. Bunların hepsini tek bir constructor içine if-else bloklarıyla doldurmak kodu çirkinleştirir.
- İnşa Ettiğimiz Nesnenin Tipini Saklamak İstediğimizde: Bazen bir fabrika, verdiğimiz parametrelere göre bir arayüzün (interface) farklı implementasyonlarını geri dönebilir ve istemci (client) kod, hangi somut sınıfın yaratıldığını bilmek zorunda kalmaz.
Örnek: 2025 Model Bir SaaS Uygulaması İçin Kullanıcı Oluşturma
2025 yılında çok popüler olan, proje yönetimi için "freemium" model (temel özellikler bedava, gelişmiş özellikler ücretli) sunan bir SaaS (Software as a Service) platformu geliştirdiğimizi düşünelim. Yeni bir kullanıcı sisteme kaydolduğunda, sadece veritabanına bir User satırı eklemek yeterli değildir. Şunlar da yapılmalıdır:
- Kullanıcıya eşsiz bir kimlik (UserId) atanmalıdır.
- Varsayılan olarak "FREE" planı tanımlanmalıdır.
- 14 günlük "PRO" plan deneme süresi başlatılmalıdır.
- Kullanıcıya yardımcı olmak için "İlk Projem" adında bir başlangıç projesi otomatik olarak oluşturulmalıdır.
Tüm bu mantığı User sınıfının constructor'ına yüklemek, onu başka sorumluluklarla kirletirdi. User sınıfının görevi bir kullanıcıyı temsil etmektir, "başlangıç projesi" yaratmanın detaylarını bilmek değil.
İşte burada bir UserFactory devreye girer.
// UserFactory, kullanıcı yaratma karmaşıklığını üstlenir.
// Genellikle bir domain servisi olarak tasarlanır.
@Component // Bu sınıfın bir Spring bileşeni olduğunu belirtiyoruz.
public class UserFactory {
// Fabrikamızın başka servislere veya repository'lere ihtiyacı olabilir.
private final ProjectRepository projectRepository;
public UserFactory(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
// İŞTE FABRİKA METODUMUZ!
// Amacını çok net bir şekilde anlatıyor: "Yeni bir freemium kullanıcı yarat".
// Karmaşık detaylar istemci koddan gizlenmiş durumda.
public User createNewFreemiumUser(String username, String email, String plainPassword) {
// 1. Temel kullanıcı nesnesini oluştur.
UserId newUserId = UserId.generate(); // Eşsiz bir kimlik üretildi.
// Şifre hash'leme gibi detaylar User entity'sinin içinde olabilir.
User newUser = new User(newUserId, username, email, plainPassword);
// 2. Varsayılan planı ata ve deneme süresini başlat.
// Bu mantık User entity'sinin bir metodu olabilir.
newUser.startProTrial(LocalDate.now().plusDays(14));
// 3. Başlangıç projesini yarat. Bu, User'ın sorumluluğu değil!
// Bu yüzden bu mantık Factory'de yer alıyor.
Project welcomeProject = new Project(ProjectId.generate(), "İlk Projem", newUserId);
// Bu projeyi veritabanına kaydetmemiz gerekebilir.
projectRepository.save(welcomeProject);
// 4. Tamamen yapılandırılmış, tutarlı bir User Aggregate'ini geri dön.
return newUser;
}
}
Factory'yi Servis Katmanında Kullanmak
Artık Application Service'imiz, kullanıcı yaratmanın karmaşık detaylarını bilmek zorunda değil. Sadece fabrikadan bir nesne "sipariş eder".
@Service
public class UserApplicationService {
private final UserRepository userRepository;
private final UserFactory userFactory; // Fabrikamızı buraya enjekte ediyoruz.
public UserApplicationService(UserRepository userRepository, UserFactory userFactory) {
this.userRepository = userRepository;
this.userFactory = userFactory;
}
public UserId registerNewUser(String username, String email, String password) {
// İstemci kod ne kadar temiz ve okunur!
// Karmaşıklık tamamen gizlendi.
// 1. Fabrikadan yeni kullanıcıyı "sipariş et".
User newUser = userFactory.createNewFreemiumUser(username, email, password);
// 2. Oluşturulan Aggregate'i kaydet.
userRepository.save(newUser);
return newUser.getId();
}
}
Kısacası Factory, bir nesnenin yaşamına nasıl başladığına dair karmaşık hikayeyi, o nesnenin kendisinden ve onu kullanacak olan istemci kodlardan soyutlayarak Taktiksel Tasarım'ın en önemli prensiplerinden biri olan sorumlulukların ayrımını (separation of concerns) mükemmel bir şekilde uygular.
Taktiksel Tasarım'ın temel yapı taşlarını neredeyse tamamladık. Entity, Value Object, Aggregate ve Factory gibi kavramlarla nesnelerimizi ve onların yaşam döngülerini modeledik.
Peki ya yapmak istediğimiz bir iş, tek bir nesneye ait değilse? Bir Entity veya Value Object'in doğal bir sorumluluğu gibi durmuyorsa? İşte bu "evsiz" kalan operasyonlar için özel bir yapı taşımız var: Domain Service (Alan Servisi).
————————
5.6. Service (Servis): Hiçbir Entity veya Value Object'e Ait Olmayan Domain Operasyonları
Şimdiye kadar, iş mantığını ve davranışları doğrudan ilgili Entity veya Aggregate'in içine koymanın önemini vurguladık. Bir siparişi kargolama işlemi (order.ship()) Order Aggregate'ine aittir. Bir parayı diğerine ekleme işlemi (money.add()) Money Value Object'ine aittir. Bu, nesnelerimizi yetenekli ve anlamlı kılar.
Ancak bazen bir operasyon, birden fazla, birbirinden bağımsız Aggregate veya Entity'yi içerir. Bu durumda bu operasyonun sorumluluğunu hangi nesneye yükleyeceğiz?
- Örnek: Bankacılık sisteminde bir hesaptan (Account) başka bir hesaba para transferi yapma işlemini düşünelim.
- Bu işlemi AccountA.transferTo(AccountB, amount) şeklinde mi yapmalıyız? Bu durumda AccountA, AccountB'nin iç yapısını (örneğin deposit metodu) bilmek zorunda kalır. Bu, nesneler arasındaki bağımlılığı artırır.
- Ya da AccountB.receiveFrom(AccountA, amount) şeklinde mi? Bu da aynı soruna yol açar.
Para transferi işlemi, ne tek başına AccountA'ya ne de tek başına AccountB'ye aittir. Bu, ikisini de kapsayan, daha üst seviye bir domain sürecidir.
İşte Domain Service, bu tür "evsiz" domain operasyonlarını barındırmak için vardır.
Bir Domain Service'in Temel Özellikleri:
- Operasyonu Temsil Eder, Durum Tutmaz (Stateless): En önemli özelliği budur. Bir Domain Service'in içinde Entity gibi alanlar (fields) bulunmaz. Sadece bir metodu vardır, bu metot gerekli parametreleri alır, işini yapar ve bir sonuç döner. Her çağrıldığında aynı girdilerle aynı sonucu üretir.
- Adı, Evrensel Dil'in (Ubiquitous Language) Bir Parçasıdır: Servisin ve metodunun adı, iş uzmanlarının da anlayacağı ve kullanacağı bir iş terimi olmalıdır. DataProcessor gibi genel bir isim yerine, FundTransferService (Fon Transfer Servisi) gibi özel ve anlamlı bir isim kullanılır.
- Diğer Domain Nesneleri Üzerinde İşlem Yapar: Parametre olarak Entity veya Value Object'ler alır ve onları orkestra şefi gibi yöneterek operasyonu gerçekleştirir.
Örnek: FundTransferService (Fon Transfer Servisi)
Para transferi örneğimizi bir Domain Service ile modelleyelim.
// Bu bir Entity değil, stateless bir servistir.
// Adı, iş dilinden gelir.
@Service // veya @Component
public class FundTransferService {
// Servis, kendi içinde bir state tutmaz.
// Gerekli olan her şeyi metod parametreleri ile alır.
/**
* İki hesap arasında para transferi işlemini gerçekleştiren domain operasyonu.
* @param sourceAccount Kaynak Hesap Aggregate'i
* @param destinationAccount Hedef Hesap Aggregate'i
* @param amount Aktarılacak miktar (Value Object)
* @throws InsufficientFundsException Yetersiz bakiye durumunda
*/
public void transfer(Account sourceAccount, Account destinationAccount, Money amount) {
// 1. İş kuralını kontrol et: Kaynak hesabın bakiyesi yeterli mi?
// Bu mantık, Account Aggregate'inin kendi içinde yer alır.
sourceAccount.ensureSufficientFunds(amount);
// 2. Operasyonu gerçekleştir: Her Aggregate kendi sorumluluğunu yerine getirir.
// Servis, bu adımları koordine eder.
sourceAccount.withdraw(amount);
destinationAccount.deposit(amount);
// Not: Burada bir Domain Event (örn: FundsTransferredEvent) de tetiklenebilir.
}
}
Domain Service vs. Application Service: Önemli Bir Ayrım
Yeni başlayanların sıkça karıştırdığı bir konudur. İkisi de "Service" kelimesini içerir ama görevleri tamamen farklıdır.
- Application Service (Uygulama Servisi): Dış dünyanın (örneğin bir Web Controller'ının) domain'imizle konuştuğu ilk kapıdır. İşlem akışını (workflow) yönetir. Güvenlik, transaction yönetimi gibi teknik işlerle ilgilenir. Repository'leri kullanarak Aggregate'leri bulur, Domain Service'leri çağırır ve sonra Aggregate'i tekrar Repository'ye kaydeder. "Ne yapılacağını" söyler.
- Domain Service (Alan Servisi): Saf iş mantığı içerir. Hiçbir teknik detayı (transaction, security, HTTP isteği vb.) bilmez. Sadece ve sadece domain kurallarını uygular. "İşin nasıl yapılacağını" bilir.
Gelin para transferi örneğimizin tam akışına bakalım:
// BU BİR APPLICATION SERVICE'TİR
@Service
public class AccountApplicationService {
private final AccountRepository accountRepository;
private final FundTransferService fundTransferService; // Domain Service'i enjekte ediyoruz
// Constructor...
// Bu metot, bir web controller tarafından çağrılabilir.
@Transactional // Transaction yönetimi Application Service'in sorumluluğudur.
public void transferFunds(AccountId fromAccountId, AccountId toAccountId, Money amount) {
// 1. Aggregate'leri Repository'den yükle
Account source = accountRepository.findById(fromAccountId).orElseThrow(...);
Account destination = accountRepository.findById(toAccountId).orElseThrow(...);
// 2. Domain mantığını bir Domain Service'e devret
fundTransferService.transfer(source, destination, amount);
// 3. Değişen Aggregate'leri kaydet
accountRepository.save(source);
accountRepository.save(destination);
}
}
Özetle, bir iş kuralı veya operasyon, tek bir Entity veya Value Object'in doğal bir sorumluluğu değilse, onu ortada "evsiz" bırakmayın. Bu işi, adı iş dilinden gelen, durumu olmayan (stateless) temiz bir Domain Service'e emanet edin.
Şimdiye kadar Taktiksel Tasarım'ın yapı taşlarını (Entity, Aggregate, Repository vb.) öğrendik. Peki bu yapı taşlarını projemizde nereye koyacağız? Kodumuzu nasıl organize edeceğiz ki yıllar sonra bile temiz, anlaşılır ve esnek kalsın? Bu bölümde, geleneksel mimari anlayışlarının ötesine geçerek DDD'nin ruhuna en uygun mimari desenlerden birini, Hexagonal Architecture'ı (Altıgen Mimari) tanıyacağız.
————————
6.1. Klasik N-Tier Mimarinin Ötesi: Hexagonal Architecture (Ports & Adapters)
Çoğumuzun kariyerinin bir noktasında karşılaştığı klasik bir mimari vardır: 3-Katmanlı Mimari (3-Tier Architecture). Genellikle şöyle görünür:
- Presentation Layer (Sunum Katmanı): Kullanıcının gördüğü ekranlar, REST API endpoint'leri.
- Business Logic Layer (İş Mantığı Katmanı): İş kurallarının bulunduğu servisler.
- Data Access Layer (Veri Erişim Katmanı): Veritabanıyla konuşan kodlar (DAO'lar, Repository'ler).
Bu mimarideki en büyük sorun, bağımlılıkların hep tek bir yönde, genellikle aşağıya doğru olmasıdır. Sunum katmanı iş mantığına, iş mantığı da veri erişim katmanına bağımlıdır. Bu durum, farkında olmadan veritabanının projenin merkezine yerleşmesine neden olur. İş mantığı katmanındaki servisler, veritabanı tablolarının yapısına göre şekillenmeye başlar. Bu, DDD'nin "önce domain, sonra altyapı" felsefesine tamamen aykırıdır.
Sorun Ne? Kalbin Yanlış Yerde Atması
Geleneksel katmanlı mimaride, iş mantığımız (kalbimiz), veritabanı gibi dış bir dünyaya sıkı sıkıya bağlıdır. Veritabanını değiştirmek (örneğin Oracle'dan PostgreSQL'e geçmek) istediğimizde, bu değişiklik dalga dalga iş mantığı katmanına kadar yayılır ve büyük bir maliyet çıkarır.
Hexagonal Architecture (aynı zamanda Ports & Adapters olarak da bilinir) bu problemi kökünden çözer. Bize şunu söyler:
"Uygulamanızın kalbi, yani saf iş mantığınız (domain), dış dünyanın hiçbir detayını bilmemelidir. Ne bir veritabanını, ne bir web framework'ünü, ne de bir mesajlaşma kuyruğunu... Hiçbirini!"
Hexagonal Architecture: Kalbi Korumak
Bu mimariyi bir bilgisayarın kasası gibi düşünebilirsiniz. Kasanın içinde en değerli parçalar vardır: işlemci, anakart, RAM... Bunlar bilgisayarın "kalbidir". Kasanın dışında ise bu kalple iletişim kurmak için standart girişler bulunur: USB portları, HDMI portu, Ethernet portu...
- Siz bu USB portuna bir klavye mi, bir fare mi, yoksa bir flash bellek mi takacağınızı bilgisayarın işlemcisi bilmek zorunda değildir. İşlemci sadece USB portundan gelen standart sinyalleri anlar.
- Klavyenin, farenin veya flash belleğin kendisi ise birer adaptördür. Her biri kendi iç mantığını (klavye tuş sinyali, fare imleç hareketi vb.) standart bir USB sinyaline çevirerek porta iletir.
Hexagonal Architecture'da da durum tam olarak budur:
- Hexagon (Altıgen - Uygulamanın Kalbi): Burası bizim saf domain ve uygulama mantığımızın yaşadığı yerdir. İçinde Aggregate'lerimiz, Value Object'lerimiz, Domain Service'lerimiz ve Application Service'lerimiz bulunur. Bu kalp, dış dünyadan tamamen habersizdir.
- Ports (Portlar - Kalbin Arayüzleri): Bunlar, kalbin dış dünya ile konuşmak için tanımladığı "standart giriş/çıkış" kapılarıdır. Teknik olarak bunlar bizim yazdığımız interface'lerdir. İki tür port vardır:
- Driving Ports (Sürücü Portlar): Dış dünyanın bizim uygulamamızı çağırmak için kullandığı kapılardır. Örneğin, bir OrderApplicationService arayüzü bir sürücü porttur.
- Driven Ports (Sürülen Portlar): Bizim uygulamamızın dış dünyadan bir şey istemek için kullandığı kapılardır. Örneğin, "bana bu siparişi kaydet" demek için tanımladığımız OrderRepository arayüzü bir sürülen porttur.
- Adapters (Adaptörler - Dış Dünya Tercümanları): Bunlar, portların somut implementasyonlarıdır ve altyapı katmanında yaşarlar. Dış dünyanın teknolojisini, bizim kalbimizin anladığı dile çevirirler.
- Driving Adapters (Sürücü Adaptörler): Dışarıdan gelen istekleri bizim portlarımıza yönlendirir. Örneğin, bir @RestController, HTTP isteğini alır ve ilgili OrderApplicationService portunu (metodunu) çağırır.
- Driven Adapters (Sürülen Adaptörler): Bizim portlarımızın isteklerini alıp belirli bir teknolojiyle yerine getirir. Örneğin, OrderRepository portunu (arayüzünü) implemente eden ve JPA kullanarak veritabanına kayıt yapan JpaOrderRepository sınıfı bir sürülen adaptördür.
En Önemli Kural: Bağımlılıkların Yönü
Hexagonal Architecture'daki en kritik kural Bağımlılıkların Tersine Çevrilmesi Prensibi'dir (Dependency Inversion Principle). Tüm bağımlılık okları, dış dünyadan (Adapters) içeriye, yani kalbe (Hexagon) doğrudur.
- JpaOrderRepository (Adaptör), OrderRepository (Port) arayüzünü bildiği için ona bağımlıdır.
- Ancak OrderApplicationService (Kalp), sadece OrderRepository arayüzünü (Port) bilir. Arka planda JPA mi, MongoDB mi, yoksa başka bir teknoloji mi çalıştığından haberi yoktur ve umursamaz.
Sonuç: Bu mimari sayesinde, uygulamanızın kalbi olan domain mantığınız, teknolojik değişikliklerden tamamen izole hale gelir. Yarın veritabanını değiştirmek veya REST API yerine gRPC kullanmak istediğinizde, sadece ilgili adaptörü değiştirmeniz yeterli olur. Kalbe dokunmanıza gerek kalmaz. Bu, test edilebilirliği inanılmaz artırır, esneklik sağlar ve projenizin yıllara meydan okumasına olanak tanır.
Bir önceki bölümde Hexagonal Architecture'ın felsefesini, yani uygulamamızın kalbini dış dünyadan nasıl izole edeceğimizi öğrendik. Şimdi bu felsefeyi, projemizin klasör yapısına ve kod organizasyonuna nasıl yansıtacağımızı görelim.
Hexagonal mimariyi uygularken, kodu genellikle üç ana mantıksal katmana veya pakete ayırırız. Bu katmanlar, geleneksel N-Tier mimarideki gibi birbirine sıkı sıkıya bağlı değildir; aksine, aralarındaki bağımlılık okları her zaman içeriye, yani kalbe doğrudur.
————————
6.2. Katmanlar: Projemizin İç Düzeni
Projemizi, bir soğanın katmanları gibi düşünebiliriz. En dışta teknolojiye bağımlı, kolayca değiştirilebilen kabuklar; en içte ise değerli, saf ve teknoloji-bağımsız olan öz, yani domain mantığı yer alır.
1. Domain Layer (Domain Katmanı): En İçteki Kalp ❤️
Bu katman, projenizin evrenidir. Her şeyin "neden" var olduğunun cevabı buradadır. Uygulamanızın en değerli, en önemli ve en korunaklı alanıdır.
- İçeriği Nedir?
- Entity'ler ve Aggregate'ler: İş kurallarını ve davranışları barındıran temel nesneler (Order, Product vb.).
- Value Object'ler: Değiştirilemez değerleri temsil eden nesneler (Money, Address vb.).
- Domain Service'ler: Birden fazla nesneyi ilgilendiren "evsiz" domain operasyonları (FundTransferService).
- Repository Arayüzleri (Interfaces): Dış dünyanın "nasıl" saklayacağını bilmediğimiz ama "saklaması gerektiğini" söylediğimiz kontratlar (OrderRepository arayüzü). Bunlar, kalbin dışarıya açılan "sürülen portlarıdır" (driven ports).
- Domain Events: Domain içinde gerçekleşen önemli olaylar (OrderShippedEvent).
- En Önemli Kuralı: Bu katmanın, kendisi dışında hiçbir katmana bağımlılığı yoktur. Bu katmandaki Java sınıfları, Spring, JPA, veya herhangi bir dış kütüphaneye ait anotasyon (@Entity, @Component vb.) veya sınıf içermemelidir. Burası saf Java ve saf iş mantığıdır. Bu katman, tek başına derlenebilmeli ve test edilebilmelidir.
2. Application Layer (Uygulama Katmanı): Orkestra Şefi 🧑指揮
Bu katman, domain katmanının hemen dışında yer alır ve dış dünya ile domain arasındaki orkestra şefidir. Dışarıdan gelen "şunu yap" isteklerini alır ve bunu domain'in anlayacağı bir dizi adıma çevirir.
- İçeriği Nedir?
- Application Service'ler: Kullanıcı senaryolarını (use cases) yöneten sınıflardır. "Yeni Kullanıcı Kaydet", "Siparişe Ürün Ekle" gibi her bir senaryo için genellikle bir metot içerirler.
- Bunlar, Hexagonal mimarideki "sürücü portlardır" (driving ports). IOrderService gibi bir arayüz ve onun implementasyonu olan OrderService sınıfından oluşabilir.
- Görevi Nedir? Bir Application Service metodunun tipik adımları şunlardır:
- Bir işlem (transaction) başlatır.
- Repository portlarını kullanarak gerekli Aggregate'i veya Aggregate'leri veritabanından bulur ve yükler.
- Yüklenen Aggregate'in ilgili metodunu çağırarak (örn: order.addItem(...)) veya bir Domain Service'i kullanarak domain mantığını tetikler.
- Değişen Aggregate'i yine Repository portu aracılığıyla veritabanına kaydeder.
- İşlemi (transaction) sonlandırır.
- Bağımlılıkları: Bu katman, sadece ve sadece içindeki Domain Katmanı'na bağımlıdır. Repository arayüzlerini ve domain nesnelerini bilir ama onların nasıl implemente edildiğini bilmez.
3. Infrastructure Layer (Altyapı Katmanı): Dış Dünya ⚙️
Bu, projenin en dıştaki, en "kirli" ama aynı zamanda en esnek katmanıdır. Dış dünya ile ilgili tüm teknik detaylar, tüm somut implementasyonlar burada yaşar. Burası, Hexagonal mimarideki Adaptörlerin evidir.
- İçeriği Nedir?
- Driving Adapters (Sürücü Adaptörler):
- REST Controller'lar: Spring Web ile yazılmış, HTTP isteklerini alıp ilgili Application Service'i çağıran sınıflar.
- Mesajlaşma Kuyruğu Dinleyicileri (Listeners): RabbitMQ veya Kafka'dan gelen mesajları dinleyip Application Service'leri tetikleyen sınıflar.
- Driven Adapters (Sürülen Adaptörler):
- Repository Implementasyonları: Domain katmanında tanımlanan OrderRepository arayüzünü, Spring Data JPA kullanarak implemente eden somut sınıflar. @Repository anotasyonları buradadır.
- E-posta Gönderim Servisleri: E-posta göndermek için dış bir servise (örn: Amazon SES) bağlanan somut sınıflar.
- Ödeme Sistemleri Entegrasyonları: Stripe, PayPal gibi ödeme sistemlerinin API'larıyla konuşan adaptörler.
- Bağımlılıkları: Bu katman, hem Application hem de Domain katmanındaki arayüzlere (portlara) bağımlıdır. Onların kontratlarını uygular. Ayrıca Spring, JPA, RabbitMQ gibi tüm dış kütüphanelere olan bağımlılıklar da bu katmanda yer alır.
Bu net katman ayrımı sayesinde, domain mantığınız (kalbiniz) dokunulmaz ve güvende kalırken, teknoloji ve dış dünya ile ilgili her şeyi, kalbi etkilemeden kolayca değiştirebilir veya güncelleyebilirsiniz.
Mimari felsefeyi ve mantıksal katmanları anladık. Şimdi bu teoriyi, start.spring.io'dan oluşturduğumuz projemizin içine nasıl yerleştireceğimize dair somut, pratik bir rehber hazırlayalım. Bu yapı, kitabımız boyunca geliştireceğimiz tüm kodlar için bir iskelet görevi görecek.
————————
6.3. Spring Boot ile Katmanlı Mimari Proje Yapısı
Teoride anlattığımız Domain, Application ve Infrastructure katmanlarını, Spring Boot projemizde anlamlı paket (package) yapılarına dönüştüreceğiz. Unutmayın, bu sadece bir öneridir; önemli olan, katmanlar arasındaki bağımlılık kurallarını çiğnememektir.
Projemizi, Bounded Context'lere göre modüllere ayırmak en iyi pratiktir. Kitabımızın ilerleyen bölümlerinde geliştireceğimiz "Sipariş" (Ordering) ve "Ürün Kataloğu" (Catalog) gibi bağlamlar için ayrı ana paketler oluşturacağız.
İşte ddd-kitap-projesi için önerdiğimiz paket yapısı:
com.yazilimokulu.dddkitapprojesi
│
├── ordering/ // Sipariş (Ordering) Bounded Context'i
│ │
│ ├── domain/ // 1. DOMAIN KATMANI (Kalp)
│ │ ├── model/
│ │ │ ├── aggregate/
│ │ │ │ └── Order.java
│ │ │ │ └── OrderItem.java
│ │ │ └── vo/ // Value Objects
│ │ │ └── OrderId.java
│ │ │ └── Money.java
│ │ ├── repository/
│ │ │ └── OrderRepository.java // <<-- SADECE INTERFACE!
│ │ └── service/
│ │ └── OrderPricingService.java // <<-- Domain Service
│ │
│ ├── application/ // 2. APPLICATION KATMANI (Orkestra Şefi)
│ │ ├── service/
│ │ │ └── OrderApplicationService.java
│ │ └── dto/ // Data Transfer Objects
│ │ ├── request/
│ │ │ └── CreateOrderRequest.java
│ │ └── response/
│ │ └── OrderSummaryDto.java
│ │
│ └── infrastructure/ // 3. INFRASTRUCTURE KATMANI (Dış Dünya)
│ ├── adapter/
│ │ ├── in/ // Driving Adapters (Gelen istekler)
│ │ │ └── web/
│ │ │ └── OrderController.java
│ │ └── out/ // Driven Adapters (Giden istekler)
│ │ ├── persistence/
│ │ │ ├── JpaOrderRepository.java // <<-- IMPLEMENTASYON
│ │ │ └── entity/
│ │ │ └── OrderJpaEntity.java // <<-- @Entity anotasyonlu sınıf
│ │ └── messaging/
│ │ └── OrderEventPublisher.java
│ └── config/
│ └── BeanConfiguration.java
│
├── catalog/ // Ürün Kataloğu (Catalog) Bounded Context'i
│ └── (benzer bir yapı...)
│
└── DddKitapProjesiApplication.java // Ana Spring Boot sınıfı
Şimdi bu katmanların içindeki kodların nasıl göründüğüne bakalım:
1. Domain Katmanı (ordering.domain)
Burası saf iş mantığının evi. Spring'e ait hiçbir şey yok!
domain/repository/OrderRepository.java (Port)
// Bu bir Spring veya JPA arayüzü değil, bizim kendi tanımladığımız bir kontrat.
// Domain katmanı, bunun nasıl implemente edileceğini bilmez.
public interface OrderRepository {
Optional<Order> findById(OrderId orderId);
void save(Order order);
}
domain/model/aggregate/Order.java (Aggregate Root)
// HİÇBİR Spring/JPA anotasyonu yok! Saf Java sınıfı.
public class Order {
private final OrderId id;
private List<OrderItem> items;
private Money totalAmount;
// ... kuralları uygulayan metodlar (addItem, ship, vb.) ...
}
————————
2. Application Katmanı (ordering.application)
Orkestrasyonun yapıldığı yer. Sadece domain katmanındaki arayüzlere bağımlı.
application/service/OrderApplicationService.java
import com.yazilimokulu.dddkitapprojesi.ordering.domain.model.aggregate.Order;
import com.yazilimokulu.dddkitapprojesi.ordering.domain.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service // Spring'e ait anotasyonlar burada başlar.
public class OrderApplicationService {
private final OrderRepository orderRepository; // <<-- Interface'e bağımlı!
public OrderApplicationService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public OrderId createNewOrder(CreateOrderRequest request) {
// ... request DTO'sundan domain nesneleri yaratılır ...
// Factory kullanarak yeni bir Order aggregate'i oluşturulur.
Order newOrder = Order.createNew(request.getCustomerId());
// Repository portu üzerinden kaydetme işlemi yapılır.
orderRepository.save(newOrder);
return newOrder.getId();
}
}
————————
3. Infrastructure Katmanı (ordering.infrastructure)
Dış dünya ile konuşan tüm adaptörler burada.
infrastructure/adapter/in/web/OrderController.java (Driving Adapter)
import com.yazilimokulu.dddkitapprojesi.ordering.application.service.OrderApplicationService;
import com.yazilimokulu.dddkitapprojesi.ordering.application.dto.request.CreateOrderRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController // Spring Web anotasyonu
public class OrderController {
private final OrderApplicationService orderService; // Application Service'e bağımlı
public OrderController(OrderApplicationService orderService) {
this.orderService = orderService;
}
@PostMapping("/orders")
public OrderId createOrder(@RequestBody CreateOrderRequest request) {
// Gelen HTTP isteğini Application katmanına delege eder.
return orderService.createNewOrder(request);
}
}
infrastructure/adapter/out/persistence/JpaOrderRepository.java (Driven Adapter)
import com.yazilimokulu.dddkitapprojesi.ordering.domain.repository.OrderRepository;
import org.springframework.stereotype.Repository;
// Bu sınıf, domain katmanındaki arayüzü implemente eder.
@Repository // Spring Data anotasyonu
public class JpaOrderRepository implements OrderRepository {
// Spring Data JPA'nın gücünü burada kullanırız.
private final SpringDataJpaOrderRepository springDataRepository;
// ... Constructor ...
@Override
public Optional<Order> findById(OrderId orderId) {
// JPA Entity'sini Domain nesnesine çevirme mantığı burada yapılır.
}
@Override
public void save(Order order) {
// Domain nesnesini JPA Entity'sine çevirme ve kaydetme mantığı burada yapılır.
}
}
// Spring Data'nın sihrini yapacağı asıl arayüz
// interface SpringDataJpaOrderRepository extends JpaRepository<OrderJpaEntity, UUID> { }
Bu yapı, "sorumlulukların ayrımı" (separation of concerns) prensibini mükemmel bir şekilde uygular. Domain katmanınız, projenizin en değerli varlığı olarak teknoloji karmaşasından korunur. Altyapı katmanı, teknoloji değiştiğinde (örneğin veritabanı veya web framework'ü), sadece ilgili adaptörün değiştirilmesiyle güncellenebilir. Bu da bize test edilebilir, esnek ve uzun ömürlü bir yazılım mimarisi sunar.
Kitabımızın en heyecan verici kısımlarından birine, yani sistemlerimizi daha esnek, ölçeklenebilir ve reaktif hale getiren modern desenlere giriş yapıyoruz. Bu desenlerin ilki ve en temel olanı Domain Events.
————————
7.1. Olay Nedir? Geçmişte Olan ve Değiştirilemeyen Bir Gerçeklik
Günlük hayatımız olaylarla doludur: "Doğdunuz", "Okuldan mezun oldunuz", "İşe başladınız". Bu olayların ortak noktası nedir? Hepsi geçmişte olmuş ve artık değiştirilemez olan birer gerçektir. Mezuniyet tarihinizi sonradan değiştiremezsiniz; o gün, o olay yaşanmıştır.
DDD dünyasında Domain Event (Alan Olayı), tam olarak budur.
Bir Domain Event, domain'iniz içinde gerçekleşen, iş açısından önemli ve artık geri alınamaz bir durumu bildiren bir mesaj nesnesidir.
Olayların en belirgin özellikleri şunlardır:
- Geçmiş Zamanda İsimlendirilirler: Bir olayın adı, her zaman geçmiş zaman kipiyle biter. Bu, olayın çoktan gerçekleştiğini ve bir "emir" değil, bir "bildirim" olduğunu vurgular.
- Yanlış: KargolaSiparis (Bu bir komuttur.)
- Doğru: SiparisKargolandi (Bu bir gerçektir.)
- Yanlış: KaydetKullanici (Bu bir komuttur.)
- Doğru: KullaniciKayitOldu (Bu bir gerçektir.)
- Değiştirilemezdirler (Immutable): Bir olay, yaşandığı ana ait bir fotoğraf karesi gibidir. Olay anındaki bilgileri içerir ve yaratıldıktan sonra içeriği asla değiştirilemez. Bu, onları Value Object'lere çok benzetir ve genellikle Java'da record ile modellenirler.
- Bir Gerçeği Temsil Ederler: Olaylar, bir "niyet" veya "istek" değil, sisteminizde yaşanmış somut bir gerçeği ifade eder. "Müşterinin şifresini sıfırlamak istiyoruz" bir olay değildir. Ama "MusterininSifreSifirlamaTalebiAlindi" bir olaydır.
Neden Önemliler?
Şimdiye kadar tasarladığımız sistemlerde, bir işlem diğerini doğrudan çağırıyordu. Örneğin, OrderApplicationService içinde bir sipariş onaylandığında, belki de doğrudan bir EmailService'i çağırıp müşteriye e-posta gönderiyorduk.
// Sıkı Sıkıya Bağlı (Tightly Coupled) Kötü Yaklaşım
public void confirmOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.confirm();
orderRepository.save(order);
// SORUN: Sipariş servisimiz, e-posta gönderme detayını bilmek zorunda!
// Ya e-posta yerine SMS de göndermek istersek? Ya da bir rapor oluşturmak?
// Bu servis giderek şişmanlar.
emailService.sendOrderConfirmationEmail(order.getCustomerId(), order.getId());
}
Domain Event'ler bu yapıyı tamamen değiştirir. Artık servisler birbirini doğrudan çağırmaz. Bunun yerine, bir servis işini bitirdiğinde, sadece olan biteni anlatan bir olayı etrafa "yayınlar". İlgilenen diğer servisler bu olayı "dinler" ve kendi işlerini yaparlar.
// Gevşek Bağlı (Loosely Coupled) İyi Yaklaşım
public void confirmOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 1. Order Aggregate'i durumunu değiştirir VE bir olay yaratır.
order.confirm(); // Bu metodun içinde "OrderConfirmedEvent" yaratılıp bir listeye eklenir.
orderRepository.save(order); // Repository, olayı da alıp yayınlar.
}
Bu OrderConfirmedEvent (Sipariş Onaylandı Olayı) yayınlandıktan sonra ne olur?
- NotificationService (Bildirim Servisi) bu olayı duyar ve müşteriye e-posta gönderir.
- InventoryService (Stok Servisi) bu olayı duyar ve ilgili ürünün stoğunu düşürür.
- ReportingService (Raporlama Servisi) bu olayı duyar ve günlük satış raporunu günceller.
En güzel yanı nedir? OrderService'in, bu diğer servislerin varlığından bile haberi yoktur! Sadece kendi işini yapar ve olanı duyurur. Bu, sistemleri inanılmaz derecede esnek ve bakımı kolay hale getirir. Yarın sisteme "Müşteriye Puan Ver" gibi yeni bir özellik eklemek istediğinizde, OrderService'e tek bir satır bile dokunmadan, sadece OrderConfirmedEvent'i dinleyen yeni bir servis yazmanız yeterlidir.
2025'ten Örnek Olaylar
- **OtonomAracSarjIstasyonunaYanasti:** Lojistik sisteminde bu olay yayınlandığında, Faturalandirma` servisi dinleyip şarj ücretini hesaplamaya başlayabilir.
- YapayZekaAnaliziTamamlandi: Bir veri analiz platformunda bu olay, sonuçları Raporlama servisine gönderebilir ve aynı anda kullanıcıya bir "Sonuçlarınız Hazır" bildirimi (Notification servisi) gönderebilir.
- KarbonKredisiSertifikasiUretildi: Enerji ticaret platformumuzda bu olay, Uyumluluk (Compliance) servisini tetikleyerek yasal raporlamaları hazırlamasını sağlayabilir.
Olaylar, sisteminizdeki farklı parçaların (Bounded Context'ler veya mikroservisler) birbirleriyle zarif, esnek ve dolaylı bir şekilde konuşmasını sağlayan modern mimarilerin temelidir.
Bir önceki bölümde Domain Event'in "ne" olduğunu anladık. Şimdi ise "neden" bu kadar devrimsel ve önemli olduğunu, özellikle de modern mimarilerde nasıl bir can simidi görevi gördüğünü inceleyelim.
Cevap tek bir sihirli kavramda saklı: Gevşek Bağlılık (Loose Coupling).
————————
7.2. Neden Önemli? Bounded Context'ler Arası Gevşek Bağlı (Loosely Coupled) İletişim
Sistemlerimizi Bounded Context'lere (Sınırlı Bağlamlara) ayırmamızın temel nedeni, karmaşıklığı yönetmek ve her bir parçanın kendi içinde tutarlı, bağımsız bir bütün olmasını sağlamaktı. Bir "Satış" bağlamı, bir "Stok Yönetimi" bağlamı, bir de "Kargo" bağlamı oluşturduk.
Peki bu bağımsız krallıklar birbirleriyle nasıl konuşacak?
Kötü Senaryo: Sıkı Sıkıya Bağlı (Tightly Coupled) İletişim
Olayları kullanmadığımız bir dünya hayal edelim. Bu dünyada, bir bağlam diğerini doğrudan, ismiyle çağırır. Buna senkron veya doğrudan çağrı (direct call) denir.
Benzetme: Bu, bir telefon görüşmesi gibidir. Birini aradığınızda, o kişinin telefonu açmasını, sizi dinlemesini ve size cevap vermesini beklersiniz. Eğer aradığınız kişinin hattı meşgulse veya telefonu kapalıysa, siz de işinize devam edemez, hatta kalıp beklersiniz.
2025 E-Ticaret Örneğimiz:
Müşteri, "Satış" bağlamı üzerinden bir ödeme yaptığında, SatisServisi'nin, KargoServisi'ne "Bu siparişi kargolamaya hazırla" demesi gerekiyor.
// SatisServisi'nin içi - KÖTÜ YAKLAŞIM
public class SatisServisi {
private final KargoServisi kargoServisi; // <<-- DOĞRUDAN BAĞIMLILIK!
private final StokServisi stokServisi; // <<-- BİR BAĞIMLILIK DAHA!
public SatisServisi(KargoServisi kargoServisi, StokServisi stokServisi) {
this.kargoServisi = kargoServisi;
this.stokServisi = stokServisi;
}
public void odemeOnayla(SiparisId siparisId) {
// ... ödeme onaylama mantığı ...
System.out.println("Ödeme onaylandı.");
try {
// 1. Stok servisini doğrudan çağır.
stokServisi.stoktanDus(siparisId);
// 2. Kargo servisini doğrudan çağır.
// 2025'te otonom drone'larla çalıştığımızı varsayalım.
kargoServisi.yeniKargoPaketiOlusturVeDroneAta(siparisId);
} catch (Exception e) {
// EYVAH! Kargo servisi çöktüyse ne olacak?
// Müşterinin ödemesini geri mi alacağız?
// Bu çok karmaşık bir hata yönetimi gerektirir.
}
}
}
Bu yaklaşımın korkunç sonuçları:
- Kırılganlık: KargoServisi'nde anlık bir sorun olursa (örneğin, drone atama servisi yanıt vermiyor), SatisServisi de işlemini tamamlayamaz ve hata verir. Bir sistemin çökmesi, diğerini de beraberinde sürükler. Domino etkisi yaratır.
- Bilgi Kirliliği: SatisServisi artık "drone atama" gibi kargo dünyasına ait bir kavramı bilmek zorunda kalır. Satış bağlamı, kargo bağlamının iç detaylarıyla kirlenmiş olur.
- Geliştirme Yavaşlığı: Kargo ekibi, yeniKargoPaketiOlusturVeDroneAta metodunun adını veya parametrelerini değiştirmek istediğinde, Satış ekibine gidip "Biz bir değişiklik yapacağız, siz de kodunuzu buna göre güncelleyin" demek zorunda kalır. Ekiplerin otonomisi kaybolur.
————————
İyi Senaryo: Olaylarla Gevşek Bağlı İletişim
Şimdi aynı senaryoyu Domain Event'ler ile yeniden tasarlayalım.
Benzetme: Bu, bir radyo yayını gibidir. Radyo spikeri (olayı yayınlayan), anonsunu yapar ve işine devam eder. "Hava durumu: Bugün Adana'da sıcaklık 40 derece." Bu anonsu kimin dinlediğini, dinleyip dinlemediğini veya dinledikten sonra ne yaptığını (şemsiye mi aldı, denize mi gitti) bilmez ve umursamaz. Dinleyiciler (olayı dinleyenler), kendi ilgilerine göre bu bilgiyi alıp kullanırlar.
2025 E-Ticaret Örneğimiz (Doğru Yaklaşım):
// SatisServisi'nin içi - DOĞRU YAKLAŞIM
public class SatisServisi {
// Artık diğer servislere doğrudan bağımlılık YOK!
private final EventPublisher eventPublisher;
// ...
public void odemeOnayla(SiparisId siparisId) {
// ... ödeme onaylama mantığı ...
System.out.println("Ödeme onaylandı.");
// TEK SORUMLULUĞUMUZ: Olanı duyurmak.
// Geçmiş zamanda, bir gerçek olarak.
OdemeOnaylandiEvent event = new OdemeOnaylandiEvent(siparisId, LocalDateTime.now());
eventPublisher.publish(event);
// İşimiz bitti! Kargo veya Stok servisinin ne yaptığı bizi ilgilendirmez.
}
}
Bu olay yayınlandıktan sonra ne olur?
- StokDinleyicisi (Stok Bağlamı'nda): OdemeOnaylandiEvent'i duyar ve kendi kendine "Ha, bir satış olmuş, şu ürünleri stoktan düşeyim" der ve işlemini yapar.
- KargoDinleyicisi (Kargo Bağlamı'nda): OdemeOnaylandiEvent'i duyar ve "Yeni bir paket hazırlamam ve bir drone atamam gerek" der ve kendi servisini tetikler.
Bu yaklaşımın harika faydaları:
- Dayanıklılık (Resilience): KargoServisi çökük olsa bile, SatisServisi işini başarıyla tamamlar. OdemeOnaylandiEvent bir mesajlaşma kuyruğuna (örn: RabbitMQ, Kafka) atılır. Kargo servisi ayağa kalktığında, kuyruktaki mesajı okur ve işine kaldığı yerden devam eder. Sistemler birbirlerinin anlık durumlarından etkilenmez.
- Otonomi ve Esneklik: Kargo ekibi, drone atama mantığını istediği gibi değiştirebilir. OdemeOnaylandiEvent'in formatı değişmediği sürece, Satış ekibinin ruhu bile duymaz. Her ekip kendi bağlamında özgürce çalışabilir.
- Genişletilebilirlik: Gelecekte, her ödeme onaylandığında "Müşteri Sadakat Puanı Ekle" diye yeni bir özellik mi geldi? Satış veya Kargo koduna dokunmanıza bile gerek yok. Sadece OdemeOnaylandiEvent'i dinleyen yeni bir SadakatPuaniDinleyicisi yazarsınız ve sisteme eklersiniz. Bu kadar!
Özetle, Domain Event'ler, Bounded Context'lerinizin birbirleriyle "telefonla konuşmak" yerine "radyo yayını ile haberleşmesini" sağlar. Bu, sistemlerinizi modern dünyanın gerektirdiği esnekliğe, dayanıklılığa ve ölçeklenebilirliğe kavuşturan en güçlü tekniktir.
Şimdi de olayların Bounded Context'ler arasındaki iletişimi nasıl sağladığını, 2025 yılına ait fütüristik ama bir o kadar da gerçekçi bir örnekle somutlaştıralım.
————————
7.3. 2025 Örneği: Otonom Araç ve Faturalandırma Senaryosu
2025 yılında faaliyet gösteren "Yeşil Rota" isimli otonom teslimat şirketimizi düşünelim. Şirketimizin iki temel iş alanı (Bounded Context) var:
- FiloYonetimi (Fleet Management) Context: Bu bağlamın sorumluluğu, otonom araçların (drone, yer aracı vb.) konumlarını, batarya seviyelerini, rotalarını ve şarj istasyonlarının durumunu yönetmektir.
- Faturalandirma (Billing) Context: Bu bağlamın sorumluluğu ise müşteri hesaplarını, hizmet bedellerini, faturaları ve ödemeleri yönetmektir.
Bu iki bağlam tamamen farklı işleri yapar ve farklı uzmanlıklar gerektirir. FiloYonetimi ekibi, lojistik ve operasyonel verimliliğe odaklanırken, Faturalandirma ekibi finansal doğruluğa ve muhasebe kurallarına odaklanır.
Senaryo: Bir Araç Şarj İstasyonuna Geldiğinde Ne Olur?
Bir otonom araç, bataryası azaldığında en yakın şarj istasyonuna yanaşır. Bu, faturalandırılması gereken bir hizmetin başlangıcıdır.
Sıkı Sıkıya Bağlı Kötü Yaklaşımda (direct call ile):
FiloYonetimi servisinin kodu şöyle görünürdü:
// FiloYonetimi Context'i içinde - KÖTÜ YAKLAŞIM
public class AracOperasyonServisi {
private final FaturalandirmaServisi faturalandirmaServisi; // <<-- KORKUNÇ BAĞIMLILIK!
public void aracIstasyonunaYanas(AracId aracId, IstasyonId istasyonId) {
// ... aracın durumunu "Şarj Oluyor" olarak güncelle ...
// Şimdi Faturalandirma Context'ini doğrudan çağır.
// FiloYonetimi ekibi, "fatura taslağı" gibi finansal bir kavramı bilmek zorunda!
faturalandirmaServisi.yeniFaturaTaslagiOlustur(aracId, istasyonId);
}
}
Bu yaklaşımda, Faturalandirma sisteminde yaşanacak en ufak bir kesinti veya değişiklik, doğrudan araçların operasyonel sürecini etkiler. Bu, kabul edilemez bir risktir.
————————
Gevşek Bağlı Doğru Yaklaşım (Domain Event ile):
Şimdi aynı senaryoyu olay güdümlü mimariyle ele alalım.
Adım 1: Olayın Tanımlanması ve Yayınlanması (FiloYonetimi Context'i)
FiloYonetimi bağlamı, bir aracın istasyona vardığını tespit ettiğinde, sadece bu gerçeği dünyaya duyurur. Kendi işini yapar ve sorumluluğu devreder.
Olayımız, Java 21 record ile basitçe şöyle modellenebilir:
// Olay, geçmiş zamanda isimlendirilir ve değiştirilemezdir.
// Olay anındaki önemli bilgileri içerir.
public record AracIstasyonunaYanas`ti`Event(
AracId aracId,
IstasyonId istasyonId,
LocalDateTime yanasmaZamani
) { }
FiloYonetimi servisinin kodu ise artık tertemizdir:
// FiloYonetimi Context'i içinde - DOĞRU YAKLAŞIM
public class AracOperasyonServisi {
private final EventPublisher eventPublisher;
public void aracIstasyonunaYanas(AracId aracId, IstasyonId istasyonId) {
// ... aracın durumunu "Şarj Oluyor" olarak güncelle ...
System.out.println(aracId + " ID'li araç, " + istasyonId + " ID'li istasyona yanaştı.");
// OLAN BİTENİ BİR GERÇEK OLARAK YAYINLA.
var event = new AracIstasyonunaYanas`ti`Event(aracId, istasyonId, LocalDateTime.now());
eventPublisher.publish(event);
// FiloYonetimi'nin işi burada bitti. Faturalandırmanın ne yaptığını umursamaz.
}
}
Adım 2: Olayın Dinlenmesi ve İşlenmesi (Faturalandirma Context'i)
Faturalandirma bağlamında, bu olayı dinlemek için özelleşmiş bir "dinleyici" (listener/handler) bulunur.
// Faturalandirma Context'i içinde
@Component // Bu bir Spring bileşenidir.
public class FaturaOlayDinleyicisi {
private final FaturaTaslagiServisi faturaTaslagiServisi;
// ... Constructor ...
// @EventListener anotasyonu sayesinde Spring, ne zaman bir AracIstasyonunaYanas`ti`Event
// yayınlansa bu metodun otomatik olarak çağrılacağını bilir.
@EventListener
public void handleVehicleArrivedEvent(AracIstasyonunaYanas`ti`Event event) {
System.out.println("Fatura Dinleyicisi: " + event.aracId() + " için bir olay yakalandı. Fatura taslağı oluşturuluyor...");
// Dinleyici, olayı alır ve kendi içindeki ilgili servise delege eder.
faturaTaslagiServisi.yeniFaturaTaslagiOlustur(
event.aracId(),
event.yanasmaZamani()
);
}
}
Sonuç:
Bu tasarımla FiloYonetimi ve Faturalandirma bağlamları birbirinden tamamen ayrıştırılmıştır.
- Otonomi: Faturalandirma ekibi, fatura oluşturma mantığını (yeniFaturaTaslagiOlustur) istediği gibi değiştirebilir. FiloYonetimi ekibinin bundan haberi bile olmaz.
- Dayanıklılık: Faturalandirma sistemi bakım için geçici olarak kapatılsa bile, FiloYonetimi sistemi araç operasyonlarına sorunsuz devam eder. AracIstasyonunaYanastiEvent'leri bir mesaj kuyruğunda birikir ve Faturalandirma sistemi tekrar açıldığında kaldığı yerden faturaları oluşturmaya devam eder.
- Genişletilebilirlik: Yarın bir Bakim (Maintenance) context'i eklemek istediğimizi ve her 5 yanaşmada bir araca otomatik bakım planlaması yapmak istediğimizi düşünelim. Yapmamız gereken tek şey, AracIstasyonunaYanastiEvent'ini dinleyen yeni bir BakimOlayDinleyicisi yazmaktır. Mevcut koda dokunmayız bile!
İşte Domain Event'lerin, birbirinden bağımsız sistemlerin uyum içinde ve esnek bir şekilde çalışmasını sağlayan gücü budur.
Domain Event'lerin ne kadar güçlü bir konsept olduğunu ve sistemleri nasıl ayrıştırdığını gördük. Şimdi bu gücü, Spring Boot'un bize sunduğu zarif ve basit araçlarla kendi projemizde nasıl hayata geçireceğimizi görelim.
Spring, kendi içinde olay güdümlü programlamayı destekleyen harika bir mekanizmaya sahiptir ve bunu kullanmak için karmaşık kütüphanelere veya altyapı kurulumlarına ihtiyacımız yoktur. Sadece iki temel anotasyonu bilmemiz yeterli: @ApplicationEventPublisher ve @EventListener.
————————
7.4. Spring Boot ile Domain Event'leri Yayınlama ve Dinleme
Bu mekanizmayı, en temel senaryomuz olan "Sipariş Oluşturulduğunda Müşteriye E-posta Gönderme" üzerinden adım adım uygulayalım.
Adım 1: Olay Nesnesini (Event Object) Tanımlama
Öncelikle, yayınlayacağımız olayın kendisini modellememiz gerekiyor. Bu, domain katmanımızda yer alacak, o anki önemli verileri taşıyan, basit ve değiştirilemez bir nesne olmalıdır. Java 21 record bunun için biçilmiş kaftandır.
ordering/domain/model/event/OrderCreatedEvent.java
import com.yazilimokulu.dddkitapprojesi.ordering.domain.model.vo.OrderId;
import com.yazilimokulu.dddkitapprojesi.ordering.domain.model.vo.CustomerId;
import java.time.LocalDateTime;
// Bir siparişin yaratıldığı gerçeğini temsil eden, değiştirilemez olay nesnesi.
// İlgili dinleyicilerin ihtiyaç duyacağı verileri (sipariş ID'si, müşteri ID'si vb.) taşır.
public record OrderCreatedEvent(
OrderId orderId,
CustomerId customerId,
LocalDateTime creationDate
) {
// İçerisine ekstra bir mantık eklememize gerek yok.
// Bu sadece bir veri taşıyıcısıdır (Data Transfer Object - DTO).
}
Adım 2: Olayı Yayınlamak (ApplicationEventPublisher ile)
Şimdi, siparişi oluşturan servisimizin, bu olayı yayınlamasını sağlamalıyız. Spring, bunu bizim için ApplicationEventPublisher adında bir arayüzle çok kolay hale getirir.
ordering/application/service/OrderApplicationService.java
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderFactory orderFactory;
// Spring'in olay yayınlama mekanizmasını buraya enjekte ediyoruz.
private final ApplicationEventPublisher eventPublisher;
public OrderApplicationService(OrderRepository orderRepository,
OrderFactory orderFactory,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.orderFactory = orderFactory;
this.eventPublisher = eventPublisher;
}
@Transactional
public OrderId createNewOrder(CreateOrderRequest request) {
// ... sipariş oluşturma mantığı ...
Order newOrder = orderFactory.createFrom(request);
orderRepository.save(newOrder);
// İŞİN EN ÖNEMLİ KISMI:
// Sipariş başarıyla veritabanına kaydedildikten sonra,
// bu gerçeği tüm uygulamanın dinlemesi için yayınlıyoruz.
System.out.println("YAYINCI: OrderCreatedEvent yayınlanıyor...");
OrderCreatedEvent event = new OrderCreatedEvent(
newOrder.getId(),
newOrder.getCustomerId(),
LocalDateTime.now()
);
eventPublisher.publishEvent(event);
return newOrder.getId();
}
}
Önemli Not: Olayı, genellikle ana işlem (orderRepository.save(newOrder)) başarıyla tamamlandıktan sonra yayınlarız. Bu sayede, veritabanı işlemi başarısız olursa olay hiç yayınlanmamış olur ve sistem tutarlılığı korunur.
Adım 3: Olayı Dinlemek (@EventListener ile)
Artık olayımız yayınlandığına göre, onu dinleyecek bir veya birden fazla bileşen oluşturabiliriz. Bu dinleyiciler, tamamen farklı Bounded Context'lerde veya aynı bağlamın farklı bir katmanında olabilirler.
Bizim senaryomuzda, bu olayla "Bildirim" (Notification) bağlamı ilgilenecek.
notification/application/listener/NotificationEventListener.java
import com.yazilimokulu.dddkitapprojesi.ordering.domain.model.event.OrderCreatedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
// Bu sınıfın görevi, domain olaylarını dinlemek ve ilgili işlemleri tetiklemektir.
@Component
public class NotificationEventListener {
// Buraya bir EmailService veya SmsService enjekte edilebilir.
// BU METOT BİR OLAY DİNLEYİCİSİDİR!
// @EventListener anotasyonu sayesinde Spring, ne zaman bir OrderCreatedEvent
// yayınlansa bu metodu otomatik olarak ve o olay nesnesiyle çağırır.
@EventListener
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
System.out.println("DİNLEYİCİ: OrderCreatedEvent yakalandı!");
System.out.println("Müşteriye e-posta gönderiliyor: " + event.customerId());
System.out.println("Sipariş Detayları: " + event.orderId());
// Burada email gönderme mantığı çalışır...
// emailService.sendOrderConfirmation(event.getCustomerId(), ...);
}
}
Sonucu Test Etmek
Uygulamayı çalıştırıp, OrderController üzerinden yeni bir sipariş oluşturma isteği gönderdiğinizde, konsolda şuna benzer bir çıktı göreceksiniz:
YAYINCI: OrderCreatedEvent yayınlanıyor...
DİNLEYİCİ: OrderCreatedEvent yakalandı!
Müşteriye e-posta gönderiliyor: CustomerId[value=c1-2-3]
Sipariş Detayları: OrderId[value=o1-2-3]
Bu kadar basit! OrderApplicationService'in, NotificationEventListener'ın varlığından haberi bile olmadan, aralarındaki iletişimi tamamen olaylar üzerinden, gevşek bağlı bir şekilde sağlamış olduk.
İleri Seviye Not: Varsayılan olarak bu olaylar senkron çalışır. Yani yayıncı, dinleyicinin işi bitene kadar bekler. Gerçek bir ayrıştırma için @Async anotasyonu ile dinleyicileri asenkron hale getirebiliriz. Bu sayede e-posta gönderme servisi yavaş çalışsa bile, sipariş oluşturma süreci anında tamamlanır. Bu konu, kitabın daha ileri bölümlerinde detaylandırılacaktır.
Şimdi de sistemlerimizi bir üst seviyeye taşıyacak, özellikle karmaşık ve yüksek performans gerektiren uygulamalar için tasarlanmış güçlü bir mimari desen olan CQRS'e giriş yapıyoruz.
————————
8.1. Yazma ve Okuma Modellerini Ayırmak: Performans ve Ölçeklenebilirlik
Şimdiye kadar tasarladığımız sistemlerde, genellikle tek bir modelimiz vardı. Bir Order (Sipariş) Aggregate'ini hem yeni bir sipariş oluşturmak (yazma işlemi) için hem de bir müşteriye geçmiş siparişlerini bir listede göstermek (okuma işlemi) için kullanıyorduk.
Bu, basit uygulamalar için gayet iyi çalışır. Ancak sistem büyüdükçe ve karmaşıklaştıkça, bu "tek model her işe yarar" yaklaşımı ciddi sorunlara yol açmaya başlar. Neden? Çünkü yazma ve okuma operasyonlarının ihtiyaçları doğaları gereği çok farklıdır.
- Yazma (Write) İhtiyaçları: Sistemin durumunu değiştiren operasyonlardır (create, update, delete). Burada öncelik, iş kurallarının korunması ve veri tutarlılığıdır. Bu yüzden zengin davranışlara sahip Aggregate'lerimizi kullanırız. Hedef, veriyi güvenli bir şekilde kaydetmektir.
- Okuma (Read) İhtiyaçları: Sistemin durumunu sadece görüntüleyen operasyonlardır. Burada öncelik, veriyi hızlı ve kullanıcı arayüzünün (UI) tam olarak ihtiyaç duyduğu formatta getirmektir. Karmaşık iş kurallarını burada çalıştırmaya gerek yoktur.
Bir düşünün: Müşteriye sipariş geçmişini gösteren bir liste ekranı için, her bir Order Aggregate'ini, içindeki tüm OrderItem'larla birlikte veritabanından yüklemek, tüm iş kurallarını doğrulamak... Bu, inanılmaz bir yavaşlığa ve verimsizliğe yol açar. O ekranın tek ihtiyacı, sipariş numarası, tarih ve toplam tutar gibi birkaç basit bilgidir.
İşte CQRS (Command Query Responsibility Segregation), yani Komut ve Sorgu Sorumluluklarını Ayırma Prensibi, bu iki farklı dünyayı birbirinden tamamen ayıran bir çözümdür.
CQRS der ki:
Sistemin durumunu değiştiren operasyonlar (Komutlar) ile sistemi sorgulayan operasyonları (Sorgular) için farklı modeller kullanın.
Bu, uygulamamızı mantıksal olarak ikiye bölmemiz anlamına gelir:
1. Komut Tarafı (Command Side - Yazma Modeli)
Burası bizim şimdiye kadar öğrendiğimiz zengin DDD dünyasıdır.
- Görevi: Sistemin durumunu değiştiren tüm istekleri karşılamak.
- Modeli: Zengin Aggregate'ler, Entity'ler ve Value Object'ler. İş kuralları ve tutarlılık kontrolleri burada yaşar.
- İş Akışı: Bir CreateOrderCommand (Sipariş Oluştur Komutu) gibi bir komut, bir CommandHandler (Komut İşleyici) tarafından alınır. İşleyici, Repository'den ilgili Aggregate'i bulur, Aggregate'in metodunu çağırır ve sonucu kaydeder.
- Odak Noktası: Tutarlılık ve İş Kuralları.
2. Sorgu Tarafı (Query Side - Okuma Modeli)
Burası, işin hız ve performans için optimize edildiği, tamamen farklı bir dünyadır.
- Görevi: Veriyi okumak ve görüntülemek için gelen tüm istekleri karşılamak.
- Modeli: Burada Aggregate'ler yoktur! Bunun yerine, doğrudan UI ekranlarına hizmet eden, basit, "aptal" DTO (Data Transfer Object) sınıfları vardır. OrderSummaryDto, ProductCatalogDto gibi.
- İş Akışı: Bir GetOrderHistoryQuery (Sipariş Geçmişini Getir Sorgusu) gibi bir sorgu, bir QueryHandler (Sorgu İşleyici) tarafından alınır. İşleyici, karmaşık domain modelini tamamen atlayarak, doğrudan veritabanı üzerinde optimize edilmiş bir sorgu çalıştırır (belki de özel olarak bu ekran için hazırlanmış bir "okuma tablosu" üzerinden) ve sonucu DTO olarak geri döner.
- Odak Noktası: Hız ve Performans.
Neden Bu Ayrım Bu Kadar Güçlü?
- Performans: Okuma işlemleri artık karmaşık Aggregate'leri yüklemek zorunda kalmaz. UI için optimize edilmiş, basit sorgularla veriyi çok daha hızlı çekerler. Bir rapor için gereken tüm veriyi, tek bir SQL sorgusuyla, 5 farklı tabloyu JOIN ederek getirebilirsiniz.
- Ölçeklenebilirlik: Yazma ve okuma tarafları tamamen bağımsız hale gelir. Uygulamanızda milyonlarca okuma isteği ama sadece binlerce yazma isteği olabilir. Bu durumda, okuma tarafı için kullandığınız veritabanını ve sunucuları, yazma tarafından tamamen bağımsız olarak, çok daha fazla sayıda olacak şekilde ölçeklendirebilirsiniz. Belki yazma için PostgreSQL, okuma için ise Elasticsearch veya Redis kullanırsınız!
- Basitlik: Her iki model de basitleşir. Aggregate'leriniz artık get... metodlarıyla veya UI'a özel verilerle kirlenmez; sadece saf iş mantığına odaklanır. Okuma modelleriniz ise iş kurallarının karmaşıklığını bilmek zorunda kalmaz; sadece veri göstermeye odaklanır.
CQRS, özellikle karmaşık raporlama ekranları, analiz panelleri veya yüksek trafikli okuma operasyonları olan sistemler için bir can simididir. Yazma ve okuma operasyonlarının farklı ihtiyaçlarını kabul ederek, her iki dünyanın da en iyi şekilde optimize edilmesine olanak tanır.
CQRS'in ikiye ayırdığı dünyanın ilk yarısına, yani Komut Tarafı'na (Command Side) odaklanalım. Bu tarafın temel yapı taşı, adını da verdiği Command'dır.
————————
8.2. Command (Komut): Sistemin Durumunu Değiştiren Operasyonlar
CQRS mimarisinde Command (Komut), sistemin durumunu değiştirmek için gönderilen, niyet odaklı bir mesaj nesnesidir. Bu, basit bir veri taşıyıcısından (DTO) daha fazlasıdır; sistemden belirli bir işi yapmasını talep eden resmi bir istektir.
Komutların temel özelliklerini ve felsefesini anlayalım:
- Bir Niyet Bildirirler (Imperative): Komutların isimleri, her zaman bir eylem, bir emir kipi taşır. Ne yapılması gerektiğini açıkça söylerler.
- CreateOrderCommand (Sipariş Yarat Komutu)
- ChangeShippingAddressCommand (Teslimat Adresini Değiştir Komutu)
- CancelMembershipCommand (Üyeliği İptal Et Komutu)
- Tek bir Alıcısı Vardır: Bir komut, her zaman onu işleyecek olan tek bir CommandHandler'a (Komut İşleyici) gönderilir. Radyo yayını gibi olan Domain Event'lerin aksine, komutlar belirli bir adrese gönderilmiş mektuplar gibidir.
- Sonuç Döndürebilir (veya Dönmeyebilir): Bir komut işlendikten sonra, işlemin başarılı olduğunu belirtmek için bir sonuç (örneğin, yaratılan Aggregate'in ID'si) dönebilir veya sadece işlemin tamamlandığını belirtmek için void olabilir. Hata durumunda ise bir istisna (exception) fırlatır.
Command vs. Domain Event: En Önemli Fark
Bu, yeni başlayanların en çok karıştırdığı noktadır. Aradaki farkı netleştirelim:
- Command (Komut): Gelecekte bir şeyin olmasını ister. Bir emirdir. Henüz gerçekleşmemiştir. "Lütfen siparişi kargola!"
- Örnek: ShipOrderCommand
- Event (Olay): Geçmişte bir şeyin olduğunu bildirir. Bir gerçektir. Çoktan gerçekleşmiştir. "Sipariş kargolandı."
- Örnek: OrderShippedEvent
Bir iş akışında genellikle önce bir Komut gelir, bu komut işlenir ve başarılı olursa sonuç olarak bir veya daha fazla Olay yayınlanır.
Bir Komut Nesnesi Nasıl Görünür?
Bir komut, genellikle operasyon için gereken tüm verileri içeren, basit ve değiştirilemez bir nesnedir. Java record'ları bu iş için yine mükemmeldir.
CreateOrderCommand.java
// Bu komut, yeni bir sipariş yaratma niyetini ve
// bu işlem için gereken tüm bilgileri taşır.
public record CreateOrderCommand(
CustomerId customerId,
List<OrderItemDto> items,
AddressDto shippingAddress
) {
// İçinde genellikle iş mantığı bulunmaz. Sadece veri taşır.
}
// Komutun içinde kullanılacak basit DTO'lar
public record OrderItemDto(ProductId productId, int quantity) { }
public record AddressDto(String street, String city, String zipCode) { }
Komutun Yolculuğu: CommandHandler (Komut İşleyici)
Peki bu CreateOrderCommand nesnesi gönderildiğinde onu kim karşılar ve işler? İşte bu noktada CommandHandler'lar devreye girer. Her komut tipi için, o komutu nasıl işleyeceğini bilen bir işleyici sınıfı yazarız.
Bir CommandHandler'ın görevi, Application Service'in yaptığı işe çok benzer:
- Komutu alır.
- Gerekirse Repository'den ilgili Aggregate'i yükler.
- Aggregate'in ilgili metodunu çağırarak domain mantığını çalıştırır.
- Sonucu Repository'ye kaydeder.
CreateOrderCommandHandler.java
import org.springframework.stereotype.Service;
@Service // Bir Spring bileşeni olarak işaretliyoruz.
public class CreateOrderCommandHandler {
private final OrderRepository orderRepository;
private final OrderFactory orderFactory;
// ... Constructor ...
// Bu metot, sadece ve sadece CreateOrderCommand'i işler.
public OrderId handle(CreateOrderCommand command) {
// Komut nesnesindeki DTO'ları, domain nesnelerimize çeviririz.
// Bu aşamada bir "mapper" sınıfı kullanılabilir.
List<OrderItem> domainItems = mapItemsFromDto(command.items());
Address shippingAddress = mapAddressFromDto(command.shippingAddress());
// Zengin domain modelimizi ve kurallarımızı kullanırız.
// Aggregate'i bir Factory ile oluşturabiliriz.
Order newOrder = orderFactory.create(command.customerId(), domainItems, shippingAddress);
// Aggregate'i kaydederiz.
orderRepository.save(newOrder);
// Başarılı operasyonun sonucunu, yani yeni siparişin ID'sini döneriz.
return newOrder.getId();
}
// ... private mapper metodları ...
}
Komutlar Nasıl Gönderilir?
Genellikle Controller'dan gelen istek, bir Command Bus veya Mediator adı verilen bir mekanizmaya CreateOrderCommand nesnesi olarak gönderilir. Command Bus, komutun tipine bakarak onu doğru CommandHandler'a (bizim örneğimizde CreateOrderCommandHandler'a) yönlendirir ve işlenmesini sağlar. Bu, Controller'ın doğrudan CommandHandler'ı bilmesini engeller ve sistemi daha da esnek hale getirir.
Özetle, Komutlar, sisteminize durum değiştirme niyetlerinizi ilettiğiniz, iyi tanımlanmış, niyet odaklı mesajlardır. Bu yapı, sisteminizin yazma tarafını (write side) son derece organize, öngörülebilir ve test edilebilir kılar.
CQRS'in "Command" tarafını, yani sistemin durumunu nasıl güvenli bir şekilde değiştirdiğimizi anladık. Şimdi de madalyonun diğer, ışık hızında çalışan yüzüne bakalım: Query (Sorgu) tarafı.
————————
8.3. Query (Sorgu): Sistemin Durumunu Değiştirmeyen, Sadece Veri Okuyan Operasyonlar
CQRS mimarisinde Query (Sorgu), sistemden veri talep eden, ancak sistemin durumunda kesinlikle hiçbir değişiklik yapmayan (side-effect free) bir mesaj nesnesidir. Tek amacı, kullanıcı arayüzlerini (UI), raporları veya diğer sistemleri beslemek için veri getirmektir.
Sorguların temel felsefesi, Komutların tam tersidir:
- Bir Soru Sorarlar: İsimleri, genellikle "Get..." veya "Find..." gibi ifadelerle başlar ve neyin istendiğini açıkça belirtir.
- GetOrderDetailsQuery (Sipariş Detaylarını Getir Sorgusu)
- FindAvailableProductsQuery (Mevcut Ürünleri Bul Sorgusu)
- GetUserDashboardStatsQuery (Kullanıcı Paneli İstatistiklerini Getir Sorgusu)
- Sistemin Durumunu Asla Değiştirmezler: Bir sorguyu bir milyon kez bile çalıştırsanız, sistemin veritabanındaki verilerde tek bir bit bile değişmemelidir. Bu, sorguların güvenli ve önbelleğe alınabilir (cacheable) olmasını sağlar.
- Her Zaman Bir Sonuç Döndürürler: Bir sorgunun tek amacı veri getirmek olduğu için, her zaman bir veri yapısı (genellikle bir DTO) döndürür.
Sorgu Tarafının Mimarisi: En Kısa Yol
Komut tarafında, iş kurallarını korumak için Aggregate'lerden oluşan zengin bir domain modelimiz vardı. Sorgu tarafında ise tek bir hedefimiz var: Hız!
Bu yüzden, sorgu tarafı zengin domain modelini tamamen baypas eder. Bir QueryHandler (Sorgu İşleyici), bir sorguyu aldığında, Aggregate'leri veya Repository'leri kullanmaz. Bunun yerine, doğrudan veritabanına gider ve kullanıcı arayüzünün ihtiyaç duyduğu verileri en verimli şekilde çeker.
Bir Sorgu Nesnesi Nasıl Görünür?
Tıpkı komutlar gibi, sorgular da genellikle o sorgu için gereken parametreleri taşıyan basit ve değiştirilemez nesnelerdir.
GetOrderDetailsQuery.java
// Bu sorgu, belirli bir siparişin detaylarını
// getirme niyetini ve siparişin kimliğini taşır.
public record GetOrderDetailsQuery(
OrderId orderId,
CustomerId requestingCustomerId // Belki güvenlik için kimin istediğini de bilmek isteriz.
) { }
Sorgunun İşlenmesi: QueryHandler ve "Okuma Modeli"
Bir GetOrderDetailsQuery gönderildiğinde, bunu ilgili QueryHandler karşılar. Bu işleyicinin görevi, Komut İşleyici'den çok farklıdır.
GetOrderDetailsQueryHandler.java
import org.springframework.stereotype.Service;
import org.springframework.jdbc.core.JdbcTemplate; // Örneğin, saf SQL için JdbcTemplate kullanabiliriz.
@Service
public class GetOrderDetailsQueryHandler {
// Handler, doğrudan bir veritabanı erişim aracına (örn: JdbcTemplate, Dapper, vs.)
// veya özel olarak bu iş için optimize edilmiş bir ORM'e (örn: Jooq) bağımlı olabilir.
private final JdbcTemplate jdbcTemplate;
public GetOrderDetailsQueryHandler(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// Bu metot, sadece GetOrderDetailsQuery'yi işler.
// Dönüş tipi, bir Aggregate değil, basit bir DTO'dur.
public OrderDetailsDto handle(GetOrderDetailsQuery query) {
// Burası çok önemli: Aggregate'i yüklemiyoruz!
// orderRepository.findById(query.orderId()) ÇAĞIRILMAZ!
// Bunun yerine, UI'ın ihtiyacına özel olarak yazılmış,
// belki de birden fazla tabloyu JOIN'leyen optimize bir SQL sorgusu çalıştırılır.
String sql = """
SELECT
o.id as order_id,
o.order_date,
o.status,
c.name as customer_name,
p.name as product_name,
oi.quantity,
oi.price
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.id = ?
""";
// Veritabanından gelen sonuç, doğrudan UI'da gösterilecek olan DTO'ya maplenir.
// Bu işlem için özel bir "RowMapper" kullanılır.
OrderDetailsDto dto = jdbcTemplate.queryForObject(sql, new OrderDetailsDtoRowMapper(), query.orderId().toString());
return dto;
}
}
OrderDetailsDto.java (Okuma Modeli)
// Bu, UI ekranında gösterilecek olan veriyi temsil eden "aptal" bir veri kabıdır.
// İçinde hiçbir iş mantığı yoktur.
public record OrderDetailsDto(
String orderId,
LocalDate orderDate,
String status,
String customerName,
List<OrderItemDetail> items
) {
public record OrderItemDetail(String productName, int quantity, BigDecimal price) { }
}
Özetle, Sorgular, sisteminizden veri okumanın en hızlı ve en verimli yolunu sunar. Zengin ve karmaşık domain modelini tamamen atlayarak, doğrudan veritabanından veya optimize edilmiş bir okuma kaynağından (read store) veri çekerler. Bu ayrım, uygulamanızın hem yazma tarafında tutarlı ve güvenli hem de okuma tarafında inanılmaz derecede performanslı ve ölçeklenebilir olmasını sağlar.
CQRS'in ne olduğunu, Komut ve Sorgu taraflarını teorik olarak anladık. Şimdi bu güçlü deseni Spring Boot projemizde nasıl uygulayabileceğimize dair pratik yöntemlere bakalım. Bu işi bizim için otomatikleştiren kapsamlı bir framework kullanabilir veya daha hafif, manuel bir yaklaşımla kendi mekanizmamızı kurabiliriz.
İki popüler yaklaşımı inceleyeceğiz:
- Axon Framework: CQRS, Domain Events ve Event Sourcing için tasarlanmış, her şeyi içinde barındıran, güçlü ve olgun bir framework.
- Hafif Mediator Deseni: .NET dünyasındaki MediatR kütüphanesinden esinlenen, herhangi bir dış bağımlılık olmadan, Spring'in kendi yetenekleriyle kolayca kurabileceğimiz basit bir yönlendirici (dispatcher) mekanizması.
————————
8.4. Spring Boot ve Axon Framework / Mediator Deseni ile Basit Bir CQRS Implementasyonu
Yaklaşım 1: Axon Framework ile "Her Şey Dahil" Çözüm
Axon, komutları, olayları ve sorguları yönetmek için gereken tüm altyapıyı size hazır olarak sunar. Yapmanız gereken, doğru yerlere doğru anotasyonları koymaktır. Axon, arka planda Command Bus, Event Bus ve Query Bus mekanizmalarını kendisi yönetir.
Adım 1: Bağımlılıkları Ekleme (pom.xml)
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.9.4</version> </dependency>
Adım 2: Komut ve Olayları Tanımlama (Plain Old Java Objects)
// Komut
public record CreateOrderCommand(
@TargetAggregateIdentifier // Bu komutun hangi aggregate'i hedeflediğini Axon'a söyler
UUID orderId,
UUID customerId
) { }
// Olay
public record OrderCreatedEvent(
UUID orderId,
UUID customerId,
LocalDateTime creationDate
) { }
Adım 3: Aggregate'i Oluşturma ve Komut/Olay İşleyicileri
Axon'da Aggregate'ler özeldir. @Aggregate anotasyonu ile işaretlenirler ve komutları/olayları doğrudan kendi içlerinde işlerler.
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
@Aggregate // Bu sınıfın bir Axon Aggregate'i olduğunu belirtir.
public class OrderAggregate {
@AggregateIdentifier // Bu alanın Aggregate'in kimliği olduğunu belirtir.
private UUID orderId;
private UUID customerId;
private OrderStatus status;
// Komut İşleyici Constructor
@CommandHandler
public OrderAggregate(CreateOrderCommand command) {
System.out.println("AXON: CreateOrderCommand işleniyor...");
// İş kuralları burada uygulanır.
// Komut başarılıysa, bir olay yayınla.
// `apply`, hem olayı yayınlar hem de aşağıdaki @EventSourcingHandler'ı çağırarak
// aggregate'in durumunu günceller.
apply(new OrderCreatedEvent(
command.orderId(),
command.customerId(),
LocalDateTime.now()
));
}
// Olay Kaynağı İşleyicisi (Event Sourcing Handler)
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
System.out.println("AXON: OrderCreatedEvent aggregate durumunu güncelliyor...");
this.orderId = event.orderId();
this.customerId = event.customerId();
this.status = OrderStatus.CREATED;
}
// Varsayılan constructor Axon için gereklidir.
protected OrderAggregate() { }
}
Adım 4: Komutu Gönderme (Controller veya Service'den)
@RestController
public class OrderController {
private final CommandGateway commandGateway; // Axon'un komut gönderme arayüzü
public OrderController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping("/axon/orders")
public CompletableFuture<String> createOrder(@RequestBody CreateOrderDto dto) {
CreateOrderCommand command = new CreateOrderCommand(
UUID.randomUUID(),
dto.getCustomerId()
);
System.out.println("AXON: CreateOrderCommand gönderiliyor...");
// commandGateway, bu komutu alıp doğru Aggregate'teki @CommandHandler'a yönlendirir.
return commandGateway.send(command).thenApply(Object::toString);
}
}
Axon'un Avantajı: Gördüğünüz gibi Axon, birçok detayı soyutlayarak sizi doğrudan iş mantığınıza odaklar. Özellikle Event Sourcing yapacaksanız vazgeçilmezdir.
Dezavantajı: Kendi "sihrini" ve konseptlerini öğrenmeyi gerektiren büyük bir framework'tür.
————————
Yaklaşım 2: Hafif Mediator Deseni ile "Kendi İşini Kendin Gör"
Eğer projenize büyük bir framework eklemek istemiyorsanız, Komutları doğru İşleyicilere yönlendirecek basit bir CommandBus mekanizmasını Spring ile kendiniz kurabilirsiniz.
Adım 1: Jenerik Arayüzleri Tanımlama
// Tüm komutlarımızın taşıyacağı işaretleyici arayüz.
public interface Command<R> { }
// Tüm komut işleyicilerimizin uygulayacağı jenerik arayüz.
public interface CommandHandler<R, C extends Command<R>> {
R handle(C command);
Class<C> getCommandType();
}
Adım 2: Somut Komut ve İşleyiciyi Yazma
// Komut (POJO)
public record CreateOrderCommand(UUID customerId) implements Command<UUID> { }
// İşleyici (Handler)
@Component // Bu bir Spring bileşeni
public class CreateOrderCommandHandler implements CommandHandler<UUID, CreateOrderCommand> {
// ... repository, factory vb. bağımlılıklar ...
@Override
public UUID handle(CreateOrderCommand command) {
System.out.println("MEDIATOR: CreateOrderCommand işleniyor...");
// Burada Aggregate'i yaratıp kaydetme mantığı bulunur.
UUID newOrderId = UUID.randomUUID();
// ...
return newOrderId;
}
@Override
public Class<CreateOrderCommand> getCommandType() {
return CreateOrderCommand.class;
}
}
Adım 3: Sihirli CommandBus'ı Oluşturma
Bu sınıf, tüm CommandHandler'ları başlangıçta bulup bir haritada saklayacak ve gelen komuta göre doğru olanı çağıracaktır.
@Component
public class SimpleCommandBus {
// Uygulamadaki tüm CommandHandler'ları tutacak olan harita.
private final Map<Class<? extends Command>, CommandHandler> handlers = new HashMap<>();
// Spring'in sihri: Uygulama başladığında, context'teki tüm CommandHandler
// tipindeki bean'leri bulur ve bu constructor'a bir liste olarak geçer.
public SimpleCommandBus(List<CommandHandler> commandHandlers) {
for (CommandHandler handler : commandHandlers) {
handlers.put(handler.getCommandType(), handler);
}
}
// Gelen komutu doğru handler'a yönlendiren metot.
public <R, C extends Command<R>> R dispatch(C command) {
CommandHandler<R, C> handler = handlers.get(command.getClass());
if (handler == null) {
throw new IllegalArgumentException("No handler found for command: " + command.getClass().getSimpleName());
}
return handler.handle(command);
}
}
Adım 4: Komutu Gönderme
@RestController
public class OrderController {
private final SimpleCommandBus commandBus;
public OrderController(SimpleCommandBus commandBus) {
this.commandBus = commandBus;
}
@PostMapping("/mediator/orders")
public UUID createOrder(@RequestBody CreateOrderDto dto) {
CreateOrderCommand command = new CreateOrderCommand(dto.getCustomerId());
System.out.println("MEDIATOR: CreateOrderCommand gönderiliyor...");
return commandBus.dispatch(command);
}
}
Mediator Yaklaşımının Avantajı: Hafiftir, dış bağımlılık yoktur ve CQRS'in arkasındaki mekaniği anlamanızı sağlar.
Dezavantajı: Sorgu tarafı, olay mekanizması gibi diğer parçaları kendiniz inşa etmeniz gerekir.
Her iki yöntem de CQRS'i Spring Boot'ta uygulamanın geçerli yollarıdır. Seçim, projenizin ölçeğine ve ne kadar "sihir" istediğinize bağlıdır.
Şimdi de DDD'nin ve modern mimarilerin en ileri seviye, en güçlü ama aynı zamanda düşünce yapısını en çok değiştiren desenlerinden birine geldik: Event Sourcing (Olay Kaynağı).
Bu desen, şimdiye kadar "veri saklama" hakkında bildiğimiz her şeyi sorgulamamızı isteyecek.
————————
9.1. Anlık Durumu Değil, Olayları Saklamak: Bir Aggregate'in Tüm Yaşam Döngüsünü Kaydetmek
Geleneksel olarak bir veriyi nasıl saklarız? Bir orders tablomuz olduğunu düşünelim. Bir siparişin durumunu "Kargolandı" olarak güncellediğimizde, veritabanına gider ve status sütunundaki 'PAID' değerini 'SHIPPED' olarak değiştiririz (UPDATE).
Bu yaklaşımın en büyük problemi şudur: Veriyi yok ederiz. 'PAID' bilgisi artık kaybolmuştur. Siparişin ne zaman ödendiğini, kargolanmadan önce başka bir durumdan geçip geçmediğini bilemeyiz. Sadece o anki, "son" durumunu biliriz. Elimizde sadece nesnenin son anının bir fotoğrafı vardır, ama o ana nasıl geldiğini anlatan bir film makarası yoktur.
Event Sourcing ise bu yaklaşıma kökten bir alternatif sunar ve der ki:
"Bir nesnenin anlık durumunu (current state) saklama. Onun yerine, o nesnenin başına gelen ve durumunu değiştiren tüm olayları (events), meydana geldikleri sırayla sakla. Nesnenin anlık durumunu öğrenmek istediğinde, olayları en baştan itibaren tekrar oynat."
Banka Hesabı Defteri Benzetmesi
Bu fikri anlamanın en iyi yolu, eski usul bir banka hesabı defterini düşünmektir.
- Geleneksel (State-Oriented) Yaklaşım: Bankanız size her gün sadece o anki bakiyenizi (current_balance = 1.250 TL) gösterir. Dün ne kadar paranız olduğunu veya bu paranın nereden geldiğini bu tek bilgiden anlayamazsınız.
- Event Sourcing Yaklaşımı: Bankanız size bir hesap dökümü (ledger) sunar. Bu dökümde şunlar yazar:
- +10.000 TL (Maaş Yattı)
- -500 TL (Kira Ödemesi)
- -1.500 TL (Kredi Kartı Ödemesi)
- +250 TL (Arkadaşından Gelen Borç)
- -7.000 TL (Tatil Harcaması)
Bu listede hiçbir veri "güncellenmez". Her işlem, yeni bir satır olarak listenin sonuna eklenir (append-only). O anki bakiyenizi mi öğrenmek istiyorsunuz? Bu olayları baştan sona toplamanız yeterlidir. (10000 - 500 - 1500 + 250 - 7000 = 1.250 TL).
En önemlisi, elinizde sadece bakiye değil, o baki-yeye nasıl ulaşıldığının tüm hikayesi vardır. İşte Event Sourcing budur!
Event Sourcing Nasıl Çalışır?
- Yazma (Olayları Kaydetme):
- Bir Aggregate'e bir komut gönderilir (örn: ShipOrderCommand).
- Aggregate, komutu işler ve sonucunda bir olay yaratır (örn: OrderShippedEvent).
- Bu OrderShippedEvent nesnesi, Event Store (Olay Deposu) adı verilen özel bir veritabanına, o Aggregate'e ait olay listesinin sonuna eklenir. Veritabanında hiçbir UPDATE sorgusu çalışmaz, sadece INSERT yapılır.
- Okuma (Aggregate'i Hayata Döndürme - Rehydration):
- Bir Aggregate üzerinde işlem yapmak istediğimizde (örn: AddGiftOptionCommand), önce o Aggregate'in anlık durumuna ihtiyacımız olur.
- Repository, Event Store'a gider ve o Aggregate'in kimliğine (orderId) ait tüm olayları (OrderCreated, ItemAdded, OrderConfirmed, OrderShipped...) sırasıyla okur.
- Boş bir Order nesnesi yaratır.
- Okuduğu olayları tek tek bu boş nesne üzerinde tekrar oynatır (replay). on(OrderCreatedEvent), on(ItemAddedEvent)... gibi metotlar çağrılır.
- Tüm olaylar bittiğinde, Order nesnesi en güncel haline ulaşmış olur. Bu işleme Rehydration (Yeniden Su Verme / Hayata Döndürme) denir. Artık Aggregate, yeni komutu işlemeye hazırdır.
Örnek: Bir OrderAggregate'in Yaşam Döngüsü
Bir siparişin Event Store'daki hikayesi şöyle görünür:
Event Store for Order ID: ord-123
- OrderCreatedEvent(customerId: 'cust-456', createdDate: '...')
- ItemAddedEvent(productId: 'prod-789', quantity: 2, price: 100.00)
- ItemAddedEvent(productId: 'prod-abc', quantity: 1, price: 50.00)
- ShippingAddressChangedEvent(newAddress: '...')
- OrderConfirmedEvent(confirmationDate: '...')
- OrderShippedEvent(shippingDate: '...', trackingNumber: 'XYZ')
OrderRepository.findById('ord-123') çağrıldığında, sistem boş bir Order yaratır ve bu 6 olayı sırayla üzerine uygulayarak siparişin en son "Kargolandı" durumuna ve 250 TL'lik toplam tutarına sahip, güncel halini bize verir.
Peki bu ne işe yarar?
- Tam Denetim İzi (Full Audit Trail): Bir Aggregate'in geçmişindeki her adımı bilirsiniz. Bu, hata ayıklama, yasal gereklilikler ve iş analizi için bir hazinedir. "Bu siparişin durumu neden 'İptal Edildi'? Hangi olaydan sonra bu oldu?" sorusunun cevabı elinizin altındadır.
- Zamanda Yolculuk: Geçmişteki herhangi bir ana gidip, o andaki sistemin durumu neydi sorusunu cevaplayabilirsiniz.
- Güçlü Analitik ve Raporlama: Olay akışını analiz ederek kullanıcı davranışları hakkında inanılmaz içgörüler elde edebilirsiniz.
- CQRS için Mükemmel Zemin: Yazma tarafı (Command) olayları Event Store'a kaydederken, okuma tarafı (Query) bu olay akışını dinleyerek kendi optimize edilmiş okuma modellerini (read models) kolayca oluşturabilir.
Event Sourcing, geleneksel veritabanı alışkanlıklarımızı değiştirmeyi gerektiren bir paradigma değişimidir, ancak sunduğu esneklik ve denetim gücü, onu özellikle karmaşık ve denetimin kritik olduğu domain'ler için vazgeçilmez kılar.
Event Sourcing, geleneksel veri saklama yöntemlerine göre daha karmaşık bir yaklaşım gibi görünse de sunduğu faydalar, bu yatırımın karşılığını fazlasıyla verir. Bu faydalar, özellikle iş mantığının karmaşık olduğu ve denetimin kritik önem taşıdığı sistemlerde kendini gösterir.
- 9.2. Faydaları: Tam denetim izi (audit trail), geçmişteki bir ana dönebilme, hata ayıklama kolaylığı.
————————
1. Tam Denetim İzi (Full Audit Trail) 🕵️♂️
Geleneksel veritabanlarında bir veriyi güncellediğinizde (UPDATE), eski veri kaybolur. Elinizde sadece son durum kalır. Peki ya birisi size şu soruyu sorarsa: "Bu siparişin tutarı neden 500 TL görünüyor, ben 450 TL olması gerektiğini hatırlıyorum. Bu değişikliği kim, ne zaman, neden yaptı?"
Geleneksel modelde bu sorunun cevabını bulmak için karmaşık loglama (logging) tabloları tutmanız gerekir. Event Sourcing'de ise bu cevap, sistemin doğasında zaten vardır.
Event Sourcing'in Sağladığı: Bir Aggregate'in tüm yaşam döngüsü, yani başına gelen her bir olay, sırasıyla kaydedildiği için elinizde mükemmel bir denetim izi bulunur. Sadece "ne" olduğunu değil, "nasıl" ve "ne zaman" olduğunu da bilirsiniz.
2025 "Yeşil Rota" Lojistik Örneği: Bir müşteri, "Paketim neden teslimat adresim yerine eski adresime gönderildi?" diye şikayette bulundu. Event Store'daki o pakete ait olay listesine baktığınızda şunu görürsünüz:
- PaketOlusturulduEvent(adres: "Yeni Adres...")
- TeslimatAdresiDegistirildiEvent(eskiAdres: "Yeni Adres...", yeniAdres: "Eski Adres...", kullaniciId: 'admin-01')
- PaketKargolandiEvent(adres: "Eski Adres...")
Bu olay akışı sayesinde, sorunun bir admin kullanıcısının adresi yanlışlıkla değiştirmesinden kaynaklandığını anında tespit edebilirsiniz. Elinizde sorgulanamaz bir kanıt vardır.
————————
2. Geçmişteki Bir Ana Dönebilme (Temporal Query) ⏳
İş dünyasında sıkça sorulan bir soru vardır: "Geçen ayın 15'i, saat 17:00 itibarıyla envanterimizdeki ürünlerin durumu neydi?" veya "Bir önceki çeyreğin sonunda kaç tane aktif kullanıcımız vardı?"
Geleneksel modelde bu soruları cevaplamak neredeyse imkansızdır, çünkü geçmişteki durumlar üzerine yazılmıştır. Event Sourcing'de ise bu, son derece basit bir işlemdir.
Event Sourcing'in Sağladığı: Bir Aggregate'in veya tüm sistemin geçmişteki herhangi bir T anındaki durumunu yeniden oluşturabilirsiniz. Yapmanız gereken tek şey, olayları en baştan itibaren T anına kadar olanları tekrar oynamaktır (replay).
2025 "Yeşil Rota" Lojistik Örneği: Bir finansal denetçi, "31 Mart 2025, gece yarısı itibarıyla A-101 numaralı otonom aracın batarya seviyesi tam olarak neydi?" diye sordu. A-101 aracının olaylarını 31 Mart gece yarısına kadar oynatarak o anki batarya seviyesini (%37.4) kesin olarak hesaplayabilir ve raporlayabilirsiniz. Bu, finansal raporlama ve yasal uyumluluk için paha biçilmez bir yetenektir.
————————
3. Hata Ayıklama (Debugging) ve Hata Düzeltme Kolaylığı 🐛
Yazılımda hatalar (bug'lar) kaçınılmazdır. Bazen canlıya çıkan hatalı bir kod, veritabanındaki yüzlerce kaydın durumunu bozabilir. Geleneksel modelde bu bozuk veriyi düzeltmek, karmaşık ve riskli "düzeltme script'leri" yazmayı gerektirir.
Event Sourcing'in Sağladığı: Bir hata nedeniyle yanlış olayların üretildiğini fark ettiğinizde, durumu düzeltmek çok daha güvenli ve yönetilebilirdir.
2025 "Yeşil Rota" Lojistik Örneği: Yeni çıkılan bir özellik nedeniyle, teslim edilen paketler için PaketTeslimEdildiEvent yerine yanlışlıkla PaketIadeEdildiEvent üretildiğini fark ettiniz.
- Hata Tespiti: Hatalı kod versiyonunu geri çekersiniz.
- Düzeltme: Event Store'a dokunmazsınız. Bunun yerine, yanlış üretilen olayları telafi edecek "dengeleyici olaylar" (compensating events) üreten bir script yazarsınız. Her bir PaketIadeEdildiEvent için, doğru PaketTeslimEdildiEvent'i üretir ve bunu olay akışının sonuna eklersiniz.
- Sonuç: Aggregate'lerin anlık durumu, bu telafi edici olaylar sayesinde doğru hale gelir. En önemlisi, hiçbir veriyi silmediniz veya üzerine yazmadınız. Sistemin geçmişi, yapılan hatayı ve o hatanın nasıl düzeltildiğini de içerecek şekilde bozulmadan kalır. Bu, sisteminize olan güveni artırır.
Event Sourcing'in faydalarını ve çalışma mantığını, günümüzün ve yakın geleceğin en hassas ve en önemli alanlarından biri olan sağlık sektörü üzerinden somut bir örnekle taçlandıralım.
————————
9.3. 2025 Örneği: Bir "Dijital Sağlık Platformu" ve Hastanın Hikayesi
2025 yılında, hastaların tüm sağlık verilerini, giyilebilir sensörlerden gelen anlık ölçümleri, doktor teşhislerini ve tedavi süreçlerini tek bir yerde toplayan bir "Dijital Sağlık Platformu" geliştirdiğimizi düşünelim. Bu sistemin merkezinde, her bir hastanın sağlık geçmişini temsil eden Patient (Hasta) Aggregate'i yer alıyor.
Bu domain'de veri, herhangi bir e-ticaret sitesinden çok daha kritiktir. Bir verinin yanlışlıkla güncellenmesi veya silinmesi, hasta sağlığını doğrudan tehlikeye atabilir ve ciddi yasal sonuçlar doğurabilir.
Geleneksel Yaklaşımın Ölümcül Riski:
Eğer geleneksel bir veritabanı tasarımı yapsaydık, patients tablomuzda current_diagnosis (mevcut teşhis), medication_list (ilaç listesi) gibi kolonlar olurdu. Bir doktor, hastanın ilacını "İlaç A"dan "İlaç B"ye değiştirdiğinde, UPDATE sorgusu çalışır ve "İlaç A" bilgisi sonsuza dek kaybolurdu. Altı ay sonra başka bir doktor "Bu hastaya neden İlaç A verilmişti ve neden bırakıldı?" diye sorduğunda, bu sorunun cevabı sistemde mevcut olmazdı. Bu, tam bir tıbbi ve yasal kabustur.
————————
Event Sourcing ile Hastanın Hikayesini Yazmak
Event Sourcing ile Patient Aggregate'inin anlık durumunu değil, onun başına gelen her bir tıbbi olayı, yani hikayesini kaydederiz. Gelin "Ayşe Yılmaz" adında bir hastanın hikayesini, Event Store'da nasıl görüneceğine bakalım:
Event Store for Patient ID: patient-ayse-yilmaz
1. HastaKaydiOlusturulduEvent (10 Ocak 2025)
- Hasta sisteme ilk kez kaydedildi.
- record HastaKaydiOlusturulduEvent(PatientId patientId, String name, LocalDate birthDate)
2. TaniKonulduEvent (15 Şubat 2025)
- Dr. Ahmet, hastaya "Hipertansiyon" tanısı koydu.
- record TaniKonulduEvent(PatientId patientId, DoctorId doctorId, DiagnosisCode diagnosisCode, String notes)
3. IlacReceteEdildiEvent (15 Şubat 2025)
- Dr. Ahmet, hipertansiyon için "Amlodipin 5mg" ilacını reçete etti.
- record IlacReceteEdildiEvent(PatientId patientId, DoctorId doctorId, DrugInfo drug, Dosage dosage)
4. KanBasinciOlcumuEklendiEvent (20 Mart 2025)
- Hastanın akıllı saatinden gelen veriyle kan basıncı (150/95 mmHg) sisteme eklendi.
- record KanBasinciOlcumuEklendiEvent(PatientId patientId, int systolic, int diastolic, Instant measurementTime)
5. IlacDozuGuncellendiEvent (22 Mart 2025)
- Dr. Ahmet, kan basıncı düşmediği için ilacın dozunu "10mg" olarak güncelledi. Sebebini not düştü.
- record IlacDozuGuncellendiEvent(PatientId patientId, DoctorId doctorId, DrugInfo drug, Dosage newDosage, String reason)
Bu olay akışı, Ayşe Hanım'ın sağlık geçmişinin sorgulanamaz, değiştirilemez ve eksiksiz bir kaydıdır.
Bu Olay Akışının Faydaları
1. Yasal Zorunluluk ve Tam Denetim İzi (Audit Trail)
Sağlık sektöründeki KVKK (GDPR) ve hasta hakları yönetmelikleri, verinin kim tarafından, ne zaman ve neden değiştirildiğinin kaydının tutulmasını zorunlu kılar. Event Sourcing, bu gerekliliği doğal olarak karşılar.
- Soru: "Dr. Ahmet'in 22 Mart'ta doz değişikliği yapma gerekçesi neydi?"
- Cevap: 5 numaralı olayın içindeki reason alanına bak: "Kan basıncı hedeflenen seviyeye inmediği için doz artırımı yapıldı." Bu, olası bir hukuki durumda hayat kurtaran bir kanıttır. Her olay, kimin (doktor/hasta/sistem), neyi, ne zaman yaptığını gösteren dijital bir imzaya sahiptir.
2. Geleceğin Tıbbı İçin Bir Hazine: Yapay Zeka Analizleri
Bu yapı, sadece geçmişi kaydetmekle kalmaz, geleceği tahmin etmek için de bir altın madenidir. Elimizdeki bu zengin olay akışı, 2025'in gelişmiş yapay zeka (AI) ve makine öğrenmesi (ML) modellerini beslemek için mükemmel bir veri setidir.
- Klinik Araştırma: "Hipertansiyon tanısı konulduktan sonra Amlodipin reçete edilen, ancak 1 ay içinde kan basıncında %10'dan az düşüş görülen tüm hastaların ortak genetik belirteçleri var mı?" Bu sorguyu, olay akışını analiz ederek kolayca çalıştırabiliriz.
- Tahminsel Tıp: "Belirli bir ilacı kullanmaya başlayan ve sonrasında belirli bir yan etkiyi gösteren hastaların olay dizilerindeki ortak desenler nelerdir?" Bu analiz, gelecekteki hastalar için kişiselleştirilmiş risk tahminleri yapılmasını sağlayabilir.
- Tedavi Etkinliği: Yapay zeka modelleri, milyonlarca hastanın olay hikayesini inceleyerek, "şu genetik profile ve yaşam tarzına sahip bir hasta için en etkili başlangıç ilacı hangisidir?" sorusuna kanıta dayalı cevaplar üretebilir.
Geleneksel, anlık durumu saklayan bir veritabanında bu tür analizleri yapmak imkansıza yakındır. Event Sourcing ise bize sadece bir hastanın anlık fotoğrafını değil, tüm tıbbi filmini sunar. Bu da onu, 2025'in veri odaklı ve proaktif sağlık hizmetleri için vazgeçilmez bir mimari desen haline getirir.
Event Sourcing'in kalbine, yani olaylarımızı güvenle sakladığımız ve onlara geri döndüğümüz o özel yere geldik. Bu yerin adı Event Store.
————————
9.4. Event Store Nedir?
Önceki bölümlerde, Event Sourcing'in bir Aggregate'in anlık durumunu değil, onun başına gelen olayların tamamını kaydettiğini öğrendik. Geleneksel veritabanları, verileri "güncellemek" (UPDATE) ve "silmek" (DELETE) üzerine kuruludur. Oysa bizim olayları asla değiştirmememiz veya silmememiz, sadece listenin sonuna yeni olaylar eklememiz gerekiyor.
İşte Event Store (Olay Deposu), tam olarak bu amaca hizmet etmek için tasarlanmış veya bu şekilde kullanılan özel bir veri saklama mekanizmasıdır. Event Sourcing mimarisinin tek ve mutlak doğruluk kaynağıdır (single source of truth).
Bir Event Store'u, geleneksel bir veritabanından ayıran temel özellikler şunlardır:
- Sadece Ekleme Yapılabilen Bir Günlük Defteridir (Append-Only Log): Event Store'a veri yazmanın tek bir yolu vardır: Mevcut olay listesinin sonuna yeni bir olay eklemek. UPDATE veya DELETE komutları burada ya hiç yoktur ya da kullanılması kesinlikle yasaktır. Bu, geçmişin asla değiştirilemeyeceği garantisini verir.
- Olayları Sırasıyla Saklar: Her olay, bir Aggregate kimliğiyle ilişkilendirilir ve o Aggregate'e ait olay akışı içinde sıralı bir şekilde (version veya sequence_number ile) saklanır. Bu sıra, olayların doğru sırada tekrar oynatılabilmesi (replay) için hayati önem taşır.
- Temel Operasyonları Basittir: Bir Event Store'un sunduğu temel işlevler şunlardır:
- AppendEvents(aggregateId, events): Belirli bir Aggregate kimliği için yeni olayları akışın sonuna ekle.
- ReadEvents(aggregateId): Belirli bir Aggregate kimliğine ait tüm olayları, en baştan en sona doğru, sıralı bir şekilde geri getir.
Geleneksel Veritabanı vs. Event Store Benzetmesi
- Geleneksel Veritabanı (CRUD Database): Bir beyaz tahta gibidir. Üzerine bir şey yazar (CREATE), okur (READ), yazdığınız şeyin bir kısmını silip düzeltir (UPDATE) veya tamamını silersiniz (DELETE). Bir düzeltme yaptığınızda, tahtanın o kısmında daha önce ne yazdığına dair bir bilgi kalmaz.
- Event Store: Bir mahkeme katibinin duruşma tutanağı gibidir. Katip, söylenen her şeyi sırasıyla yazar. Birisi daha önce söylediği bir şeyi düzeltmek isterse, katip eski ifadeyi silmez. Sadece yeni bir satır açar ve "Sanık, önceki ifadesini şu şekilde düzeltti: ..." diye yazar. Tutanak, hem orijinal ifadeyi, hem de düzeltme eyleminin kendisini içerir. Her şey kayıt altındadır.
Event Store Olarak Ne Kullanılabilir?
Piyasada bu iş için özel olarak tasarlanmış çözümler olduğu gibi, mevcut teknolojileri bu amaca uygun şekilde kullanmak da mümkündür. 2025 itibarıyla popüler seçenekler şunlardır:
- Özelleşmiş Event Store Veritabanları:
- EventStoreDB: Event Sourcing konseptinin öncüleri tarafından geliştirilmiş, bu iş için özel olarak tasarlanmış açık kaynaklı bir veritabanıdır. Güçlü sorgulama yetenekleri ve projeksiyon (projection) özellikleriyle öne çıkar.
- Axon Server: Axon Framework ile birlikte gelen, hem Event Store hem de mesaj yönlendirme (message routing) işlevlerini yerine getiren, yüksek performanslı bir çözümdür.
- Geleneksel Veritabanlarını Kullanmak:
- İlişkisel Veritabanları (PostgreSQL, SQL Server vb.): events adında basit bir tablo oluşturarak kendi Event Store'unuzu yaratabilirsiniz. Bu tabloda aggregate_id, sequence_number, event_type, event_data (genellikle JSON veya binary formatta) gibi kolonlar bulunur. UPDATE ve DELETE yetkilerini kısıtlayarak "append-only" davranışını taklit edebilirsiniz.
- NoSQL Veritabanları (MongoDB, DynamoDB vb.): Doküman veya anahtar-değer (key-value) tabanlı veritabanları, olayları esnek bir formatta saklamak için oldukça uygundur.
- Mesajlaşma Kuyruklarını ve Akış Platformlarını Kullanmak:
- Apache Kafka: Doğası gereği dağıtık, değiştirilemez bir "commit log" olduğu için Event Store olarak kullanılmaya çok uygundur. Olayları "topic"lerde saklayarak hem kalıcılık sağlar hem de bu olayları dinleyecek farklı servislere (consumer'lara) kolayca dağıtılmasını sağlar.
Hangi Çözümü Seçmelisiniz?
- Yeni Başlıyorsanız: Geleneksel bir SQL veritabanı ile başlamak, konsepti anlamak için en basit yoldur.
- Büyük Ölçekli ve Ciddi Bir Proje Yapıyorsanız: EventStoreDB veya Axon Server gibi bu işe özel çözümleri değerlendirmek, uzun vadede size zaman ve performans kazandıracaktır.
- Mikroservis Mimarisi ve Yüksek Veri Akışı Varsa: Kafka, hem olay depolama hem de olay dağıtımı için güçlü bir seçenek olarak öne çıkar.
Özetle, Event Store, olay kaynaklı sisteminizin kalbidir. Olayların güvenli, değiştirilemez ve sıralı bir şekilde saklandığı, sistemin tüm geçmişinin ve gerçeğinin yattığı yerdir.
Kitabımızın teorik ve ileri seviye konularını tamamladıktan sonra, tüm bu öğrendiklerimizi bir araya getireceğimiz, baştan sona bir proje geliştireceğimiz pratik uygulama kısmına başlıyoruz. Bu bölümde, DDD'nin stratejik ve taktiksel araçlarını, 2025 yılına uygun, modern ve karmaşık bir problem üzerinde uygulayacağız.
————————
10.1. Proje Vizyonu: "Yeşil Rota" - Akıllı Lojistik Platformu
Problem Alanı: Yıl 2025. E-ticaret, günlük hayatın vazgeçilmez bir parçası haline gelmiş, şehirlerde anlık teslimat beklentisi tavan yapmıştır. Ancak bu durum, ciddi bir sorunu da beraberinde getirmiştir: Şehir merkezlerini dolduran fosil yakıtlı kurye motorları ve kamyonetlerin yarattığı trafik sıkışıklığı, gürültü kirliliği ve devasa karbon ayak izi. Tüketiciler hem hız hem de çevreye duyarlılık talep ederken, mevcut lojistik firmaları bu iki beklentiyi bir arada karşılamakta zorlanmaktadır.
Çözüm Vizyonumuz: Yeşil Rota
"Yeşil Rota", bu çelişkiyi ortadan kaldırmak için doğmuş bir teknoloji platformudur. Vizyonumuz, şehir içi "son mil" (last-mile) teslimatını, ekolojik ve ekonomik olarak sürdürülebilir bir şekilde yeniden tanımlamaktır.
Platformumuz, temel olarak, heterojen bir filoyu akıllıca yönetir:
- Elektrikli Otonom Drone'lar: Küçük ve hafif paketlerin, trafik sorununa takılmadan, en hızlı şekilde teslimatı için kullanılır.
- Otonom Yer Araçları (AGV - Automated Ground Vehicles): Daha büyük paketlerin, belirlenmiş güvenli rotalar üzerinden verimli bir şekilde taşınması için kullanılır.
"Akıllı" Olan Ne? Core Domain'imiz
"Yeşil Rota" sadece bir paket takip sistemi değildir. Platformun kalbi ve bizi rakiplerimizden ayıran Core Domain'i, çok faktörlü rota optimizasyon motorudur. Bir teslimat emri geldiğinde, sistemimiz sadece en hızlı rotayı değil, en "optimal" rotayı bulur. Bu "optimal" tanımı, aşağıdaki gibi birçok dinamiği hesaba katan karmaşık bir iş mantığı içerir:
- Paketin özellikleri: Ağırlık, boyut, hassasiyet.
- Filo durumu: Hangi drone'un veya yer aracının bataryası en dolu ve konumu en yakın?
- Anlık trafik ve hava durumu: Bir yer aracı için trafik sıkışıklığı, bir drone için ise şiddetli rüzgar kritik bir veridir.
- Enerji Tüketimi: Hangi rota ve hangi araç kombinasyonu, kilometre başına en az enerjiyi tüketir?
- Ve en önemlisi: Karbon Emisyonu Minimizasyonu: Tüm bu faktörler, tek bir ana hedef doğrultusunda değerlendirilir: Her bir teslimatın karbon ayak izini matematiksel olarak en aza indirmek.
Hedefimiz
"Yeşil Rota"nın nihai hedefi, sadece paket teslim eden bir şirket olmak değil, aynı zamanda şehirlerin daha yaşanabilir ve daha yeşil olmasına katkıda bulunan bir teknoloji ortağı olmaktır. E-ticaret firmalarına, müşterilerine "karbon-nötr teslimat" seçeneği sunma imkanı vererek, hem onlara rekabet avantajı sağlamak hem de gezegenimiz için pozitif bir etki yaratmak istiyoruz.
Bu vizyon, üzerine DDD prensiplerini uygulayacağımız zengin, karmaşık ve anlamlı bir problem alanı sunmaktadır. Sonraki adımlarda, bu vizyonu nasıl stratejik ve taktiksel tasarımlara dönüştüreceğimizi adım adım göreceğiz.
"Yeşil Rota" platformumuzun vizyonu artık net. Şimdi bu vizyonu, DDD'nin en güçlü stratejik araçlarını kullanarak somut bir mimari plana dönüştürme zamanı. Projemizin iskeletini, yani savaş haritasını çizeceğiz.
————————
10.2. Stratejik Tasarım
Stratejik tasarımın amacı, büyük ve karmaşık bir problemi, yönetilebilir, bağımsız ve odaklanmış parçalara ayırmak ve bu parçalar arasındaki ilişkileri netleştirmektir.
Bounded Context'leri (Sınırlı Bağlamları) Belirleme
"Yeşil Rota" platformunun vizyonunu incelediğimizde, birbiriyle ilişkili ama farklı sorumluluklara sahip olan dört ana iş alanı (subdomain) ortaya çıkıyor. Her bir subdomain, kendi diline ve kendi modeline sahip olacak bir Bounded Context'e karşılık gelecektir.
1. Shipment (Gönderi) Context:
- Sorumluluğu: Bu, bizim Core Domain'imizdir. Platformun varoluş sebebidir. Bir paketin alınma talebinden, en optimal rotanın hesaplanmasına, araca atanmasına ve son kullanıcıya teslim edilmesine kadar olan tüm yaşam döngüsünü yönetir.
- Evrensel Dili (Ubiquitous Language): ShipmentRequest (Gönderi Talebi), PackageDetails (Paket Detayları), OptimalRoute (Optimal Rota), DeliveryStatus (Teslimat Durumu: PENDING, SCHEDULED, IN_TRANSIT, DELIVERED), ProofOfDelivery (Teslimat Kanıtı).
2. FleetManagement (Filo Yönetimi) Context:
- Sorumluluğu: Bu, Core Domain'i destekleyen kritik bir Supporting Subdomain'dir. Otonom araçların (drone, yer aracı) fiziksel durumunu yönetir. Araçların anlık konumu, batarya seviyesi, bakım takvimi ve operasyonel durumu (IDLE, CHARGING, MAINTENANCE) gibi konularla ilgilenir.
- Evrensel Dili: Vehicle (Araç), Drone, AGV, BatteryLevel (Batarya Seviyesi), VehicleStatus (Araç Durumu), TelemetryData (Telemetri Verisi).
3. Billing (Faturalandırma) Context:
- Sorumluluğu: Bu da bir Supporting Subdomain'dir. Başarıyla tamamlanan her teslimat için maliyetin hesaplanması, faturaların oluşturulması ve müşteri ödemelerinin takibi ile ilgilenir.
- Evrensel Dili: Invoice (Fatura), Price (Fiyat), CostCalculation (Maliyet Hesabı), Transaction (Finansal İşlem), PaymentStatus (Ödeme Durumu).
4. Customer (Müşteri) Context:
- Sorumluluğu: Bu, genellikle bir Generic Subdomain'dir. Platformu kullanan müşterilerin kimlik bilgilerini, hesaplarını, adres defterlerini ve iletişim tercihlerini yönetir. Kullanıcı kaydı, kimlik doğrulama gibi standart işleri yapar.
- Evrensel Dili: CustomerProfile (Müşteri Profili), UserAccount (Kullanıcı Hesabı), Authentication (Kimlik Doğrulama), AddressBook (Adres Defteri).
Context Map (Bağlam Haritası) Çizimi
Bu dört Bounded Context'in birbirinden tamamen izole yaşayamayacağı açıktır. Birbirleriyle konuşmak zorundadırlar. Context Map, bu konuşmanın kurallarını ve diplomatik ilişkilerini tanımlar.
Aşağıda "Yeşil Rota" platformunun Bağlam Haritası'nı ve ilişkilerin açıklamasını bulabilirsiniz:
İlişkilerin Açıklaması:
- Shipment ve FleetManagement İlişkisi:
- Desen: Customer-Supplier (Müşteri-Tedarikçi). Shipment Context'i (Müşteri), bir gönderiyi atamak için uygun bir araca ihtiyaç duyar. FleetManagement Context'i (Tedarikçi) ise bu araç bilgisini sağlar.
- Entegrasyon: FleetManagement, dış dünyaya "bana uygun bir araç bul" gibi servislerini bir Open Host Service (OHS) ile sunar. Bu servisin kullandığı dil (VehicleStatusDto vb.) ise bir Published Language (PL)'dir. Shipment context'i bu API'yi kullanarak ihtiyacı olan aracı talep eder.
- Shipment ve Billing İlişkisi:
- Desen: Upstream/Downstream (Yukarı Akış/Aşağı Akış) + Anticorruption Layer (ACL). Shipment context'i (Upstream), bir işi tamamladığında bunu dünyaya duyurur. Billing context'i (Downstream) ise bu bilgiyi dinleyerek kendi işini yapar.
- Entegrasyon: Shipment context'i, bir teslimat tamamlandığında ShipmentDeliveredEvent adında bir Domain Event yayınlar. Billing context'i, bu olayı doğrudan kullanmak yerine, kendi iç modelini "kirletmemek" için bir Anticorruption Layer (ACL) kullanır. Bu ACL, ShipmentDeliveredEvent'i alır ve onu Billing dünyasının anladığı CreateInvoiceCommand gibi bir komuta veya BillableActivityOccurredEvent gibi kendi olayına dönüştürür. Bu, iki sistem arasında gevşek bağlı (loosely coupled) bir ilişki sağlar.
- Shipment ve Customer İlişkisi:
- Desen: Customer-Supplier + Anticorruption Layer (ACL). Shipment, bir gönderi oluşturmak için gönderici ve alıcı bilgilerine ihtiyaç duyar. Bu bilgileri Customer context'inden alır.
- Entegrasyon: Customer context'i, bir Open Host Service (OHS) aracılığıyla findCustomerById gibi basit bir servis sunar. Shipment context'i, Customer context'inin tüm karmaşık modelini (parola bilgisi, hesap ayarları vb.) kendi içine almak istemez. Bu yüzden, Customer API'sından gelen veriyi, kendi ihtiyacı olan (customerId, name, address gibi) basit bir Sender (Gönderici) nesnesine dönüştüren bir Anticorruption Layer (ACL) kullanır.
- Billing ve Customer İlişkisi:
- Desen: Shipment ile Customer arasındaki ilişkinin aynısıdır. Billing context'i de bir fatura oluşturmak ve göndermek için müşteri bilgilerine ihtiyaç duyar ve bu bilgiyi Customer context'inden bir ACL aracılığıyla alır.
Bu stratejik harita, bize projenin genel mimarisini, takımların birbirleriyle nasıl iletişim kurması gerektiğini ve nerede hangi entegrasyon desenini kullanacağımızı net bir şekilde gösterir. Artık bu haritayı takip ederek Taktiksel Tasarım'a, yani kodun içindeki güzelliklere geçebiliriz.
Stratejik haritamızı çizdikten sonra, şimdi projemizin kalbi olan Shipment (Gönderi) Context'ine yakından bakma ve onu Taktiksel Tasarım'ın yapı taşlarıyla inşa etme zamanı. Bu, vizyonumuzun koda dönüştüğü yerdir.
————————
10.3. Taktiksel Tasarım: Shipment Context'ine Derinlemesine Bakış
Bu bölümde, Shipment Bounded Context'inin içindeki temel nesneleri, yani Aggregate'leri, Entity'leri, Value Object'leri ve Domain Event'leri tasarlayacağız.
Önemli Bir Not: Aggregate Sınırları
Başlarken, prompt'ta yer alan Drone'un bir Aggregate olduğu fikrini netleştirelim. DDD'de en önemli kararlardan biri, Aggregate sınırlarını doğru çizmektir.
- Drone (veya daha genel adıyla Vehicle), fiziksel bir varlıktır. Onun batarya durumu, konumu, bakım geçmişi gibi özellikleri vardır. Bu sorumluluklar, bizim stratejik haritamızda FleetManagement (Filo Yönetimi) Context'ine aittir.
- Shipment (Gönderi) ise bir iş sürecidir. Bir paketin bir yerden bir yere taşınmasıdır.
Bu nedenle, Drone veya Vehicle, Shipment Context'inin içinde bir Aggregate olamaz. Shipment Context'i, bir Vehicle'ı yönetmez; sadece o anki gönderi için bir Vehicle'ı kullanır. Bu yüzden Shipment Aggregate'i, Vehicle'ın kendisine değil, sadece onun kimliğine (VehicleId) bir referans tutacaktır. Bu, iki bağlam arasındaki sorumluluk ayrımını korumak için hayati bir kuraldır.
Shipment Context'indeki tek Aggregate'imiz, Shipment'in kendisidir.
————————
Shipment Aggregate'i
Bu, tüm iş mantığının merkezidir. Bir gönderinin yaşam döngüsünü ve kurallarını yönetir.
- Aggregate Root: Shipment
Shipment Aggregate'ini Oluşturan Yapı Taşları:
Value Objects (Değer Nesneleri)
Modelimizi zenginleştiren, değiştirilemez ve kimliği olmayan küçük yapı taşlarıdır.
- ShipmentId: Gönderinin eşsiz kimliği.
- public record ShipmentId(UUID value) { }
- PackageDetails: Paketin fiziksel özellikleri. Başka Value Object'ler içerebilir.
- // Weight ve Dimensions da kendi başlarına birer Value Object'tir.
- public record PackageDetails(Weight weight, Dimensions dimensions, boolean isFragile) { }
- public record Weight(double value, WeightUnit unit) { }
- public record Dimensions(double width, double height, double depth, LengthUnit unit) { }
- Route: Gönderinin başlangıç ve varış noktası.
- public record Route(Address origin, Address destination) { }
- Address: Tam bir adres bilgisi.
- public record Address(String street, String city, String zipCode, String country) { }
- VehicleId: FleetManagement Context'indeki bir araca olan referans.
- public record VehicleId(UUID value) { }
Entity (Varlık)
Modelimizdeki tek ana varlık, Aggregate Root'un kendisidir.
- Shipment: Kimliği (ShipmentId) olan ve yaşam döngüsü boyunca durumu değişen ana nesnemiz.
Shipment.java (Aggregate Root Taslağı)
public class Shipment {
private final ShipmentId id;
private final CustomerId senderId;
private final PackageDetails packageDetails;
private final Route route;
private ShipmentStatus status;
private VehicleId assignedVehicleId; // Atanan aracın sadece kimliğini tutuyoruz.
// Constructor...
// --- Davranışlar (İş Mantığı) ---
// Gönderiyi bir araca planlar.
public void scheduleFor(VehicleId vehicleId) {
if (this.status != ShipmentStatus.PENDING) {
throw new IllegalStateException("Shipment can only be scheduled if it is pending.");
}
this.assignedVehicleId = vehicleId;
this.status = ShipmentStatus.SCHEDULED;
// Burada bir Domain Event yayınlanabilir: ShipmentScheduledEvent
}
// Gönderiyi yola çıkarır.
public void dispatch() {
if (this.status != ShipmentStatus.SCHEDULED || this.assignedVehicleId == null) {
throw new IllegalStateException("Shipment must be scheduled and have an assigned vehicle to be dispatched.");
}
this.status = ShipmentStatus.IN_TRANSIT;
// Olay yayınla!
DomainEvents.publish(new VehicleDispatchedEvent(this.id, this.assignedVehicleId));
}
// Gönderiyi teslim eder.
public void deliver(ProofOfDelivery proof) {
if (this.status != ShipmentStatus.IN_TRANSIT) {
throw new IllegalStateException("Shipment cannot be delivered if it is not in transit.");
}
this.status = ShipmentStatus.DELIVERED;
// Olay yayınla! Bu olayı Billing Context'i dinleyecek.
DomainEvents.publish(new ShipmentDeliveredEvent(this.id, proof.getDeliveryTimestamp()));
}
}
————————
Domain Event'ler
Shipment Aggregate'inin yaşam döngüsündeki önemli, geri alınamaz anları temsil ederler.
- ShipmentCreatedEvent: Yeni bir gönderi talebi sisteme ilk girdiğinde yayınlanır.
- public record ShipmentCreatedEvent(
- ShipmentId shipmentId,
- CustomerId senderId,
- PackageDetails packageDetails,
- Route route
- ) { }
- VehicleDispatchedEvent: Gönderi yola çıktığında yayınlanır. Müşteriye bildirim göndermek gibi işler için kullanılabilir. (Prompt'taki DroneDispatched'in daha genel hali)
- public record VehicleDispatchedEvent(
- ShipmentId shipmentId,
- VehicleId vehicleId
- ) { }
- ShipmentDeliveredEvent: Teslimat başarıyla tamamlandığında yayınlanır. Bu, Billing Context'inin faturayı oluşturmasını tetikleyecek en kritik olaydır.
- public record ShipmentDeliveredEvent(
- ShipmentId shipmentId,
- LocalDateTime deliveryTimestamp,
- ProofOfDelivery proof // Teslimat kanıtı (fotoğraf URL'si, imza vb.)
- ) { }
Bu taktiksel tasarım, Shipment Context'inin kendi sınırları içinde son derece tutarlı ve odaklanmış kalmasını sağlar. Diğer bağlamlarla olan tüm iletişimi, VehicleId gibi basit kimlik referansları ve tüm dünyaya yayınlanan Domain Event'ler üzerinden kurarak esnek ve yönetilebilir bir yapı oluşturur.
Şimdi kollarımızı sıvayıp teoriyi koda dökme zamanı. Önceki bölümlerde tasarladığımız Shipment (Gönderi) Context'ini, Hexagonal Architecture prensiplerine sadık kalarak Spring Boot, Java 21, JPA ve PostgreSQL kullanarak adım adım hayata geçireceğiz.
————————
10.4. Kodlama: Shipment Servisini Adım Adım Geliştirme
Bu bölümde, bir gönderi oluşturma (createShipment) senaryosunu baştan sona kodlayacağız. Bu süreç, katmanlar arasındaki etkileşimi ve sorumlulukların nasıl ayrıldığını net bir şekilde gösterecek.
————————
Adım 1: Proje Kurulumu ve Bağımlılıklar
Öncelikle projemizin veritabanı ile konuşabilmesi için gerekli bağımlılıkları ve ayarları yapmamız gerekiyor.
1. pom.xml Dosyasına Bağımlılıkları Ekleme:
Mevcut projemize Spring Data JPA ve PostgreSQL sürücüsünü ekliyoruz.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
2. application.properties Dosyasını Yapılandırma:
src/main/resources altındaki bu dosyaya, Spring Boot'a PostgreSQL veritabanına nasıl bağlanacağını söylüyoruz.
# PostgreSQL Veritabanı Bağlantı Ayarları
spring.datasource.url=jdbc:postgresql://localhost:5432/yesil_rota_db
spring.datasource.username=postgres
spring.datasource.password=sifreniz
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA ve Hibernate Ayarları
spring.jpa.hibernate.ddl-auto=update # Geliştirme için 'update', production için 'validate' veya 'none' kullanın
spring.jpa.show-sql=true # Çalışan SQL sorgularını konsolda göster
spring.jpa.properties.hibernate.format_sql=true
————————
Adım 2: Domain Katmanını Kodlama (Teknoloji Bağımsız Kalp)
Bu katmanda hiçbir JPA veya Spring anotasyonu bulunmaz. Burası saf iş mantığıdır. (Bu kodları 10.3'te tasarlamıştık, şimdi son hallerini görüyoruz.)
shipment/domain/model/aggregate/Shipment.java
// Saf Java sınıfı, hiçbir dış bağımlılık yok.
public class Shipment {
private final ShipmentId id;
private ShipmentStatus status;
// ... diğer domain alanları ve iş kurallarını içeren metodlar ...
public static Shipment createNew(CustomerId senderId, Route route, PackageDetails details) {
// Factory metodu ile yeni bir gönderi oluşturma mantığı
// ...
return new Shipment(/*...*/);
}
}
shipment/domain/repository/ShipmentRepository.java
// Bizim tanımladığımız, teknolojiden bağımsız kontrat (Port)
public interface ShipmentRepository {
void save(Shipment shipment);
Optional<Shipment> findById(ShipmentId shipmentId);
}
————————
Adım 3: Infrastructure Katmanını Kodlama (Teknoloji Adaptörleri)
İşte burada veritabanı teknolojisi (JPA/PostgreSQL) devreye girer. Domain katmanındaki saf nesneler ile veritabanı tabloları arasındaki köprüyü kurarız.
1. ShipmentJpaEntity Oluşturma (Veritabanı Modeli):
Bu sınıf, veritabanındaki shipments tablosunu temsil eder ve JPA anotasyonlarını içerir. Domain modelimizi temiz tutmak için bu ayrımı yaparız.
shipment/infrastructure/adapter/out/persistence/entity/ShipmentJpaEntity.java
import jakarta.persistence.*;
@Entity
@Table(name = "shipments")
public class ShipmentJpaEntity {
@Id
private UUID id;
@Enumerated(EnumType.STRING)
private ShipmentStatus status;
// ... Diğer kolonlar ve @Embedded ile Value Object'ler ...
}
2. ShipmentMapper Oluşturma (Dönüştürücü):
Domain nesnesi ile JPA nesnesi arasında dönüşümü yapan bir yardımcı sınıf.
shipment/infrastructure/adapter/out/persistence/mapper/ShipmentMapper.java
@Component
public class ShipmentMapper {
public ShipmentJpaEntity toJpaEntity(Shipment shipment) { /* ... dönüşüm mantığı ... */ }
public Shipment toDomainEntity(ShipmentJpaEntity jpaEntity) { /* ... dönüşüm mantığı ... */ }
}
3. ShipmentRepository Implementasyonu (Adaptör):
Domain katmanında tanımladığımız ShipmentRepository portunu (arayüzünü) implemente eden somut sınıf.
shipment/infrastructure/adapter/out/persistence/ShipmentRepositoryImpl.java
import org.springframework.stereotype.Repository;
@Repository // Bu bir Spring bileşeni ve altyapı adaptörüdür.
public class ShipmentRepositoryImpl implements ShipmentRepository {
private final SpringDataShipmentRepository jpaRepository; // Spring Data'nın sihirli arayüzü
private final ShipmentMapper mapper;
public ShipmentRepositoryImpl(SpringDataShipmentRepository jpaRepository, ShipmentMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public void save(Shipment shipment) {
ShipmentJpaEntity jpaEntity = mapper.toJpaEntity(shipment);
jpaRepository.save(jpaEntity);
}
// ... findById implementasyonu ...
}
// Spring Data'nın bizim için dolduracağı arayüz
interface SpringDataShipmentRepository extends JpaRepository<ShipmentJpaEntity, UUID> {}
————————
Adım 4: Application ve Web Katmanlarını Kodlama
Artık altyapı hazır olduğuna göre, dış dünyadan gelen istekleri karşılayabiliriz.
shipment/application/service/ShipmentApplicationService.java (Orkestra Şefi)
@Service
public class ShipmentApplicationService {
private final ShipmentRepository shipmentRepository; // <<-- DOMAIN INTERFACE'ine bağımlı
// ... eventPublisher vs.
public ShipmentApplicationService(ShipmentRepository shipmentRepository) {
this.shipmentRepository = shipmentRepository;
}
@Transactional
public ShipmentId createShipment(CreateShipmentRequest dto) {
// DTO'dan domain nesneleri yaratılır
Shipment shipment = Shipment.createNew(/*...*/);
shipmentRepository.save(shipment);
// Domain Event yayınlanır...
return shipment.getId();
}
}
shipment/infrastructure/adapter/in/web/ShipmentController.java (Giriş Kapısı)
@RestController
@RequestMapping("/api/v1/shipments")
public class ShipmentController {
private final ShipmentApplicationService shipmentService;
public ShipmentController(ShipmentApplicationService shipmentService) {
this.shipmentService = shipmentService;
}
@PostMapping
public ResponseEntity<ShipmentId> createShipment(@RequestBody CreateShipmentRequest request) {
ShipmentId newShipmentId = shipmentService.createShipment(request);
return new ResponseEntity<>(newShipmentId, HttpStatus.CREATED);
}
}
Bu adımlarla, Hexagonal Architecture'a uygun, katmanları birbirinden net bir şekilde ayrılmış, test edilebilir ve esnek bir servis geliştirmiş olduk. Domain katmanımız teknoloji kirliliğinden korunurken, Infrastructure katmanı tüm teknik detayları üstlenir. Application katmanı ise bu iki dünya arasında orkestra şefi görevi görür.
Çalışan bir servisimiz var. Peki onun doğru çalıştığından, gelecekte yapacağımız değişikliklerle bozulmayacağından nasıl emin olacağız? Cevap, elbette, test yazmakta. DDD ve katmanlı mimari, test yazmayı bir angarya olmaktan çıkarıp, keyifli ve son derece odaklı bir aktiviteye dönüştürür.
————————
10.5. Test: DDD'de Testin Önemi
DDD ile kurduğumuz mimarinin en büyük faydalarından biri, test edilebilirliğidir. Katmanları birbirinden net bir şekilde ayırdığımız için, her bir parçayı kendi başına, izole bir şekilde test edebiliriz. Bu, hataları çok daha hızlı bulmamızı ve daha güvenilir kod yazmamızı sağlar.
Test stratejimizi iki ana kategoriye ayıracağız:
- Birim Testleri (Unit Tests): Tek bir sınıfın veya metodun, dış dünyadan tamamen izole bir şekilde, sadece kendi iç mantığının doğru çalışıp çalışmadığını kontrol eder. Çok hızlı çalışırlar.
- Entegrasyon Testleri (Integration Tests): Birden fazla bileşenin (örneğin, servis katmanının veritabanıyla konuşması gibi) bir araya geldiğinde doğru çalışıp çalışmadığını kontrol eder. Daha yavaş çalışırlar ama sistemin bütünlüğü hakkında daha fazla güvence verirler.
Birim Testleri: Her Parçayı Mercek Altına Almak
1. Domain Katmanını Test Etmek (En Önemlisi!)
Bu, testin en saf ve en değerli olduğu yerdir. Domain katmanımızın hiçbir dış bağımlılığı olmadığı için (Spring, JPA yok), onu saf Java nesneleri olarak test edebiliriz. Burada iş kurallarını test ederiz.
- Senaryo: Shipment (Gönderi) SCHEDULED (Planlandı) durumunda değilse, dispatch() (yola çıkar) metodunun bir IllegalStateException fırlatması gerektiğini test edelim.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ShipmentTest {
@Test
void givenPendingShipment_whenDispatch_thenThrowException() {
// Arrange (Hazırlık)
// Yeni oluşturulmuş bir gönderi, PENDING durumundadır.
Shipment shipment = Shipment.createNew(/*...*/);
// Act & Assert (Eylem & Doğrulama)
// dispatch() metodunun IllegalStateException fırlattığını doğrula.
assertThrows(IllegalStateException.class, () -> {
shipment.dispatch();
});
}
@Test
void givenScheduledShipment_whenDispatch_thenStatusShouldBeInTransit() {
// Arrange (Hazırlık)
Shipment shipment = Shipment.createNew(/*...*/);
VehicleId vehicleId = new VehicleId(UUID.randomUUID());
shipment.scheduleFor(vehicleId); // Durumu SCHEDULED yap.
// Act (Eylem)
shipment.dispatch();
// Assert (Doğrulama)
// Durumun doğru şekilde değiştiğini kontrol et.
assertEquals(ShipmentStatus.IN_TRANSIT, shipment.getStatus());
}
}
2. Application Servislerini Test Etmek
Application servislerini test ederken, onların bağımlı olduğu Repository gibi altyapı bileşenlerinin "taklitlerini" (mock) kullanırız. Amacımız, veritabanına gerçekten gitmek değil, Application Service'in doğru Repository metodunu doğru parametrelerle çağırıp çağırmadığını kontrol etmektir. Bunun için Mockito gibi bir kütüphane kullanırız.
- Senaryo: ShipmentApplicationService'in, createShipment metodu çağrıldığında shipmentRepository.save() metodunu çağırdığını test edelim.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ShipmentApplicationServiceTest {
@Mock // Bu, sahte bir ShipmentRepository nesnesi oluşturur.
private ShipmentRepository shipmentRepository;
@InjectMocks // Bu, sahte repository'yi kullanarak gerçek bir servis nesnesi oluşturur.
private ShipmentApplicationService shipmentService;
@Test
void whenCreateShipment_thenRepositorySaveShouldBeCalled() {
// Arrange (Hazırlık)
CreateShipmentRequest request = new CreateShipmentRequest(/*...*/);
// Act (Eylem)
shipmentService.createShipment(request);
// Assert (Doğrulama)
// shipmentRepository.save() metodunun herhangi bir Shipment nesnesiyle
// tam olarak 1 kere çağrıldığını doğrula.
verify(shipmentRepository, times(1)).save(any(Shipment.class));
}
}
Entegrasyon Testleri: Parçaları Birleştirmek
1. Persistence (Repository) Katmanını Test Etmek
Buradaki amacımız, Shipment domain nesnemizin, ShipmentJpaEntity'ye doğru bir şekilde maplenip PostgreSQL veritabanına kaydedildiğini ve doğru bir şekilde geri okunduğunu test etmektir. Spring Boot, @DataJpaTest anotasyonu ile bu işi çok kolaylaştırır. Bu testler genellikle gerçek veritabanı yerine H2 gibi bir bellek-içi (in-memory) veritabanı veya Testcontainers ile ayağa kaldırılan geçici bir PostgreSQL container'ı üzerinde çalışır.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
class ShipmentRepositoryImplTest {
@Autowired
private SpringDataShipmentRepository jpaRepository; // Gerçek Spring Data arayüzü
private ShipmentRepository shipmentRepository; // Test edeceğimiz kendi repository implementasyonumuz
// ... setup metodu ...
@Test
void givenShipment_whenSaveAndFindById_thenReturnSameShipment() {
// Arrange (Hazırlık)
Shipment shipment = Shipment.createNew(/*...*/);
// Act (Eylem)
shipmentRepository.save(shipment);
Optional<Shipment> foundShipment = shipmentRepository.findById(shipment.getId());
// Assert (Doğrulama)
assertTrue(foundShipment.isPresent());
assertEquals(shipment.getId(), foundShipment.get().getId());
}
}
2. Web (Controller) Katmanını Test Etmek
Bu testin amacı, ShipmentController'a bir HTTP isteği geldiğinde, doğru HTTP durum kodunu (201 CREATED gibi) ve doğru cevabı dönüp dönmediğini kontrol etmektir. Servis katmanını mock'layarak sadece Controller'ın sorumluluğunu test ederiz.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ShipmentController.class)
class ShipmentControllerTest {
@Autowired
private MockMvc mockMvc; // Sahte HTTP istekleri göndermemizi sağlayan araç
@MockBean // Application katmanını taklit et
private ShipmentApplicationService shipmentService;
// ...
@Test
void whenPostRequestToCreateShipment_thenReturns201Created() throws Exception {
// Arrange (Hazırlık)
String shipmentJson = "{\"senderId\":\"...\", ...}";
// Servis çağrıldığında ne döneceğini belirle
when(shipmentService.createShipment(any())).thenReturn(new ShipmentId(UUID.randomUUID()));
// Act & Assert (Eylem & Doğrulama)
mockMvc.perform(post("/api/v1/shipments")
.contentType("application/json")
.content(shipmentJson))
.andExpect(status().isCreated()); // HTTP 201 durumunu bekle
}
}
Bu katmanlı test yaklaşımı, size her seviyede güven verir. İş kurallarınızın doğru çalıştığından, veritabanı işlemlerinizin sorunsuz olduğundan ve API'larınızın beklendiği gibi davrandığından emin olursunuz.
Bağlam içerisinde sorulabilecek en önemli sorulardan birine geldik. Bu, DDD yolculuğuna çıkan birçok ekibin kafasını karıştıran, modern yazılım mimarisinin en önemli tartışma konularından biridir.
————————
11.1. Her Bounded Context Bir Mikroservis mi Olmalı? Yaygın Bir Yanılgı ve Doğrusu
Bu sorunun kısa ve net cevabı: Hayır, zorunda değil.
Bu "her Bounded Context bir mikroservistir" fikri, son derece yaygın ve tehlikeli bir yanılgıdır. Bu iki kavram birbiriyle çok yakından ilişkili olsa da aynı şey değildir. Aralarındaki farkı anlamak, başarılı bir sistem mimarisi kurmanın anahtarıdır.
Temel Fark: Mantıksal Sınır vs. Fiziksel Sınır
- Bounded Context (Sınırlı Bağlam): Bu, mantıksal bir sınırdır. Bir modelin, bir dilin (Ubiquitous Language) tutarlı olduğu, kavramsal bir çerçevedir. Stratejik tasarım sırasında, iş domain'ini daha iyi anlamak ve modellemek için çizdiğimiz bir sınırdır. Henüz kodun nerede ve nasıl çalışacağıyla ilgilenmez.
- Mikroservis: Bu, fiziksel bir sınırdır. Kendi sürecinde (process) çalışan, bağımsız olarak deploy edilebilen (dağıtılabilen) bir kod paketidir. Bir mimari karardır ve dağıtım (deployment) ile ilgilidir.
Özetle, Bounded Context probleminizi nasıl böldüğünüzle, mikroservis ise çözümünüzü nasıl deploy ettiğinizle ilgilidir.
İlişki Nasıl Olmalı?
Bir Bounded Context, bir mikroservis için ideal bir başlangıç noktası ve güçlü bir adaydır. Çünkü Bounded Context'in amacı olan "yüksek uyum (high cohesion)" ve "düşük bağlılık (low coupling)" prensipleri, mikroservislerin de temel hedefleridir.
Ancak birebir eşleşme her zaman doğru veya gerekli değildir. Gerçek hayatta üç farklı senaryo ile karşılaşabiliriz:
Senaryo 1: Bir Bounded Context = Bir Mikroservis (İdeal Durum)
Bu en yaygın ve genellikle en çok istenen durumdur. "Yeşil Rota" örneğimizdeki Shipment (Gönderi) Context'i gibi karmaşık ve işin kalbinde yer alan bir bağlam, kendi veritabanına sahip, bağımsız bir ShipmentService mikroservisi olarak hayata geçirilebilir. Billing (Faturalandırma) Context'i de kendi BillingService mikroservisi olabilir. Bu, takımların otonom çalışması için harika bir modeldir.
Senaryo 2: Bir Bounded Context > Birden Fazla Mikroservis (Bölünme)
Bazen bir Bounded Context, zamanla o kadar büyür ve farklı operasyonel ihtiyaçlara sahip olur ki, onu tek bir servis olarak çalıştırmak mantıksız hale gelir.
- Örnek: "Yeşil Rota"daki Shipment Context'ini düşünelim.
- Komut (Command) tarafı: Yeni gönderi oluşturma, atama yapma gibi işlemler, yüksek tutarlılık gerektirir ve belki de günde on binlerce kez çalışır.
- Sorgu (Query) tarafı: Müşterilerin anlık kargo takibi yapması, milyonlarca anlık okuma isteği yaratabilir. Ayrıca, yöneticiler için karmaşık analitik raporlar hazırlayan bir tarafı da olabilir.
Bu durumda, tek bir Shipment Context'ini, farklı ihtiyaçlara göre optimize edilmiş birden fazla servise bölmek mantıklı olabilir:
- ShipmentCommandService: Yazma operasyonlarını yöneten servis.
- ShipmentQueryService: Anlık kargo takibi için okuma operasyonlarını yöneten, belki de Redis gibi hızlı bir veritabanı kullanan servis.
- ShipmentAnalyticsService: Raporlama için veri ambarı üzerinde çalışan ayrı bir servis.
Hepsi aynı dili ve aynı mantıksal modeli paylaşır (yani aynı Bounded Context'tedirler), ancak farklı fiziksel birimler olarak deploy edilirler.
Senaryo 3: Birden Fazla Bounded Context > Tek Bir Mikroservis (Birleşme - Monolith)
Özellikle projenin başlangıcında, her bir Bounded Context için ayrı bir mikroservis yaratmak, altyapı yönetimi, ağ iletişimi ve deployment karmaşıklığı açısından aşırı bir yük (overhead) getirebilir.
- Örnek: Customer (Müşteri) ve Billing (Faturalandırma) bağlamlarımızı düşünelim. Belki de projenin ilk aşamasında bu iki bağlam o kadar basit ve küçüktür ki, onları tek bir uygulama (örneğin, CustomerAndBillingService) içinde iki ayrı paket (customer ve billing) olarak geliştirmek daha mantıklıdır. Bu yapıya Modüler Monolit denir.
Bu yaklaşımda, bağlamlar mantıksal olarak hala ayrıdır (farklı paketler, farklı modeller), ancak fiziksel olarak aynı deploy edilebilir birimin içinde yaşarlar. Gelecekte Billing context'i çok büyürse ve kendi ekibine ihtiyaç duyarsa, bu modüler yapı sayesinde onu monolit içinden ayırıp kendi mikroservisine dönüştürmek çok daha kolay olur.
Sonuç: Bir Bounded Context'i bir mikroservis için başlangıç noktası olarak görün, ancak bu kuralın sizi kör etmesine izin vermeyin. Kararınızı; takım yapısı, işin karmaşıklığı, operasyonel gereksinimler ve projenin bulunduğu evre gibi gerçek dünya faktörlerine göre verin. Önce mantıksal sınırları doğru çizin (Stratejik Tasarım), sonra bu sınırları nasıl deploy edeceğinize karar verin.
Mikroservisler arası iletişim, senkron (eş zamanlı) ve asenkron (eş zamanlı olmayan) olmak üzere iki ana yolla yapılır ve doğru yöntemi seçmek, sisteminizin esnekliği ve dayanıklılığı için kritik öneme sahiptir.
————————
- 11.2. Servisler Arası İletişim: Senkron (REST) ve Asenkron (RabbitMQ/Kafka) İletişim.
Senkron İletişim: REST API 📞
Senkron iletişim, bir servisin başka bir servisi doğrudan çağırıp ondan bir cevap beklemesi prensibine dayanır. Bu, bir telefon görüşmesi gibidir: Birini ararsınız, o telefonu açar, sorunuzu sorarsınız ve cevap verene kadar hatta beklersiniz. Cevabı almadan işinize devam edemezsiniz. Mikroservis dünyasında bu yaklaşımın en popüler uygulaması REST API'larıdır.
- Örnek: "Yeşil Rota" platformumuzda, Shipment (Gönderi) servisinin, bir paketi atamak için o an müsait bir araca ihtiyacı var.
- Shipment servisi, FleetManagement (Filo) servisinin /api/vehicles/findAvailable REST endpoint'ine bir HTTP isteği gönderir.
- Shipment servisi, FleetManagement servisinden "İşte müsait aracın ID'si: drone-xyz" cevabı gelene kadar bloke olur ve bekler.
- Cevap geldikten sonra, Shipment servisi bu araç ID'sini kullanarak kendi işine devam eder.
Avantajları
- Basitlik: Mantığı anlamak ve uygulamak kolaydır. İstek at, cevap bekle.
- Anında Geri Bildirim: Çağrılan serviste bir hata oluşursa, çağıran servis bu hatadan anında haberdar olur ve bir HTTP 500 - Internal Server Error gibi bir cevap alır.
Dezavantajları
- Düşük Dayanıklılık (Resilience): Eğer FleetManagement servisi o an kapalıysa veya yavaş çalışıyorsa, Shipment servisi de başarısız olur veya yavaşlar. Bu zincirleme hata (cascading failure) riskini doğurur.
- Sıkı Zamansal Bağlılık (Temporal Coupling): İki servisin de aynı anda ayakta ve çalışır durumda olması gerekir.
————————
Asenkron İletişim: Mesajlaşma Kuyrukları (RabbitMQ/Kafka) asynchronously
Asenkron iletişim, servislerin birbirleriyle doğrudan konuşmadığı, bunun yerine mesajlarını mesaj aracısı (message broker) adı verilen ortak bir posta kutusuna bıraktığı bir modele dayanır. Bu, bir e-posta veya anlık mesajlaşma gibidir: Mesajınızı gönderirsiniz ve kendi işinize devam edersiniz. Alıcının o an çevrimiçi olup olmadığını, mesajı ne zaman okuyacağını veya okuduktan sonra ne yapacağını umursamazsınız.
- Örnek: Shipment servisi, bir paketi başarıyla teslim ettiğinde bu durumu diğer servislere bildirmek ister.
- Shipment servisi, içinde shipmentId ve deliveryTime gibi bilgilerin olduğu bir ShipmentDeliveredEvent (Gönderi Teslim Edildi Olayı) oluşturur.
- Bu olay mesajını, RabbitMQ veya Kafka gibi bir mesaj aracısının ilgili "topic" veya "exchange"ine gönderir ve kendi işini bitirir.
- Billing (Faturalandırma) servisi ve Notification (Bildirim) servisi, bu topic'i dinlemektedir. Mesaj geldiğinde, her ikisi de mesajın bir kopyasını alır ve birbirlerinden tamamen habersiz ve bağımsız olarak kendi işlerini yaparlar. Billing servisi faturayı oluştururken, Notification servisi de müşteriye "Paketiniz teslim edildi!" e-postası gönderir.
Avantajları
- Yüksek Dayanıklılık: Billing servisi o an kapalı olsa bile, Shipment servisi işini sorunsuz tamamlar. Mesaj, Billing servisi tekrar ayağa kalkana kadar mesaj aracısında güvenle bekler. Sistemler birbirlerinin anlık durumlarından etkilenmez.
- Gevşek Bağlılık (Loose Coupling): Shipment servisi, mesajını kimin dinlediğini bilmek zorunda değildir. Gelecekte AnalyticsService adında yeni bir servis ekleyip aynı olayı dinlemesini sağlamak, mevcut sisteme hiç dokunmadan yapılabilir.
- Ölçeklenebilirlik: Eğer fatura oluşturma işlemi çok yoğunlaşırsa, sadece Billing servisinin dinleyici sayısını artırarak yükü kolayca dağıtabilirsiniz.
Dezavantajları
- Artan Karmaşıklık: Araya bir mesaj aracısı girdiği için sistemin kurulumu ve yönetimi daha karmaşıktır.
- Nihai Tutarlılık (Eventual Consistency): Veri, sistemin farklı parçalarında anında değil, bir süre sonra tutarlı hale gelir. Müşteri, teslimat bildirimini faturası oluşmadan birkaç milisaniye önce alabilir. Bu durumun iş kuralları açısından kabul edilebilir olması gerekir.
Ne Zaman Hangisini Kullanmalı?
Modern sistemler genellikle hibrit bir yaklaşım kullanır:
- Bir işlem için başka bir servisten anında bir veri almanız gerekiyorsa (örn: "bana müsait bir araç ver"), senkron (REST) iletişim daha uygundur.
- Bir işlemin sonucunu başka servislere sadece haber vermek istiyorsanız ve anında bir cevaba ihtiyacınız yoksa (örn: "sipariş teslim edildi, bilginiz olsun"), asenkron (mesajlaşma) iletişim çok daha güçlü ve dayanıklıdır.
Dağıtık sistemlerin en zorlu problemlerinden birine ve onun en popüler çözümüne geldik: Tutarlılık ve Saga Pattern.
————————
11.3. Dağıtık Sistemlerde Tutarlılık: Saga Pattern'ine Giriş
Mikroservis mimarisinde, her servis genellikle kendi veritabanına sahiptir. Peki, birden fazla servisi ilgilendiren bir iş akışında veri tutarlılığını nasıl sağlayacağız?
Problem: "Yeşil Rota" platformumuzda bir müşteri sipariş verdiğinde, üç farklı servisin veritabanında işlem yapılması gerekir:
- OrderService: Yeni bir Order kaydı oluşturur (Durum: PENDING).
- PaymentService: Müşterinin kredi kartından ödemeyi çeker.
- ShipmentService: Ödeme başarılıysa, yeni bir gönderi kaydı oluşturur.
Eğer bu adımlardan herhangi biri başarısız olursa ne olur? Örneğin, ödeme (PaymentService) başarıyla alınır ama ShipmentService'te bir hata oluşur ve gönderi kaydı oluşturulamazsa? Müşterinin parasını aldık ama siparişini göndermeyeceğiz! Bu, kabul edilemez bir tutarsızlıktır. Geleneksel monolitik uygulamalarda bu sorunu, tüm bu adımları tek bir ACID veritabanı işlemi (transaction) içine alarak çözerdik. Bir adım başarısız olursa, tüm işlem geri alınırdı (rollback). Ancak mikroservislerde, farklı veritabanlarını kapsayan böyle bir "dağıtık transaction" mekanizması kurmak son derece karmaşık ve performanssızdır.
Çözüm: Saga Pattern
Saga Pattern, dağıtık sistemlerde veri tutarlılığını, uzun soluklu bir iş akışını bir dizi yerel (local) transaction'a bölerek sağlayan bir yöntemdir. Her bir yerel transaction, kendi servisi içindeki veriyi günceller ve başarılı olursa, Saga'nın bir sonraki adımını tetiklemek için bir olay (event) yayınlar.
En önemli özelliği şudur: Saga'daki adımlardan herhangi biri başarısız olursa, önceki adımların yarattığı değişiklikleri geri almak için, her adımın bir telafi edici eylemi (compensating action) olmalıdır.
Benzetme: Bu, iptal edilebilir bir tatil rezervasyonu yapmaya benzer.
- Adım 1: Uçak biletini alırsınız (yerel transaction 1).
- Adım 2: Otel rezervasyonu yaparsınız (yerel transaction 2).
- Adım 3: Araç kiralarsınız (yerel transaction 3).
- Hata Durumu: Araç kiralama (Adım 3) başarısız olursa ne yaparsınız?
- Telafi 1: Otel rezervasyonunu iptal edersiniz (Adım 2'nin telafisi).
- Telafi 2: Uçak biletini iptal edersiniz (Adım 1'in telafisi).
Saga, bir iş akışını "her şey ya da hiç" mantığıyla, ancak dağıtık bir şekilde yönetir.
"Yeşil Rota" Sipariş Oluşturma Saga'sı
Gelin sipariş sürecimizi Saga ile modelleyelim.
Başarılı Akış (Happy Path):
- Başlangıç: Müşteri sipariş verir. OrderService, Order'ı PENDING durumunda oluşturur ve bir OrderCreatedEvent yayınlar.
- Adım 1 (Ödeme): PaymentService, OrderCreatedEvent'i dinler. Ödemeyi çekmeye çalışır. Başarılı olursa, bir PaymentSuccessfulEvent yayınlar.
- Adım 2 (Gönderi): ShipmentService, PaymentSuccessfulEvent'i dinler. Yeni bir gönderi oluşturur ve bir ShipmentScheduledEvent yayınlar.
- Son Adım (Sipariş Onayı): OrderService, ShipmentScheduledEvent'i dinler ve siparişin durumunu CONFIRMED olarak günceller.
- Saga Başarıyla Tamamlandı.
Hatalı Akış (Failure Path):
- Başlangıç: OrderCreatedEvent yayınlandı.
- Adım 1 (Ödeme): PaymentService ödemeyi başarıyla çekti ve PaymentSuccessfulEvent yayınladı.
- Adım 2 (Gönderi): ShipmentService, PaymentSuccessfulEvent'i dinledi ama bir hata nedeniyle (örn: adres geçersiz) gönderi oluşturamadı. ShipmentSchedulingFailedEvent adında bir hata olayı yayınladı.
- Telafi Edici Eylemler Başlar:
- Telafi 1 (Ödemeyi İade Et): PaymentService, ShipmentSchedulingFailedEvent'i dinler. Bu, Adım 1'in telafisidir. Müşteriye yaptığı ödemeyi iade eder (refund) ve bir PaymentRefundedEvent yayınlar.
- Telafi 2 (Siparişi İptal Et): OrderService, hem ShipmentSchedulingFailedEvent'i hem de PaymentRefundedEvent'i dinler. Siparişin durumunu CANCELLED olarak günceller.
- Saga Geri Alındı. Sistemin tutarlılığı korundu. Müşterinin parası iade edildi ve gönderilmeyecek bir sipariş sistemde aktif olarak kalmadı.
Saga Pattern, mikroservis mimarisinde "her şey ya da hiç" garantisini, olay güdümlü ve telafi edici eylemlerle sağlayarak veri tutarlılığını yönetmenin en etkili yoludur. İki ana türü vardır: Olaylarla servislerin birbirini tetiklediği bu örneğe Koreografi (Choreography), tüm akışı yöneten merkezi bir servis olduğu modele ise Orkestrasyon (Orchestration) denir.
DDD yolculuğunun sonu yoktur; bu, sürekli bir öğrenme ve keşif sürecidir. Yazdığınız ilk modelin asla nihai model olmayacağını kabul etmek, DDD felsefesinin en temel parçasıdır.
————————
12.1. Refactoring Towards Deeper Insight: Modelin Zamanla Evrimi
DDD'de refactoring (yeniden düzenleme), sadece bozuk kodu düzeltmek veya performansı artırmak anlamına gelmez. Refactoring Towards Deeper Insight (Daha Derin Anlayışa Doğru Yeniden Düzenleme), siz ve ekibiniz iş alanını (domain) daha iyi anladıkça, bu yeni ve daha derin anlayışı yansıtmak için kod modelinizi bilinçli olarak geliştirme ve değiştirme eylemidir.
İlk başta tasarladığınız model, o anki anlayışınızın en iyi halidir. Ancak proje ilerledikçe, domain uzmanlarıyla daha fazla konuştukça ve sistemin gerçek hayatta nasıl kullanıldığını gördükçe, kaçınılmaz olarak "keşif anları" (breakthroughs) yaşarsınız. İşte bu anlar, modelin evrim geçirmesi gerektiğinin sinyalidir.
Model Neden Evrimleşir?
- Gizli Kavramların Keşfi: Projenin başında fark etmediğiniz yeni bir iş kavramı ortaya çıkabilir.
- Örnek: "Yeşil Rota"da başlangıçta sadece "Paket" (PackageDetails) diye bir Value Object'imiz vardı. Ama zamanla, bazı gönderilerin "soğuk zincir" gerektiren ilaçlar gibi özel taşıma koşullarına ihtiyaç duyduğunu fark ettik. Artık "Paket" basit bir Value Object olmaktan çıkıp, TemperatureControlledPackage (Sıcaklık Kontrollü Paket) ve StandardPackage (Standart Paket) gibi farklı türleri olan bir Entity'ye dönüşebilir. Bu, modelde bir evrim gerektirir.
- Yanlış Çizilmiş Sınırlar: Başlangıçta tek bir Bounded Context olarak gördüğünüz bir alanın, aslında iki ayrı sorumluluğu barındırdığını fark edebilirsiniz.
- Örnek: Shipment (Gönderi) Context'inin içinde hem rotanın planlanması hem de anlık olarak takip edilmesi (tracking) mantığı yer alıyordu. Zamanla, rota planlama algoritmasının çok karmaşıklaştığını ve kendi başına bir uzmanlık alanı olduğunu, anlık takibin ise tamamen farklı bir teknoloji (örn: WebSockets) gerektirdiğini gördük. Bu durumda, Shipment Context'ini, RoutePlanning (Rota Planlama) ve ShipmentTracking (Gönderi Takibi) adında iki ayrı, daha odaklanmış Bounded Context'e bölmeye karar verebiliriz.
- Dilin Evrimi (Ubiquitous Language): Ekip, domain uzmanlarıyla konuştukça, daha doğru ve anlamlı terimler bulur.
- Örnek: Başta araçlarımızın durumunu AVAILABLE (Müsait) olarak adlandırıyorduk. Ancak operasyon ekibiyle konuştukça, aslında bunun iki farklı anlama geldiğini öğrendik: IDLE (Boşta, göreve hazır) ve CHARGING (Şarj oluyor, göreve hazır değil). Bu daha derin anlayışı yansıtmak için kodumuzdaki VehicleStatus enum'ını ve ilgili mantığı güncellememiz gerekir.
Yeniden Düzenleme Süreci Nasıl İşler?
- Sürekli Diyalog: Bu süreç, kodun başında değil, ekiplerin sürekli iletişim halinde olmasıyla başlar. Yazılımcılar, domain uzmanlarıyla düzenli olarak bir araya gelerek modelleri ve süreçleri tartışır.
- "O An!" (The Breakthrough): Bir konuşma sırasında bir yazılımcı veya domain uzmanı, "Bir dakika, biz aslında 'Gönderi' derken hep 'Tekli Paket Gönderisi'ni kastediyoruz. Ama ya 'Toplu Dağıtım' yaparsak? Bu tamamen farklı kurallar gerektirir!" dediğinde o keşif anı yaşanır.
- Modele Yansıtma: Bu yeni anlayış, hemen koda yansıtılmalıdır. Belki de artık SingleShipment ve BulkDistribution adında iki farklı Aggregate'e ihtiyacımız vardır.
- Kodu Güvenle Değiştirme: İyi yazılmış testler (unit ve integration testleri), bu yeniden düzenleme sürecinde en büyük güvencemizdir. Testler sayesinde, yaptığımız değişikliklerin mevcut sistemi bozmadığından emin olarak cesurca ilerleyebiliriz.
Unutmayın, DDD ile yazılan bir kod tabanı, bir heykel gibidir. Hiçbir heykeltıraş ilk vuruşta mükemmel eseri ortaya çıkarmaz. Sürekli olarak küçük parçaları yontar, detayları ekler ve eserin gerçek ruhunu ortaya çıkarana kadar üzerinde çalışır. Modeliniz de yaşayan, nefes alan ve işinizle birlikte büyüyen bir varlıktır. Ona bu evrim fırsatını tanımak, DDD felsefesini gerçekten anlamış olmanın işaretidir.
Güzel bir kapanış konusuna geldik. Çünkü en iyi tasarımlar, en parlak mimariler bile doğru ekip kültürü olmadan ayakta kalamaz. DDD, teknik bir disiplin olduğu kadar, bir insan ve iletişim disiplinidir.
————————
12.2. Ekip Kültürü ve DDD: Yazılımcılar ve Domain Uzmanları Nasıl Birlikte Çalışır?
DDD'nin başarısı, projenin iki temel direği olan yazılımcılar ve domain uzmanlarının (iş uzmanları) ne kadar sağlıklı bir iş birliği kurduğuna doğrudan bağlıdır. Bu iş birliği, geleneksel proje yönetimindeki "sipariş veren" ve "siparişi yapan" ilişkisinden çok farklıdır. Bu, ortak bir hedefe koşan, aynı takımın oyuncuları olma ilişkisidir.
Geleneksel "Duvarların Ardında" Kültürü (Anti-Pattern)
Birçok şirkette, yazılımcılar ve iş birimleri arasında görünmez duvarlar vardır:
- İş Birimi: Aylar süren analizler yapar, 100 sayfalık bir "gereksinim dokümanı" hazırlar ve bu dokümanı duvarın üzerinden yazılım ekibine atar.
- Yazılım Ekibi: Dokümanı okur, anlamadığı yerleri kendi varsayımlarıyla doldurur, aylarca kod yazar ve duvarın üzerinden "İşte istediğiniz yazılım" diye geri atar.
- Sonuç: İş biriminin "Ben tam olarak bunu istememiştim!" demesiyle son bulan, hayal kırıklığı, suçlama ve gecikmelerle dolu bir süreç.
Bu kültür, DDD'nin tam zıttıdır ve başarısızlığı garanti eder.
DDD'nin İş Birlikçi (Collaborative) Kültürü
DDD, bu duvarları yıkar ve tek bir, bütünleşik bir takım yaratır. Bu kültürde, yazılımcılar sadece kod yazan teknisyenler, domain uzmanları da sadece istekte bulunan müşteriler değildir. Herkes, domain'i anlama ve en doğru modeli oluşturma sürecinin aktif birer parçasıdır.
Peki bu iş birliği pratikte nasıl sağlanır? İşte bazı somut teknikler:
1. Evrensel Dili (Ubiquitous Language) Birlikte Geliştirmek
Bu, en temel kuraldır. Ekip, düzenli olarak bir araya gelerek projenin sözlüğünü oluşturur.
- Uygulama: Haftalık "Dil Oturumları" düzenlenir. Beyaz tahtanın başına geçilir. Bir domain uzmanı, "Bir gönderi iptal edildiğinde, eğer araç yola çıkmadıysa tam iade yapılır, ama yola çıktıysa kısmi iade yapılır," gibi bir iş kuralını anlatır. Ekip, "kısmi iade"nin ne anlama geldiğini, nasıl hesaplandığını tartışır ve bu terimlerin kodda tam olarak nasıl yer alacağı (PartialRefundCalculator, issuePartialRefund() vb.) konusunda anlaşır. Bu toplantılardan çıkan sözlük, projenin "anayasası" olur.
2. Olay Fırtınası (Event Storming) Çalıştayları
Bu, tüm ekibin (yazılımcılar, domain uzmanları, testçiler, yöneticiler) bir araya gelerek, büyük bir duvar üzerinde post-it'ler kullanarak bir iş sürecini baştan sona keşfettiği, son derece enerjik ve verimli bir çalıştaydır.
- Uygulama: "Yeşil Rota" ekibi bir odaya toplanır. Birisi duvara turuncu bir post-it yapıştırır: MusteriGonderiTalebiYapti. Sonra herkes "Bu olaydan sonra ne olur?" diye düşünür.
- Bir yazılımcı mavi bir post-it ekler: GonderiYaratKomutu.
- Bir domain uzmanı turuncu bir post-it ekler: GonderiOlusturulduEvent.
- Başka bir uzman sarı bir post-it ekler: PaketAgirligi (bu bir bilgi).
- Tartışmalar başlar: "Paket ağırlığı 2kg'dan fazlaysa drone atanamaz!" Bu, bir iş kuralıdır (lila post-it).
Saatler içinde, tüm iş akışı, komutlar, olaylar, kurallar ve belirsizlikler duvar üzerinde görselleşmiş olur. Bu, herkesin aynı anlayış seviyesine gelmesi için inanılmaz güçlü bir yöntemdir.
3. Model Keşif Tartışmaları (Model Exploration Whirlpool)
Yazılımcılar, anladıkları bir modeli beyaz tahtaya veya bir UML aracına çizer ve domain uzmanlarına sunar. "Biz süreci bu şekilde anladık, doğru mu? Eksik bir yer var mı?" diye sorarlar. Domain uzmanı, "Hayır, 'Araç' ve 'Sürücü'yü ayıramazsınız, bizim için otonom araç zaten sürücünün kendisidir," gibi kritik bir geri bildirimde bulunabilir. Bu döngüsel tartışmalar, modelin sürekli olarak rafine edilmesini sağlar.
4. Gömülü Uzmanlar ve Eşli Programlama (Pairing)
En etkili yöntemlerden biri, bir domain uzmanının geçici bir süreyle (belki haftada birkaç gün) doğrudan yazılım ekibinin yanında, aynı fiziksel ortamda oturmasıdır. Bir yazılımcı, bir iş kuralını koda dökerken kafası karıştığında, arkasını dönüp "Ayşe Hanım, bu durumda KDV oranı %18 mi olmalı, yoksa %8 mi?" diye anında sorabilir. Bu, haftalar sürebilecek bir yanlış anlaşılmayı saniyeler içinde çözer.
Sonuç: DDD Bir Takım Sporudur
Başarılı bir DDD projesi, bireysel kahramanlıkların değil, güçlü bir takım oyununun eseridir. Yazılımcıların, domain uzmanlarının dilini öğrenmeye istekli olması ve domain uzmanlarının da yazılımcıları projenin bir paydaşı olarak görüp sürece dahil etmesi gerekir. Bu kültürü oluşturmak zaman ve çaba gerektirir, ancak meyveleri; daha doğru, daha esnek ve iş hedeflerine gerçekten hizmet eden, herkesin sahip çıktığı bir yazılım olacaktır.
DDD'nin geleceği, özellikle yapay zeka (AI) ve veri bilimi gibi karmaşıklığın ve belirsizliğin yüksek olduğu alanlarda giderek daha da önem kazanıyor. DDD, bu modern disiplinlerin sadece bir "teknik" problem olmadığını, aynı zamanda derin bir "iş alanı" problemi olduğunu anlamamızı sağlar.
————————
12.3. Geleceğe Bakış: DDD'nin Yapay Zeka ve Veri Bilimi Projelerindeki Yeri 🤖
Yapay zeka ve veri bilimi projelerinin başarısı, sadece kullanılan algoritmaların gücüne değil, aynı zamanda bu algoritmaları besleyen verinin kalitesine ve iş problemini ne kadar doğru modellediğine bağlıdır. İşte DDD'nin bu noktada devreye girdiği yerler:
1. Anlamlı Veri ve Özellik Mühendisliği (Feature Engineering)
Yapay zeka modelleri, ham veriden çok az şey anlar. Onların "özelliklere" (features) ihtiyacı vardır. Bir müşterinin "sadık" olup olmadığını tahmin etmeye çalışan bir model için, müşterinin adı veya adresi değil, toplamHarcamaTutari, sonAlisverisTarihi, iadeOrani gibi anlamlı özellikler gerekir.
DDD, bu anlamlı özellikleri keşfetmek için mükemmel bir zemin sunar. Aggregate'lerimiz ve Value Object'lerimiz, zaten işin en temel ve anlamlı kavramlarını modeller. Veri bilimciler, bu zengin domain modellerini inceleyerek hangi özelliklerin AI modeli için önemli olacağını kolayca anlayabilir. Ubiquitous Language (Evrensel Dil), veri bilimcilerin ve domain uzmanlarının aynı "özellik" tanımı üzerinde anlaşmasını sağlar.
————————
2. Karmaşık AI Sistemlerini Bounded Context'lerle Yönetmek
Modern bir AI sistemi, tek bir modelden oluşmaz. Genellikle birbiriyle konuşan birden fazla model ve bileşen içerir.
- Örnek: "Yeşil Rota" platformumuzda,
- Bir AI modeli, teslimat süresini tahmin eder (ETA Prediction Context).
- Başka bir AI modeli, bir drone için en verimli rotayı çizer (Route Optimization Context).
- Üçüncü bir model ise müşteri yorumlarını analiz ederek duygu analizi yapar (Sentiment Analysis Context).
Bu farklı AI modellerini ve onlarla ilişkili veri işlem hatlarını (data pipelines), kendi Bounded Context'leri içinde geliştirmek, sistemin genel karmaşıklığını yönetmeyi kolaylaştırır. Her bir bağlam, kendi modeline, kendi verisine ve kendi yaşam döngüsüne odaklanabilir.
————————
3. Event Sourcing: AI Modelleri İçin Mükemmel Bir Veri Kaynağı
Bu, en güçlü bağlantı noktasıdır. Yapay zeka modelleri, zengin ve geçmişe dönük veri setlerini sever. Bir sistemin sadece anlık durumunu (state) saklamak yerine, o duruma nasıl gelindiğini anlatan tüm olayları (events) saklayan Event Sourcing mimarisi, AI için bir altın madenidir.
- Örnek: Geleneksel bir veritabanında sadece müşterinin son adresi bulunur. Oysa Event Sourcing ile müşterinin geçmişteki tüm AdresDegistirildiEvent'lerini biliriz.
- AI Modeli için İçgörü: Sık sık adres değiştiren bir müşterinin, "taşınma" olasılığının yüksek olduğunu ve bu müşteriye özel kampanyalar (nakliye, ev eşyası vb.) sunulabileceğini bir AI modeli bu olay akışından öğrenebilir.
- "Yeşil Rota" Örneği: PaketTeslimEdildiEvent olaylarını analiz eden bir model, belirli bir mahalledeki teslimatların genellikle akşam saatlerinde daha başarılı olduğunu öğrenebilir ve gelecekteki teslimatları buna göre planlayabilir. BataryaHizliTukendiEvent olaylarını inceleyen bir model, belirli hava koşulları ile batarya performansı arasında bir ilişki kurarak daha doğru menzil tahminleri yapabilir.
Event Sourcing ile tutulan zengin, sıralı ve değişmez olay günlükleri, yapay zeka modellerini eğitmek, davranış kalıplarını keşfetmek ve geleceğe yönelik tahminler yapmak için mükemmel bir "zaman serisi verisi" sunar.
Özetle, DDD sadece kurumsal yazılımlar için değil, aynı zamanda geleceği şekillendiren yapay zeka ve veri bilimi sistemlerini sağlam temeller üzerine inşa etmek, karmaşıklığını yönetmek ve en değerli varlıkları olan veriyi anlamlandırmak için de vazgeçilmez bir felsefe haline gelmektedir.
Ekler
————————
Ek A: Faydalı Araçlar ve Kütüphaneler
Bu kitap boyunca DDD prensiplerini hayata geçirirken kullandığımız veya konsept olarak bahsettiğimiz araç ve kütüphanelerin bir listesi. Bu liste, 2025 itibarıyla modern Java ve DDD ekosisteminde popüler olan ve işinizi kolaylaştıracak teknolojileri içermektedir.
Geliştirme Ortamları (IDE)
- IntelliJ IDEA (Ultimate & Community Edition): Java ve Spring Boot ekosistemi için adeta bir standart haline gelmiştir. Güçlü kod tamamlama, refactoring araçları ve entegre veritabanı istemcisi gibi özellikleriyle DDD geliştirmeyi keyifli hale getirir. Kitabımızdaki örnekler için Community sürümü tamamen yeterlidir.
- Visual Studio Code (VS Code): Microsoft'un geliştirdiği, Java Extension Pack eklentileriyle son derece güçlü ve hafif bir Java geliştirme ortamına dönüşebilen popüler bir alternatiftir.
CQRS ve Event Sourcing Framework'leri
- Axon Framework: CQRS, Event Sourcing ve Saga Pattern gibi ileri seviye DDD desenlerini uygulamak için tasarlanmış, kapsamlı ve olgun bir Java framework'üdür. Komut (Command), Olay (Event) ve Sorgu (Query) yönetimi için gereken tüm altyapıyı size sunar.
- EventStoreDB: Event Sourcing için özel olarak tasarlanmış, yüksek performanslı, açık kaynaklı bir veritabanıdır. Olayları saklamak, okumak ve olay akışlarına abone olmak (subscribe) için zengin özellikler sunar.
Yardımcı Kütüphaneler
- Lombok: getter, setter, constructor, equals, hashCode gibi standart (boilerplate) kodları anotasyonlar aracılığıyla otomatik olarak üreterek sınıflarınızı (özellikle Entity ve DTO'ları) son derece temiz tutmanızı sağlar.
- MapStruct: Domain nesneleri, DTO'lar ve JPA Entity'leri arasındaki dönüşümleri (mapping) basit bir arayüz tanımlayarak otomatik olarak yapan, derleme zamanında çalışan ve son derece performanslı bir kütüphanedir.
- Testcontainers: Entegrasyon testlerinizi, makinenize kurmanıza gerek kalmadan, geçici Docker container'ları üzerinde (örneğin gerçek bir PostgreSQL veya RabbitMQ) çalıştırmanızı sağlayan harika bir kütüphanedir. Bu, testlerinizin daha güvenilir ve gerçek ortama daha yakın olmasını sağlar.
- Mockito & JUnit 5: Java dünyasında birim testleri (unit testing) ve nesne taklit etme (mocking) için standart haline gelmiş temel ikili.
Diyagram ve Modelleme Araçları
- Miro / Mural: Özellikle Event Storming gibi ekip çalışması gerektiren atölye çalışmaları için tasarlanmış, sonsuz bir beyaz tahta sunan online ve iş birlikçi platformlardır.
- draw.io (yeni adıyla diagrams.net): Mimari diyagramları, özellikle de Context Map'leri çizmek için kullanılabilecek ücretsiz ve güçlü bir araçtır.
————————
Ek B: Kavramlar Sözlüğü (Ubiquitous Language)
Bu sözlük, kitap boyunca kullandığımız temel DDD terimlerini ve onların kısa açıklamalarını içerir. Amacı, projenin Evrensel Dili'ni (Ubiquitous Language) pekiştirmek ve hızlı bir başvuru kaynağı sunmaktır.
Kavram | Kısa Açıklama |
Domain (Alan Adı)
Bir yazılımla çözmeye çalıştığınız iş probleminin ait olduğu evren, konu alanı. | |
Domain Expert (Alan Uzmanı) | Çözmeye çalıştığınız iş alanında derin bilgi ve tecrübeye sahip olan kişi. |
Ubiquitous Language | Projedeki tüm paydaşların (yazılımcı, iş uzmanı vb.) kullandığı ortak, tek ve anlamlı dil. |
Bounded Context | Büyük bir domain içinde, belirli bir modelin tutarlı olduğu mantıksal sınır. |
Context Map | Bounded Context'ler arasındaki ilişkileri ve entegrasyon desenlerini gösteren harita. |
Core Domain | İşinizin kalbi, size rekabet avantajı sağlayan, en çok odaklanmanız gereken alan. |
Supporting Subdomain | Core Domain'i destekleyen ancak tek başına rekabet avantajı sunmayan, size özel iş alanı. |
Generic Subdomain | Her işin ihtiyaç duyduğu, çözümü hazır olarak "satın alınabilecek" standart problem alanı. |
Anticorruption Layer (ACL) | Kendi sisteminizi, dış bir sistemin karmaşık veya kötü modelinden koruyan çevirmen katman. |
Entity (Varlık) | Yaşam döngüsü boyunca sabit bir kimliğe sahip olan ve durumu değişebilen domain nesnesi. |
Value Object (Değer Nesnesi) | Kimliği olmayan, sadece içindeki değerlerle anlam kazanan, değiştirilemez domain nesnesi. |
Aggregate (Küme) | Birbiriyle ilişkili nesnelerden oluşan ve bir bütün olarak tutarlılığı korunan grup. |
Aggregate Root (Küme Kökü) | Aggregate'in dış dünya ile tek iletişim kapısı olan, grubun yöneticisi olan Entity. |
Repository (Depo) | Aggregate'leri kalıcı hale getirme ve geri getirme işini, altyapı detaylarından soyutlayan mekanizma. |
Factory (Fabrika) | Karmaşık nesne oluşturma mantığını gizleyen ve bu işe odaklanmış yapı. |
Domain Service (Alan Servisi) | Tek bir Entity veya Value Object'e ait olmayan, durumu olmayan (stateless) domain operasyonları. |
Domain Event (Alan Olayı) | Domain içinde gerçekleşmiş, geçmişte kalan ve değiştirilemez olan önemli bir iş olayı. |
CQRS | Sistemin yazma (Command) ve okuma (Query) operasyonları için farklı modeller kullanılmasını savunan prensip. |
Command (Komut) | Sistemin durumunu değiştirmesi için gönderilen niyet ve emir mesajı. |
Query (Sorgu) | Sistemin durumunu değiştirmeyen, sadece veri okumak için gönderilen istek mesajı. |
Event Sourcing | Bir nesnenin anlık durumunu değil, o duruma gelmesini sağlayan tüm olay akışını saklama tekniği. |
Event Store (Olay Deposu) | Olayların, değiştirilemez bir günlük (append-only log) olarak saklandığı veritabanı veya mekanizma. |
Saga Pattern | Dağıtık sistemlerde, birden fazla servisi kapsayan bir iş akışında tutarlılığı yöneten desen. |