← Back to list

DDD’nin Kalbinde Design Patterns — Makale 6: Domain Service — İş Mantığının Vatansızları

İçindekiler

Sahin Yelkenci · 2026-04-28 17:51 · 0 claps · 18.8 min read
#domain-driven-design #design-patterns #domain-services #stateless #aggregation
Open on Medium ↗

Domain Service — İş Mantığının Vatansızları

Domain Service — İş Mantığının Vatansızları

DDD’nin Kalbinde Design Patterns — Makale 6: Domain Service — İş Mantığının Vatansızları

İçindekiler

» Makale 6: Domain Service — İş Mantığının Vatansızları » Giriş: Bir İş Kuralının Evsizliği » 1. Domain Service Nedir? Kesin Tanım » 2. Domain Service vs Application Service: En Kritik Ayrım » 3. GoF Strategy Pattern ve Domain Service » 4. Domain Service’in Üç Kural Testi » 5. Trendyol Domain Service Kataloğu » 6. Domain Service’in Mimari Konumu » 7. Domain Service’in Bağımlılık Yönetimi » 8. Anti-Pattern’ler: Domain Service Yanlış Kullanımı » 9. Test Stratejisi: Domain Service’i Nasıl Test Ederiz? » 10. Trendyol Örneği: Domain Service’lerin Tam Haritası » 11. Sonuç: Vatansızlık Bir Güç Kaynağıdır » Serinin Bir Sonraki Makalesinde

Giriş: Bir İş Kuralının Evsizliği

Her iyi tasarımda bir an gelir ve şu soru sorulur: “Bu iş mantığını nereye koyacağım?” Makale 4'te Entity ve Value Object’leri öğrendik — kendi kimliği veya değeriyle var olan nesneler. Makale 5'te Aggregate’leri öğrendik — birlikte tutarlı olması gereken nesnelerin kümesi. Bu iki yapı, domain mantığının büyük çoğunluğunu barındırır.

Ama her iş kuralı bir nesneye sığmaz. Bazen bir kural, birden fazla Aggregate’i, birden fazla domain kavramını aynı anda ilgilendirir — ama bu kuralı hiçbirine ait kılamazsınız. Her birine koymak anlamsız olur; birbirine kopyalamak ise felakete davet çıkarmaktır.

Somut bir örnek: Trendyol’da bir sipariş oluşturulurken fiyat hesabı yapılıyor. Bu hesaplama şunları içeriyor: temel ürün fiyatı, müşteri tipi indirimi (premium üye mi?), aktif kampanyalar, sadakat puanı kullanımı, kargo ücreti (ücretsiz kargo eşiği aşıldı mı?), vergi. Bu hesaplamanın sahibi kim?

Order Aggregate'i mi? Hayır — Order henüz oluşturulmamış; ayrıca bu hesaplama, Customer'ı, Campaign'i ve LoyaltyAccount'u da ilgilendiriyor. Customer Aggregate'i mi? Hayır — müşteri kendi fiyatını hesaplamaz; bu onun sorumluluğu değil. Product mu? Hayır — ürün fiyatı bilir ama kampanya ve sadakat hesabını bilmez.

Bu hesaplama “evsizdir.” Hiçbir nesneye ait değildir ama açıkça domain’e aittir — teknik bir altyapı işlemi değil, saf iş mantığıdır. İşte tam bu durumlar için DDD, Domain Service kavramını tanımlar.

Bu makale, Domain Service’in ne olduğunu, ne olmadığını, Application Service’ten nasıl ayrıldığını, GoF’un Strategy pattern’i ve Command pattern’i ile ilişkisini ve Trendyol senaryolarında nasıl uygulandığını derinlemesine inceliyor.

1. Domain Service Nedir? Kesin Tanım

1.1 Üç Koşullu Tanım

Eric Evans, Domain Service’i şu üç koşulla tanımlar: birincisi, operasyon doğası gereği bir Entity veya Value Object’e ait değildir; ikincisi, operasyon domain sözlüğünden anlamlı bir kavramı temsil eder; üçüncüsü, arayüz domain modeli açısından tanımlanır — Entity ve Value Object’ler bakımından.

Bu üç koşul birlikte şunu söyler: Domain Service, domain’e özgü iş mantığını barındıran, ama bu mantığın doğal sahibi olan tek bir nesne bulunmayan durumlarda ortaya çıkan yapıdır. “Domain’e özgü” vurgusu kritiktir — bu mantık teknik değildir, iş dünyasından gelir. “Tek bir nesne bulunmayan” vurgusu da kritiktir — eğer mantık doğal olarak bir Entity’ye aitse, orada yaşamalıdır; Domain Service’e zorla taşımak anti-pattern olur.

Domain Service’in en belirleyici özelliği stateless (durumsuz) olmasıdır. Domain Service, hiçbir zaman kendi içinde state tutmaz. Bir PricingService, "son hesapladığım fiyat" bilgisini içinde saklamaz. Her çağrı bağımsızdır; geçmiş çağrıların etkisi yoktur. Bu stateless doğa, Domain Service'i hem test edilmesi son derece kolay hem de eşzamanlı ortamlarda güvenli kılar.

Bu karar ağacı, bir iş mantığının nereye yerleşeceğini belirlemek için sorulması gereken üç kritik soruyu göstermektedir. İlk soru, mantığın doğal bir sahibi olup olmadığını sorgular — eğer varsa, o nesnenin içine gider; zorla Domain Service yapmak anti-pattern olur. İkinci soru, mantığın domain sözlüğünden gelip gelmediğini sorgular — teknik bir detaysa Infrastructure’a aittir. Üçüncü soru, mantığın birden fazla Aggregate’i ilgilendirip ilgilendirmediğini sorgular — ilgilendiriyorsa Domain Service, ilgilendirmiyorsa yine Entity/VO metodu. Bu akışın sonunda “ama dikkat!” notu önemlidir: Domain Service ile Application Service arasındaki sınır bazen bulanıklaşır; bu sınırı bir sonraki bölümde derinlemesine inceleyeceğiz.

1.2 Stateless Olmanın Derin Anlamı

Domain Service’in stateless olması, yüzeysel bir teknik kural değildir. Bu kural, Domain Service’in ne olduğu hakkında derin bir şey söyler: Domain Service bir kavramı temsil etmez, bir operasyonu temsil eder.

Bir Order Entity'si "bir sipariş"i temsil eder — kimliği, durumu, geçmişi olan bir varlık. Bir Money Value Object'i "bir para değeri"ni temsil eder — değeriyle var olan bir kavram. Ama bir PricingService, "fiyatlandırma yapma operasyonunu" temsil eder — bir şeyi değil, bir eylemi. Ve bu eylem her çalıştırıldığında bağımsızdır; önceki çalışmanın izini taşımaz.

Bu stateless doğanın pratik sonuçları şunlardır: Domain Service’ler Spring @Service ile singleton olarak tanımlanabilir — state yoksa paylaşım güvenlidir. Domain Service'ler çok kolay unit test edilir — mock'a gerek yok, sadece girdi ver, çıktıyı doğrula. Domain Service'ler eşzamanlı ortamlarda thread-safe'dir — state olmadığından race condition riski yoktur.

2. Domain Service vs Application Service: En Kritik Ayrım

2.1 İki Servisin Farklı Soruları

Domain Service ve Application Service arasındaki ayrım, DDD’nin en sık yanlış anlaşılan konularından biridir. Çoğu geliştirici, her iş mantığını Application Service’e koyar; Domain Service ise ya hiç yazılmaz ya da yanlış yazılır.

Bu iki servis, birbirinden köklü biçimde farklı iki soruyu yanıtlar:

Domain Service: “Bu iş kuralı nedir ve nasıl hesaplanır?” — Bu soru, iş dünyasından gelir. Cevabı, domain uzmanıyla birlikte verilir. “Premium müşteri indirimi nasıl hesaplanır?” sorusunun cevabı bir Domain Service’te yaşar.

Application Service: “Bu use case’i gerçekleştirmek için hangi adımlar hangi sırayla atılmalı?” — Bu soru, teknik koordinasyondan gelir. Cevabı, geliştirici tarafından verilir. “Sipariş oluşturma use case’ini yürütmek için önce ne yapılır, sonra ne yapılır?” sorusunun cevabı Application Service’te yaşar.

Bu karşılaştırma tablosu, iki servis türü arasındaki altı temel boyuttaki farkı göstermektedir. Katman farkı: Domain Service, Domain katmanında yaşar; Application Service, Application katmanında. Bilgi farkı: Domain Service yalnızca domain nesnelerini bilir — Entity, Value Object, Aggregate; Application Service ise Repository, Event Bus ve Transaction sınırlarını bilir. Test farkı: Domain Service saf birim testiyle, hiçbir mock olmadan test edilebilir; Application Service ise genellikle Repository’yi mock’lamayı gerektirir. Bu altı boyuttaki farkı zihinsel model olarak içselleştirmek, kod yazarken “bu nereye ait?” sorusuna anlık yanıt vermeyi sağlar.

2.2 Kod Düzeyinde Fark: Somut Karşılaştırma

Fiyatlandırma senaryosu üzerinden iki yaklaşımı karşılaştıralım:

// ─────────────────────────────────────────────────────────────────
// ❌ YANLIŞ: İş mantığı Application Service'te
// ─────────────────────────────────────────────────────────────────
@Service
@Transactional
public class OrderApplicationService_WRONG {

private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final CampaignRepository campaignRepository;
    public void placeOrder(PlaceOrderCommand command) {
        Customer customer = customerRepository.findById(command.customerId())
                                              .orElseThrow();
        List<Campaign> activeCampaigns = campaignRepository.findActive();
        // ❌ İş mantığı Application Service içinde!
        // Bu mantık domain bilgisi - burası değil!
        BigDecimal discount = BigDecimal.ZERO;
        if (customer.getType().equals("PREMIUM")) {
            discount = discount.add(new BigDecimal("0.10")); // %10 premium indirim
        }
        for (Campaign campaign : activeCampaigns) {
            if (campaign.isApplicable(command.items())) {
                discount = discount.add(campaign.getDiscountRate());
            }
        }
        // Daha 50 satır iş mantığı devam ediyor...
        // Application Service şişiyor, test edilemez hale geliyor
    }
}
// ─────────────────────────────────────────────────────────────────
// ✅ DOĞRU: İş mantığı Domain Service'te
// ─────────────────────────────────────────────────────────────────
// Domain Katmanı - PricingService (Domain Service)
// Stateless, saf iş mantığı, hiçbir infrastructure bilgisi yok
public class PricingService {
    // ─── Ana fiyatlandırma operasyonu ────────────────────────────
    public PricingResult calculateOrderPrice(
            Customer customer,
            List<OrderItemRequest> items,
            List<Campaign> applicableCampaigns,
            LoyaltyPoints availablePoints) {
        // 1. Adım: Temel fiyat hesabı
        Money subtotal = calculateSubtotal(items);
        // 2. Adım: Müşteri tipi indirimi (Domain kuralı)
        Money customerDiscount = calculateCustomerTypeDiscount(customer, subtotal);
        // 3. Adım: Kampanya indirimi (Domain kuralı)
        Money campaignDiscount = calculateBestCampaignDiscount(
            applicableCampaigns, subtotal, customer
        );
        // 4. Adım: En iyi indirim seç (birleşmez kural)
        Money totalDiscount = selectBestDiscount(customerDiscount, campaignDiscount);
        // 5. Adım: Sadakat puanı indirimi (Domain kuralı)
        Money loyaltyDiscount = calculateLoyaltyDiscount(availablePoints, subtotal);
        // 6. Adım: Kargo ücreti (Domain kuralı)
        Money shippingCost = calculateShippingCost(subtotal, totalDiscount, customer);
        // 7. Adım: Nihai toplam
        Money finalTotal = subtotal
                            .subtract(totalDiscount)
                            .subtract(loyaltyDiscount)
                            .add(shippingCost);
        return new PricingResult(
            subtotal,
            customerDiscount,
            campaignDiscount,
            totalDiscount,
            loyaltyDiscount,
            shippingCost,
            finalTotal
        );
    }
    // ─── İş kuralları - her biri bağımsız test edilebilir ────────
    private Money calculateCustomerTypeDiscount(Customer customer, Money subtotal) {
        return switch (customer.type()) {
            case PREMIUM  -> subtotal.reduceBy(Percentage.of(10)); // %10 premium
            case PLUS     -> subtotal.reduceBy(Percentage.of(5));  // %5 Plus
            case STANDARD -> Money.ZERO_TRY;
        };
    }
    private Money calculateBestCampaignDiscount(
            List<Campaign> campaigns, Money subtotal, Customer customer) {
        return campaigns.stream()
                        .filter(c -> c.isApplicableTo(customer))
                        .map(c -> c.calculateDiscount(subtotal))
                        .max(Comparator.comparing(Money::amount))
                        .orElse(Money.ZERO_TRY);
    }
    private Money selectBestDiscount(Money customerDiscount, Money campaignDiscount) {
        // İş kuralı: müşteri indirimi ve kampanya indirimi birleşmez,
        // yalnızca büyük olan uygulanır
        return customerDiscount.isGreaterThan(campaignDiscount)
               ? customerDiscount
               : campaignDiscount;
    }
    private Money calculateLoyaltyDiscount(LoyaltyPoints points, Money subtotal) {
        if (points.isZero()) return Money.ZERO_TRY;
        // İş kuralı: sadakat puanı en fazla sepet tutarının %20'si kadar indirim sağlar
        Money maxLoyaltyDiscount = subtotal.reduceBy(Percentage.of(20));
        Money loyaltyValue = points.monetaryValue();
        return loyaltyValue.isGreaterThan(maxLoyaltyDiscount)
               ? maxLoyaltyDiscount
               : loyaltyValue;
    }
    private Money calculateShippingCost(
            Money subtotal, Money discount, Customer customer) {
        Money discountedSubtotal = subtotal.subtract(discount);
        // İş kuralı: 150 TL üzeri sepetlerde veya Premium müşterilerde ücretsiz kargo
        boolean freeShipping = discountedSubtotal.isGreaterThanOrEqual(Money.ofTRY("150"))
                            || customer.type() == CustomerType.PREMIUM;
        return freeShipping ? Money.ZERO_TRY : Money.ofTRY("29.90");
    }
}
// Application Katmanı - PlaceOrderCommandHandler (Application Service)
// İnce, sadece koordinasyon yapıyor
@Service
@Transactional
public class PlaceOrderCommandHandler {
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final CampaignRepository campaignRepository;
    private final LoyaltyAccountRepository loyaltyAccountRepository;
    private final OrderFactory orderFactory;
    private final PricingService pricingService;     // Domain Service inject
    private final DomainEventPublisher eventPublisher;
    public OrderId handle(PlaceOrderCommand command) {
        // 1. Gerekli domain nesnelerini yükle
        Customer customer = customerRepository.findById(command.customerId())
            .orElseThrow(() -> new CustomerNotFoundException(command.customerId()));
        List<Campaign> campaigns = campaignRepository.findActiveForCustomer(
            command.customerId()
        );
        LoyaltyPoints loyaltyPoints = loyaltyAccountRepository
            .findByCustomerId(command.customerId())
            .map(LoyaltyAccount::availablePoints)
            .orElse(LoyaltyPoints.ZERO);
        // 2. Domain Service ile fiyatlandırma yap
        // Application Service iş mantığını BILMEZ - Domain Service biliyor
        PricingResult pricing = pricingService.calculateOrderPrice(
            customer,
            command.items(),
            campaigns,
            loyaltyPoints
        );
        // 3. Domain Factory ile Order oluştur
        Order order = orderFactory.create(
            command.customerId(),
            command.items(),
            pricing,
            command.shippingAddress()
        );
        // 4. Kaydet
        orderRepository.save(order);
        // 5. Domain Event'leri yayımla
        order.domainEvents().forEach(eventPublisher::publish);
        order.clearDomainEvents();
        return order.id();
    }
}

Bu iki kod bloğu, Domain Service ve Application Service arasındaki farkı somut biçimde ortaya koymaktadır. Yanlış yaklaşımda OrderApplicationService_WRONG, premium indirim hesabını, kampanya kontrolünü ve diğer iş kurallarını kendi içinde barındırıyor. Bu sınıf şişiyor, test etmek zorlaşıyor ve değişime kapalı hale geliyor. Doğru yaklaşımda ise PricingService, tüm iş kurallarını bağımsız private metodlarda barındırıyor. Her kural ayrı ayrı test edilebiliyor. PlaceOrderCommandHandler ise yalnızca koordinasyon yapıyor: yükle, hesapla, oluştur, kaydet, yayımla. Yedi satır, düzenli ve net.

3. GoF Strategy Pattern ve Domain Service

3.1 Strategy Pattern: Domain Service’in Teknik Kökü

GoF’un Strategy pattern’i şunu söyler: bir davranış ailesini (algoritma ailesi) kapsülleyin, her birini ayrı bir sınıfa koyun ve istemci kodunun bu algoritmalar arasında geçiş yapmasına izin verin.

Domain Service, Strategy pattern’in domain-aware evrimi olarak görülebilir. Ama aralarında kritik bir fark vardır: GoF Strategy, teknik bir algoritma seçimini temsil eder — sıralama algoritması, arama algoritması. Domain Service ise bir iş operasyonunu temsil eder — fiyatlandırma operasyonu, dolandırıcılık tespiti operasyonu.

Bu fark, isimlendirmeye yansır: GoF Strategy → SortingStrategy, SearchStrategy. Domain Service → PricingService, FraudDetectionService. Ve domain kurallarını strateji olarak modellerken kullanılan interface isimlendirmesi → DiscountPolicy, ShippingPolicy (Makale 3'te gördüğümüz gibi, "Policy" DDD'de Strategy'nin domain diliyle ifadesidir).

Bu diyagram, GoF Strategy pattern’inden DDD Policy’ye ve oradan Domain Service’e uzanan evrimsel zinciri göstermektedir. GoF Strategy teknik algoritma seçimini modellerken, DDD Policy aynı yapıyı iş kuralı kapsülleme için kullanır ve domain diliyle isimlendirir. Domain Service ise bir adım daha ileri gider: birden fazla Policy’yi ve birden fazla domain nesnesini orkestre eden, daha karmaşık iş operasyonlarını yönetir. Her seviye bir öncekinin üzerine inşa edilir; yeni bir kavram değil, mevcut kavramın domain kontekstine taşınmış halidir.

3.2 Policy ve Domain Service Birlikte: Kompozisyon Gücü

DiscountPolicy interface'i ve onu kullanan PricingService birlikte çok güçlü bir kompozisyon sağlar. Her Policy bağımsız test edilebilir; PricingService ise Policy'leri orkestre eder.

// Domain Katmanı — DiscountPolicy (Strategy Pattern'in Domain Evrimi)
public interface DiscountPolicy {
    Money calculateDiscount(Order order, Customer customer);
    boolean isApplicableTo(Customer customer);
    // Policy'nin önceliği — çakışma durumunda hangisi uygulanır?
    int priority();
}
// Somut Policy 1: Kampanya indirimi
public class CampaignDiscountPolicy implements DiscountPolicy {
    private final Campaign campaign;
    public CampaignDiscountPolicy(Campaign campaign) {
        this.campaign = campaign;
    }
    @Override
    public Money calculateDiscount(Order order, Customer customer) {
        // Kampanya tiplerine göre hesaplama
        return switch (campaign.type()) {
            case PERCENTAGE -> order.subtotal().reduceBy(campaign.discountRate());
            case FIXED_AMOUNT -> campaign.fixedDiscountAmount();
            case FREE_PRODUCT -> Money.ZERO_TRY; // Ayrı bir ürün eklenir
        };
    }
    @Override
    public boolean isApplicableTo(Customer customer) {
        return campaign.targetCustomerTypes().contains(customer.type())
            && campaign.isActive()
            && campaign.hasRemainingQuota();
    }
    @Override
    public int priority() { return 10; } // Kampanya yüksek öncelik
}
// Somut Policy 2: Premium üye indirimi
public class PremiumMemberDiscountPolicy implements DiscountPolicy {
    private static final Percentage PREMIUM_DISCOUNT = Percentage.of(10);
    @Override
    public Money calculateDiscount(Order order, Customer customer) {
        return order.subtotal().reduceBy(PREMIUM_DISCOUNT);
    }
    @Override
    public boolean isApplicableTo(Customer customer) {
        return customer.type() == CustomerType.PREMIUM;
    }
    @Override
    public int priority() { return 5; } // Premium orta öncelik
}
// Somut Policy 3: Sadakat puanı indirimi
public class LoyaltyPointDiscountPolicy implements DiscountPolicy {
    private static final Percentage MAX_LOYALTY_DISCOUNT_RATE = Percentage.of(20);
    private final LoyaltyPoints availablePoints;
    public LoyaltyPointDiscountPolicy(LoyaltyPoints availablePoints) {
        this.availablePoints = availablePoints;
    }
    @Override
    public Money calculateDiscount(Order order, Customer customer) {
        Money maxDiscount = order.subtotal().reduceBy(MAX_LOYALTY_DISCOUNT_RATE);
        Money loyaltyValue = availablePoints.monetaryValue();
        return loyaltyValue.isGreaterThan(maxDiscount) ? maxDiscount : loyaltyValue;
    }
    @Override
    public boolean isApplicableTo(Customer customer) {
        return availablePoints.isPositive();
    }
    @Override
    public int priority() { return 1; } // Sadakat en düşük öncelik
}
// Domain Service - Policy'leri orkestre eder
public class DiscountOrchestrationService {
    /**
     * Verilen politikalar arasından uygulanabilir olanları seçer
     * ve iş kurallarına göre en iyi kombinasyonu hesaplar.
     * İş kuralı: Kampanya ve üyelik indirimi birleşmez; yalnızca büyük olanı uygulanır.
     * İş kuralı: Sadakat indirimi her zaman ayrıca uygulanabilir.
     */
    public DiscountBreakdown calculateBestDiscount(
            Order order,
            Customer customer,
            List<DiscountPolicy> availablePolicies) {
        // Uygulanabilir politikaları filtrele
        List<DiscountPolicy> applicablePolicies = availablePolicies.stream()
            .filter(policy -> policy.isApplicableTo(customer))
            .sorted(Comparator.comparingInt(DiscountPolicy::priority).reversed())
            .collect(toList());
        // Sadakat puanı ayrı tutulur
        Optional<DiscountPolicy> loyaltyPolicy = applicablePolicies.stream()
            .filter(p -> p instanceof LoyaltyPointDiscountPolicy)
            .findFirst();
        // Geri kalan politikalar arasından en iyisini seç
        List<DiscountPolicy> nonLoyaltyPolicies = applicablePolicies.stream()
            .filter(p -> !(p instanceof LoyaltyPointDiscountPolicy))
            .collect(toList());
        Money primaryDiscount = nonLoyaltyPolicies.stream()
            .map(p -> p.calculateDiscount(order, customer))
            .max(Comparator.comparing(Money::amount))
            .orElse(Money.ZERO_TRY);
        Money loyaltyDiscount = loyaltyPolicy
            .map(p -> p.calculateDiscount(order, customer))
            .orElse(Money.ZERO_TRY);
        return new DiscountBreakdown(primaryDiscount, loyaltyDiscount);
    }
}

4. Domain Service’in Üç Kural Testi

4.1 Ne Zaman Domain Service Yazılır?

Domain Service yazmak için üç koşulun aynı anda sağlanması gerekir. Bu koşullardan herhangi biri sağlanmıyorsa, Domain Service doğru yer olmayabilir.

Bu diyagram, Domain Service yazma kararını üç ayrı test üzerinden göstermektedir. Her testin hem “geçti” hem “geçmedi” örneği verilmiştir. Birinci test, kaç Aggregate’in dahil olduğunu sorgular: tek Aggregate ise Entity metodu, birden fazla ise Domain Service. İkinci test, domain kavramı olup olmadığını sorgular: teknik detaysa Infrastructure, iş kavramıysa Domain Service. Üçüncü test, stateless kalınıp kalınamayacağını sorgular: state gerekliyse başka bir yapı düşünülmeli, stateless ise Domain Service.

5. Trendyol Domain Service Kataloğu

5.1 PricingService — Fiyatlandırma Operasyonu

Fiyatlandırma, bir e-ticaret sisteminin en karmaşık iş mantığını barındıran alanlarından biridir. Trendyol senaryosunda şu faktörler fiyatı etkiler: ürün baz fiyatı, müşteri tipi indirimi, aktif kampanyalar, sadakat puanı, kargo ücreti. Bu faktörlerin her biri farklı bir Aggregate veya domain kavramından gelir; hiçbiri tek başına bir Order’a ya da Customer’a ait değildir.

// Domain Katmanı — PricingService
// Bağımlılıkları: Yalnızca domain nesneleri
// Infrastructure bağımlılığı: Sıfır
public class PricingService {

// Sabitler - iş kurallarının kodlanmış hali
    private static final Money FREE_SHIPPING_THRESHOLD    = Money.ofTRY("150.00");
    private static final Money STANDARD_SHIPPING_COST     = Money.ofTRY("29.90");
    private static final Percentage PREMIUM_DISCOUNT_RATE = Percentage.of(10);
    private static final Percentage MAX_LOYALTY_RATE      = Percentage.of(20);
    /**
     * Sipariş için tam fiyatlandırma hesabı yapar.
     * Bu metod, fiyatlandırmayı etkileyen tüm domain kurallarını
     * sıralı ve tutarlı biçimde uygular.
     *
     * @param customer     Siparişi veren müşteri (tip bilgisi için)
     * @param items        Sipariş edilmek istenen ürünler ve miktarlar
     * @param campaigns    Geçerli kampanyalar
     * @param loyaltyPoints Müşterinin kullanılabilir sadakat puanı
     * @return             Tüm indirim kırılımını ve nihai tutarı içeren sonuç
     */
    public PricingResult calculateOrderPrice(
            Customer customer,
            List<OrderItemRequest> items,
            List<Campaign> campaigns,
            LoyaltyPoints loyaltyPoints) {
        // Adım 1: Temel fiyat (ürün fiyatları × miktarlar)
        Money subtotal = items.stream()
                              .map(item -> item.unitPrice().multiply(
                                  BigDecimal.valueOf(item.quantity().value())))
                              .reduce(Money.ZERO_TRY, Money::add);
        // Adım 2: En iyi indirim (kampanya vs üyelik - birleşmez)
        Money campaignDiscount    = bestCampaignDiscount(campaigns, subtotal, customer);
        Money membershipDiscount  = membershipDiscount(customer, subtotal);
        Money primaryDiscount     = campaignDiscount.isGreaterThan(membershipDiscount)
                                    ? campaignDiscount : membershipDiscount;
        // Adım 3: Sadakat puanı indirimi (ayrıca uygulanabilir)
        Money loyaltyDiscount = loyaltyDiscount(loyaltyPoints, subtotal);
        // Adım 4: İndirimli ara toplam
        Money discountedSubtotal = subtotal
                                    .subtract(primaryDiscount)
                                    .subtract(loyaltyDiscount);
        // Adım 5: Kargo ücreti
        Money shippingCost = shippingCost(discountedSubtotal, customer);
        // Adım 6: Nihai toplam
        Money finalTotal = discountedSubtotal.add(shippingCost);
        return PricingResult.builder()
            .subtotal(subtotal)
            .campaignDiscount(campaignDiscount)
            .membershipDiscount(membershipDiscount)
            .appliedPrimaryDiscount(primaryDiscount)
            .loyaltyDiscount(loyaltyDiscount)
            .shippingCost(shippingCost)
            .finalTotal(finalTotal)
            .build();
    }
    private Money bestCampaignDiscount(
            List<Campaign> campaigns, Money subtotal, Customer customer) {
        return campaigns.stream()
                        .filter(c -> c.isApplicableTo(customer))
                        .map(c -> c.calculateDiscount(subtotal))
                        .max(Comparator.comparing(Money::amount))
                        .orElse(Money.ZERO_TRY);
    }
    private Money membershipDiscount(Customer customer, Money subtotal) {
        return switch (customer.type()) {
            case PREMIUM  -> subtotal.reduceBy(PREMIUM_DISCOUNT_RATE);
            case PLUS     -> subtotal.reduceBy(Percentage.of(5));
            case STANDARD -> Money.ZERO_TRY;
        };
    }
    private Money loyaltyDiscount(LoyaltyPoints points, Money subtotal) {
        if (points.isZero()) return Money.ZERO_TRY;
        Money maxAllowed = subtotal.reduceBy(MAX_LOYALTY_RATE);
        Money pointValue = points.monetaryValue();
        return pointValue.isGreaterThan(maxAllowed) ? maxAllowed : pointValue;
    }
    private Money shippingCost(Money discountedSubtotal, Customer customer) {
        boolean isFree = discountedSubtotal.isGreaterThanOrEqual(FREE_SHIPPING_THRESHOLD)
                      || customer.type() == CustomerType.PREMIUM;
        return isFree ? Money.ZERO_TRY : STANDARD_SHIPPING_COST;
    }
}

5.2 FraudDetectionService — Dolandırıcılık Tespiti

Dolandırıcılık tespiti, klasik bir Domain Service örneğidir: birden fazla domain kavramını (müşteri geçmişi, sipariş tutarı, ödeme yöntemi, konum bilgisi) bir araya getirerek bir risk skoru üretir. Bu hesaplama ne Order'a, ne Customer'a, ne de PaymentMethod'a tek başına aittir.

// Domain Katmanı — FraudDetectionService
public class FraudDetectionService {
// Risk eşikleri - iş tarafıyla mutabık kılınmış değerler
    private static final Money HIGH_VALUE_THRESHOLD       = Money.ofTRY("5000.00");
    private static final int   MAX_DAILY_ORDER_COUNT      = 10;
    private static final int   RISK_SCORE_BLOCK_THRESHOLD = 80;
    private static final int   RISK_SCORE_REVIEW_THRESHOLD= 50;
    /**
     * Siparişin dolandırıcılık riskini değerlendirir.
     * Her risk faktörü ayrı puanlanır ve ağırlıklı toplam hesaplanır.
     */
    public FraudAssessment assess(
            Order order,
            Customer customer,
            CustomerOrderHistory history,
            PaymentMethod paymentMethod) {
        int riskScore = 0;
        List<String> riskFactors = new ArrayList<>();
        // Risk Faktörü 1: Yüksek tutar
        if (order.totalAmount().isGreaterThan(HIGH_VALUE_THRESHOLD)) {
            riskScore += 30;
            riskFactors.add("Yüksek sipariş tutarı: " + order.totalAmount());
        }
        // Risk Faktörü 2: Yeni müşteri yüksek tutar kombinasyonu
        if (customer.isNew() && order.totalAmount().isGreaterThan(Money.ofTRY("1000"))) {
            riskScore += 25;
            riskFactors.add("Yeni müşteri - yüksek tutar kombinasyonu");
        }
        // Risk Faktörü 3: Günlük sipariş limiti
        if (history.todayOrderCount() >= MAX_DAILY_ORDER_COUNT) {
            riskScore += 20;
            riskFactors.add("Günlük sipariş limitine yakın: "
                            + history.todayOrderCount() + " sipariş");
        }
        // Risk Faktörü 4: Farklı teslimat adresi (son siparişe göre)
        if (history.hasOrders()
                && !order.shippingAddress().city()
                          .equals(history.lastOrderShippingCity())) {
            riskScore += 15;
            riskFactors.add("Bilinen adresin dışında teslimat");
        }
        // Risk Faktörü 5: İlk kez kullanılan ödeme yöntemi + yüksek tutar
        if (paymentMethod.isFirstUsage()
                && order.totalAmount().isGreaterThan(Money.ofTRY("2000"))) {
            riskScore += 20;
            riskFactors.add("İlk kullanım ödeme yöntemi + yüksek tutar");
        }
        // Risk değerlendirmesi
        FraudRiskLevel riskLevel = assessRiskLevel(riskScore);
        return new FraudAssessment(riskScore, riskLevel, riskFactors);
    }
    private FraudRiskLevel assessRiskLevel(int score) {
        if (score >= RISK_SCORE_BLOCK_THRESHOLD) return FraudRiskLevel.BLOCK;
        if (score >= RISK_SCORE_REVIEW_THRESHOLD) return FraudRiskLevel.MANUAL_REVIEW;
        return FraudRiskLevel.APPROVE;
    }
}
// FraudAssessment - Value Object olarak sonuç
public record FraudAssessment(
    int riskScore,
    FraudRiskLevel riskLevel,
    List<String> riskFactors
) {
    public boolean isBlocked()       { return riskLevel == FraudRiskLevel.BLOCK; }
    public boolean requiresReview()  { return riskLevel == FraudRiskLevel.MANUAL_REVIEW; }
    public boolean isApproved()      { return riskLevel == FraudRiskLevel.APPROVE; }
}

5.3 InventoryAllocationService — Stok Dağıtım Servisi

// Domain Katmanı — InventoryAllocationService
// Bir siparişin birden fazla depo ve stok kaynağından nasıl karşılanacağını belirler
public class InventoryAllocationService {

/**
     * Sipariş kalemlerini mevcut stok ve depo kapasitelerine göre dağıtır.
     * İş kuralı: Mümkünse müşteriye en yakın depodan karşıla.
     * İş kuralı: Tek depodan karşılanamıyorsa böl, birden fazla kargoya ver.
     * İş kuralı: Hiçbir depoda stok yoksa "bekleme listesi"ne al.
     */
    public AllocationPlan allocate(
            Order order,
            List<StockItem> availableStock,
            List<Warehouse> warehouses,
            ShippingAddress deliveryAddress) {
        List<AllocationLine> allocationLines = new ArrayList<>();
        List<OrderItem> unallocatedItems = new ArrayList<>();
        for (OrderItem item : order.items()) {
            // Her kalem için stok bul
            List<StockItem> itemStock = availableStock.stream()
                .filter(s -> s.productId().equals(item.productId()))
                .filter(s -> s.availableQuantity().isGreaterThanOrEqual(item.quantity()))
                .collect(toList());
            if (itemStock.isEmpty()) {
                unallocatedItems.add(item);
                continue;
            }
            // En uygun depoyu seç (müşteriye en yakın ve yeterli stoklu)
            Warehouse bestWarehouse = selectBestWarehouse(
                itemStock, warehouses, deliveryAddress
            );
            allocationLines.add(new AllocationLine(
                item.orderItemId(),
                item.productId(),
                item.quantity(),
                bestWarehouse.warehouseId()
            ));
        }
        return new AllocationPlan(allocationLines, unallocatedItems);
    }
    private Warehouse selectBestWarehouse(
            List<StockItem> stock,
            List<Warehouse> warehouses,
            ShippingAddress deliveryAddress) {
        // İş kuralı: Önce müşteriye en yakın depoyu tercih et
        return warehouses.stream()
            .filter(w -> stock.stream()
                              .anyMatch(s -> s.warehouseId().equals(w.warehouseId())))
            .min(Comparator.comparingDouble(
                w -> w.distanceTo(deliveryAddress.city())
            ))
            .orElseThrow(() -> new NoAvailableWarehouseException(
                "Uygun depo bulunamadı"
            ));
    }
}

6. Domain Service’in Mimari Konumu

6.1 Tam Akış Diyagramı: Domain Service Nasıl Çağrılır?

Bu sequence diyagramı, Domain Service’in gerçek bir use case akışında nasıl konumlandığını adım adım göstermektedir. Akışın genel ritmi şudur: Application Service (PlaceOrderCommandHandler) önce gerekli tüm domain nesnelerini Repository'lerden yükler. Sonra bu nesneleri Domain Service'lere (PricingService, FraudDetectionService) parametre olarak geçirir. Domain Service'ler saf iş mantığını çalıştırır ve sonuç döner. Application Service bu sonuçları kullanarak Aggregate'i oluşturur, kaydeder ve event'leri yayımlar. Dikkat çeken kritik nokta: Domain Service'ler hiçbir Repository çağrısı yapmaz. Onlara ihtiyaç duydukları nesneler Application Service tarafından sağlanır. Bu tasarım, Domain Service'i hem tamamen bağımsız hem de kolayca test edilebilir kılar.

6.2 Domain Service’in Paket Yapısındaki Yeri

com.trendyol.ordering/
├── domain/
│   ├── model/
│   │   ├── order/          (Order, OrderItem, OrderStatus...)
│   │   ├── customer/       (Customer, CustomerType...)
│   │   └── campaign/       (Campaign, CampaignType...)
│   │
│   ├── service/            ← DOMAIN SERVICE'LER BURADA
│   │   ├── PricingService.java
│   │   ├── FraudDetectionService.java
│   │   ├── InventoryAllocationService.java
│   │   └── ShippingCostService.java
│   │
│   ├── policy/             ← POLICY'LER (Strategy Pattern)
│   │   ├── DiscountPolicy.java         (interface)
│   │   ├── CampaignDiscountPolicy.java
│   │   ├── PremiumMemberDiscountPolicy.java
│   │   └── LoyaltyPointDiscountPolicy.java
│   │
│   ├── event/
│   └── repository/         (interface'ler)
│
└── application/
    └── handler/
        ├── PlaceOrderCommandHandler.java   ← Domain Service'i çağırır
        └── CancelOrderCommandHandler.java

Bu paket yapısı, Domain Service’lerin domain/service/ altında yaşadığını, Policy'lerin domain/policy/ altında ayrıca organize edildiğini ve Application Service'lerin application/handler/ altında yaşadığını göstermektedir. Bu ayrım, bir bakışta "bu sınıf ne tür bir sorumluluk taşıyor?" sorusunu yanıtlar. domain/service/ altındaki her sınıfın stateless bir domain operasyonu olduğunu, application/handler/ altındaki her sınıfın bir use case koordinatörü olduğunu anlamak için sınıfı açmaya bile gerek yoktur.

7. Domain Service’in Bağımlılık Yönetimi

7.1 Domain Service’e Ne Inject Edilir?

Domain Service’e yalnızca diğer Domain Service’ler ve domain nesneleri inject edilir. Repository, Kafka, REST client, email servisi — bunların hiçbiri Domain Service’e inject edilmez.

Bu diyagram, Domain Service’in bağımlılık kurallarını sınır koyan biçimde göstermektedir. İzin verilenler kategorisinde yalnızca domain dünyasına ait yapılar bulunur: diğer Domain Service’ler, Policy’ler, domain sabitleri ve parametre olarak geçilen domain nesneleri. Yasak kategorisinde ise tüm infrastructure bağımlılıkları yer alır: Repository’ler, email servisleri, HTTP client’lar ve Spring context’e bağlı servisler. Bu kural, Domain Service’in test edilmesini basit tutar: hiçbir mock gerekmez, sadece domain nesnelerini parametre olarak geç ve sonucu doğrula.

7.2 “Repository’ye İhtiyacım Var!” Durumu

Zaman zaman bir Domain Service’in bir nesneyi yüklemesi gerekiyor gibi görünür. “Kampanya detaylarını PricingService içinden okumam gerekiyor” gibi. Bu durumda iki seçenek vardır:

Seçenek 1 (Tercih edilen): Repository çağrısını Application Service’e taşı, yüklenmiş nesneyi Domain Service’e parametre olarak geç. Bu, Domain Service’i saf tutar.

Seçenek 2 (Nadir durum): Eğer gerçekten Domain Service’in kendisi yükleme yapması gerekiyorsa, bir “Domain Repository Interface” parametre olarak alınabilir — ama bu arayüz Domain katmanında tanımlanmış olmalıdır, Infrastructure’da değil.

// Seçenek 1 — Tercih edilen
// Application Service yüklüyor, Domain Service sadece hesaplıyor
public class PlaceOrderCommandHandler {
    public OrderId handle(PlaceOrderCommand command) {
        // Repository çağrısı Application Service'te
        List<Campaign> campaigns = campaignRepository.findActiveForCustomer(
            command.customerId()
        );
        // Domain Service yalnızca hesaplıyor
        PricingResult result = pricingService.calculateOrderPrice(
            customer, items, campaigns, loyaltyPoints
        );
    }
}

// Seçenek 2 - Nadir durum, gerekli olduğunda
// Domain Interface - Domain katmanında tanımlı
public interface CampaignRepository {
    List<Campaign> findActiveForCustomer(CustomerId customerId);
}
// Domain Service - Domain interface kullanıyor
public class PricingService {
    private final CampaignRepository campaignRepository; // Domain interface!
    public PricingResult calculateOrderPrice(
            Customer customer,
            List<OrderItemRequest> items,
            LoyaltyPoints loyaltyPoints) {
        // Kendi içinde yüklüyor - ama arayüz domain'e ait
        List<Campaign> campaigns = campaignRepository
            .findActiveForCustomer(customer.id());
        // ...
    }
}

8. Anti-Pattern’ler: Domain Service Yanlış Kullanımı

8.1 Anti-Pattern Kataloğu

Bu diyagram, Domain Service kullanımındaki dört temel anti-pattern’i kod örnekleri ve somut sonuçlarıyla göstermektedir. Birinci anti-pattern, tüm iş mantığını Domain Service’e doldurarak Anemic Domain Model’i farklı bir isimle geri getirmektir. İkinci anti-pattern, Domain Service’e Repository veya Kafka gibi infrastructure bağımlılıkları enjekte etmektir. Üçüncü anti-pattern, singleton bir Spring bean içinde state tutmaya çalışmaktır. Dördüncü anti-pattern, use case koordinasyonunu — transaction yönetimi, event yayımı, email gönderimi — Domain Service’e taşımaktır.

9. Test Stratejisi: Domain Service’i Nasıl Test Ederiz?

9.1 Saf Birim Testi — Mock Yok, Hız Yüksek

Domain Service’in en büyük avantajlarından biri test edilebilirliğidir. Herhangi bir mock framework’üne gerek yoktur — sadece domain nesnelerini oluştur ve Domain Service’i çağır.

class PricingServiceTest {

private PricingService pricingService;
    @BeforeEach
    void setUp() {
        // Tek satır setup - mock yok, container yok, DB yok
        pricingService = new PricingService();
    }
    // ─── Premium Müşteri İndirimi ────────────────────────────────
    @Test
    void premium_musteri_yuzde_on_indirim_almali() {
        // Given
        Customer premiumCustomer = CustomerTestData.premiumCustomer();
        List<OrderItemRequest> items = List.of(
            new OrderItemRequest(ProductId.generate(), Money.ofTRY("200.00"), Quantity.of(1))
        );
        // When
        PricingResult result = pricingService.calculateOrderPrice(
            premiumCustomer,
            items,
            List.of(),          // Kampanya yok
            LoyaltyPoints.ZERO  // Puan yok
        );
        // Then
        assertThat(result.membershipDiscount()).isEqualTo(Money.ofTRY("20.00")); // %10
        assertThat(result.shippingCost()).isEqualTo(Money.ZERO_TRY);            // Premium: ücretsiz kargo
        assertThat(result.finalTotal()).isEqualTo(Money.ofTRY("180.00"));       // 200 - 20
    }
    // ─── Kampanya vs Üyelik: En İyi İndirim Seçimi ──────────────
    @Test
    void kampanya_premium_indiriminden_buyukse_kampanya_secilmeli() {
        // Given
        Customer premiumCustomer = CustomerTestData.premiumCustomer(); // %10 indirim
        Money subtotal = Money.ofTRY("500.00");
        List<OrderItemRequest> items = List.of(
            new OrderItemRequest(ProductId.generate(), Money.ofTRY("500.00"), Quantity.of(1))
        );
        // %15 kampanya - premium %10'dan büyük
        Campaign bigCampaign = CampaignTestData.percentageCampaign(Percentage.of(15));
        // When
        PricingResult result = pricingService.calculateOrderPrice(
            premiumCustomer,
            items,
            List.of(bigCampaign),
            LoyaltyPoints.ZERO
        );
        // Then: Kampanya seçilmeli (%15 > %10)
        assertThat(result.campaignDiscount()).isEqualTo(Money.ofTRY("75.00")); // %15
        assertThat(result.membershipDiscount()).isEqualTo(Money.ofTRY("50.00")); // %10
        assertThat(result.appliedPrimaryDiscount()).isEqualTo(Money.ofTRY("75.00")); // büyük olan
    }
    // ─── Ücretsiz Kargo Eşiği ───────────────────────────────────
    @Test
    void yuz_elli_tl_uzeri_siparisinlerde_kargo_ucretsiz_olmali() {
        // Given
        Customer standardCustomer = CustomerTestData.standardCustomer();
        List<OrderItemRequest> items = List.of(
            new OrderItemRequest(ProductId.generate(), Money.ofTRY("200.00"), Quantity.of(1))
        );
        // When
        PricingResult result = pricingService.calculateOrderPrice(
            standardCustomer, items, List.of(), LoyaltyPoints.ZERO
        );
        // Then
        assertThat(result.shippingCost()).isEqualTo(Money.ZERO_TRY);
    }
    @Test
    void yuz_elli_tl_altindaki_siparislerde_kargo_ucretli_olmali() {
        // Given
        Customer standardCustomer = CustomerTestData.standardCustomer();
        List<OrderItemRequest> items = List.of(
            new OrderItemRequest(ProductId.generate(), Money.ofTRY("100.00"), Quantity.of(1))
        );
        // When
        PricingResult result = pricingService.calculateOrderPrice(
            standardCustomer, items, List.of(), LoyaltyPoints.ZERO
        );
        // Then
        assertThat(result.shippingCost()).isEqualTo(Money.ofTRY("29.90"));
    }
    // ─── Dolandırıcılık Tespiti Testi ───────────────────────────
    @Test
    void yuksek_tutarli_yeni_musteri_review_durumuna_dusmeli() {
        FraudDetectionService fraudService = new FraudDetectionService();
        // Given
        Customer newCustomer = CustomerTestData.newCustomer();
        Order highValueOrder = OrderTestData.orderWithAmount(Money.ofTRY("2000.00"));
        CustomerOrderHistory emptyHistory = CustomerOrderHistory.empty();
        PaymentMethod firstTimeCard = PaymentMethodTestData.firstUsageCard();
        // When
        FraudAssessment assessment = fraudService.assess(
            highValueOrder, newCustomer, emptyHistory, firstTimeCard
        );
        // Then
        assertThat(assessment.requiresReview()).isTrue();
        assertThat(assessment.riskScore()).isGreaterThanOrEqualTo(50);
    }
}

Bu test sınıfı, Domain Service’in neden en kolay test edilen domain yapısı olduğunu somut olarak göstermektedir. @BeforeEach metodunda tek satır var: new PricingService(). Spring context yok, veritabanı yok, Kafka yok, mock framework yok. Her test yalnızca domain nesneleri oluşturuyor ve sonucu doğruluyor. Bu testler son derece hızlı çalışır (milisaniyeler) ve hiçbir harici bağımlılık gerektirmez. Bu test hızı ve bağımsızlığı, Domain Service'lerin gerçekten infrastructure'dan yalıtık olduğunun en güçlü kanıtıdır.

10. Trendyol Örneği: Domain Service’lerin Tam Haritası

10.1 Ordering Context — Domain Service Ekosistemi

Bu diyagram, Trendyol’un sipariş context’indeki Domain Service ekosistemini kapsamlı biçimde göstermektedir. Dört Domain Service her birinin giriş parametrelerini ve çıkış değerlerini açıkça listeler. Her Domain Service’in neyi aldığı ve neyi döndürdüğü bu diyagramdan okunabilmektedir; bu bilgi hem tasarım kararları için hem de ekip içi iletişim için güçlü bir referans noktası sağlar. Policy’ler (DiscountPolicy, ShippingPolicy) Domain Service’lere inject edilebilir ya da parametre olarak geçilebilir. Application Service’ler (PlaceOrderCommandHandler vb.) tüm bu Domain Service'leri ve Policy'leri koordine eden orkestratörleridir.

11. Sonuç: Vatansızlık Bir Güç Kaynağıdır

Domain Service, DDD’nin en az anlaşılan ama en güçlü araçlarından biridir. “Vatansızlık” (statelessness), bir zayıflık değil, kasıtlı bir tasarım güçtür. Hiçbir state tutmayan bir sınıf; thread-safe’dir, test edilmesi zahmetsizdir, paylaşılması güvenlidir ve değişimi izoledir.

GoF’un Strategy pattern’i bize davranışları kapsüllemeyi öğretti. DDD bu öğretiyi domain bağlamına taşıdı: bir iş kuralını DiscountPolicy olarak adlandırmak, hem teknik organizasyonu hem de domain anlamını aynı anda ifade eder. Domain Service ise bu Policy'leri orkestre eden, birden fazla Aggregate'i kapsayan karmaşık iş operasyonlarını saf biçimde ifade eder.

Bu serinin geri kalanında, Domain Service’in destekçilerini — Repository, Factory ve Domain Event — teker teker inceleyeceğiz. Bu üç yapı, Domain Service’in ihtiyaç duyduğu domain nesnelerini sağlamak, oluşturmak ve değişimleri kaydetmek için kritik roller oynar.

Serinin Bir Sonraki Makalesinde

**Makale 7: Repository — Kalıcılığın Soyutlaması**

“Bu siparişi veritabanından getir” cümlesi, domain dünyasında şöyle ifade edilir: orderRepository.findById(orderId). Repository, domain katmanının persistence teknolojisinden tamamen habersiz kalmasını sağlayan köprüdür. GoF'un Proxy ve Facade pattern'lerinin domain-aware birleşimi olan Repository'nin anatomisini, doğru interface tasarımını, Specification pattern entegrasyonunu ve test stratejisini derinlemesine inceliyoruz.

**İçindekiler… « Önceki [Makale 5: Aggregate — Tutarlılığın Kalesi] » Sonraki **[Makale 7: Repository — Kalıcılığın Soyutlaması]


메타데이터
post_id
b2c12654e5ef
slug
dddnin-kalbinde-design-patterns-makale-6-domain-service-i̇ş-mantığının-vatansızları-b2c12654e5ef
url
https://medium.com/@sahinyelkenci/dddnin-kalbinde-design-patterns-makale-6-domain-service-i%CC%87%C5%9F-mant%C4%B1%C4%9F%C4%B1n%C4%B1n-vatans%C4%B1zlar%C4%B1-b2c12654e5ef
canonical_url
https://medium.com/@sahinyelkenci/dddnin-kalbinde-design-patterns-makale-6-domain-service-i%CC%87%C5%9F-mant%C4%B1%C4%9F%C4%B1n%C4%B1n-vatans%C4%B1zlar%C4%B1-b2c12654e5ef
author_url
https://medium.com/@sahinyelkenci
status
ok
fetched_at
2026-06-09 14:34:10