← Back to list

DDD’nin Kalbinde Design Patterns — Makale 12: Command Pattern — CQRS’nin Temeli

Command Pattern, isteği nesneye dönüştürür; niyeti görünür kılar, yazma/okuma sorumluluklarını ayırarak CQRS’ye temel olur.

Sahin Yelkenci · 2026-05-02 18:05 · 0 claps · 15.7 min read
#domain-driven-design #design-patterns #command-pattern #cqrs #query
Open on Medium ↗
Wiki topics: 🥊 · Combat Sports

DDD’nin Kalbinde Design Patterns — Makale 12: Command Pattern — CQRS’nin Temeli

İçindekiler

» Makale 12: Command Pattern — CQRS’nin Temeli » Giriş: Bir İsteği Nesneye Dönüştürmenin Derin Anlamı » 1. GoF Command Pattern: Temel Yapı ve Motivasyon » 2. CQRS: Command ve Query’nin Ayrılması » 3. Command Tasarımı: Doğru Command Nesnesi Nasıl Olur? » 4. Command Handler: Koordinasyonun Ustası » 5. Validation Pipeline: Command’ı Doğrulamak » 6. GoF Bağlantısı: Command + Mediator Pattern » 7. Command’ın Undo/Redo Boyutu: Compensating Command » 8. Query Side: CQRS’nin Diğer Yarısı » 9. Command’ın Kuyruğa Alınması: Asenkron Yürütme » 10. Trendyol Örneği: Tam CQRS Command Side Mimarisi » 11. Anti-Pattern’ler: Command Pattern Yanlış Kullanımı » 12. Sonuç: İsteği Nesneye Dönüştürmenin Mimari Gücü » Serinin Bir Sonraki Makalesinde

Giriş: Bir İsteği Nesneye Dönüştürmenin Derin Anlamı

Yazılım sistemlerinde bir kullanıcı işlemi gerçekleştirmek istediğinde — sipariş vermek, ödeme yapmak, adresi güncellemek — bu istek nasıl temsil edilmelidir? Geleneksel yaklaşımda istek, doğrudan bir metod çağrısına dönüşür: orderService.placeOrder(customerId, items, address). Bu çalışır. Ama bu yaklaşımın gizli bir maliyeti vardır.

Metodun parametreleri zaman içinde değişir — yeni bir alan eklenir, bir alan kaldırılır. Her değişiklik, bu metodu çağıran tüm yerlerde değişikliği zorunlu kılar. Metodun neyi değiştirdiği, neyi sorguladığı belirsizdir — service.process() hem okuma hem yazma yapıyor olabilir. Bir isteği kuyruğa almak, loglamak, tekrar oynatmak istediğinizde imkânı yoktur — çünkü istek sadece bir metod çağrısıdır, bellekte yaşayan bir nesne değil. Ve en önemlisi, bu yaklaşım validation, authorization ve business rule kontrollerinin nerede yapılacağını belirsiz bırakır.

GoF’un Command pattern’i, bu sorunlara 1994'te bir çözüm getirdi: isteği bir nesneye dönüştür. Bu nesne, isteğin tüm parametrelerini taşısın, kim gönderdiğini bilsin, ne zaman gönderildiğini bilsin. Bir metod çağrısı artık geri alınamaz; ama bir Command nesnesi kuyruğa alınabilir, loglanabilir, tekrar oynatılabilir, iptal edilebilir.

DDD bu öğretiyi CQRS (Command Query Responsibility Segregation) mimarisinin temeli haline getirdi. Command, sistemi değiştiren tüm istekleri temsil eder. Query, sistemden okuyan tüm istekleri temsil eder. Bu iki sorumluluk birbirinden ayrıldığında, yazma ve okuma modelleri bağımsız olarak evrilebilir, ölçeklenebilir ve optimize edilebilir.

Bu makale, Command pattern’inin anatomisini, DDD bağlamındaki evrimini, Command Handler mekanizmasını, validation pipeline’ını, GoF Mediator ile bağlantısını ve Trendyol gibi bir e-ticaret sistemindeki CQRS Command side mimarisini derinlemesine inceliyor.

1. GoF Command Pattern: Temel Yapı ve Motivasyon

1.1 Pattern’in Anatomisi

GoF Command pattern’i dört temel bileşenden oluşur. Command, bir isteği kapsüller — tüm parametreleri içerir ve execute() metoduna sahiptir. ConcreteCommand, gerçek iş mantığını içerir ve bir Receiver'a delege eder. Invoker, Command'ı ne zaman çalıştıracağına karar verir. Receiver, gerçek iş mantığını bilen nesnedir.

Bu yapının vaadi şudur: Invoker, ne yapıldığını bilmez — sadece Command nesnesini çalıştırır. Command, nasıl yapıldığını bilmez — sadece Receiver’a delege eder. Receiver, kim sorduğunu bilmez — sadece kendi işini yapar. Bu üç seviyeli bilgisizlik, sistemin esnekliğinin kaynağıdır.

Bu diyagram, GoF Command pattern’inin tam yapısını ve sağladığı beş kritik yeteneği bir arada göstermektedir. Bileşenler tarafında dört aktörün birbirinden ne kadar yalıtık olduğu görülmektedir: Invoker yalnızca Command arayüzünü bilir, ConcreteCommand yalnızca Receiver’a delege eder, Receiver kendi iş mantığını bilir. Bu üçlü yalıtım, Command pattern’in gücünün kaynağıdır. Kazanımlar tarafında ise beş güçlü yetenek listelenmektedir: kuyruğa alma, loglama, undo/redo, makro command ve gecikmiş çalışma. Bu yeteneklerin tamamı, isteği nesneye dönüştürmenin doğal sonuçlarıdır.

1.2 GoF Command → DDD CQRS Command: Evrim

GoF Command, teknik bir soruyu cevaplar: “Bir isteği nasıl kapsüllerim?” DDD’nin CQRS Command’ı ise iki farklı soruyu birlikte cevaplar: “Bu istek sistemi değiştiriyor mu yoksa sorguluyor mu? Ve bu değiştirme isteğini nasıl açık, doğrulanabilir ve izlenebilir bir nesne olarak ifade ederim?”

Bu diyagram, GoF Command’dan DDD CQRS Command’a olan beş boyutlu evrimsel dönüşümü göstermektedir. İsim dönüşümü, Ubiquitous Language’ı yansıtır: CommandPlaceOrderCommand. Parametre dönüşümü, tip güvenliğini artırır: Object[]CustomerId, List<OrderItemRequest>. Sorumluluk dönüşümü, Command nesnesini sadece veri taşıyıcısına indirgeyerek execute() mantığını ayrı bir Handler sınıfına taşır. Validation eklenmesi, komutun geçerli olup olmadığını sisteme girmeden önce kontrol eder. RequestId eklenmesi, idempotent processing'i mümkün kılar. Bu beş dönüşüm birlikte, GoF'un teknik pattern'ini domain-aware, test edilebilir ve güvenilir bir yapıya taşır.

2. CQRS: Command ve Query’nin Ayrılması

2.1 CQS’ten CQRS’ye: Soyutlama Seviyesi Yükseliyor

Bertrand Meyer 1988'de CQS (Command Query Separation) prensibini tanımladı: “Her metod ya state değiştiren bir komut (command) olmalı ya da state döndüren bir sorgu (query) olmalı; her ikisi birden olmamalı.”

CQRS, bu prensibi metod seviyesinden mimari seviyeye taşır: yazma modeli (Write Model / Command Side) ve okuma modeli (Read Model / Query Side) tamamen ayrı yapılarda organize edilir. Bu ayrım, her iki tarafın birbirinden bağımsız olarak optimize edilmesini, ölçeklendirilmesini ve evrilebilmesini sağlar.

Bu diyagram, CQS ve CQRS arasındaki soyutlama seviyesi farkını netleştirmektedir. CQS metod seviyesinde çalışır: aynı sınıf içinde Command ve Query metodları bir arada yaşayabilir, ama her metod yalnızca biri olmalıdır. CQRS mimari seviyede çalışır: Write Side ve Read Side tamamen ayrı yapılara sahiptir. Write Side, güçlü tutarlılık gerektiren Aggregate’ler ve Domain Event’lerle çalışır. Read Side ise hızlı okuma için optimize edilmiş, denormalize edilmiş Projection’larla çalışır. Domain Event, Write Side’dan Read Side’a veri akışını sağlar: bir Aggregate güncellendiğinde yayımlanan event, ilgili Projection’ları günceller.

2.2 Command Side Mimarisi: Katmanlar ve Sorumluluklar

Bu diyagram, CQRS’nin Command Side mimarisini katmanlar ve sorumluluklar açısından göstermektedir. Presentation katmanı dış dünyadan gelen istekleri (HTTP, Kafka) Command nesnelerine dönüştürür ve Command Bus’a iletir. Application katmanında üç ayrı yapı çalışır: Command Bus (Mediator olarak hangi handler’a yönlendirileceğini belirler), Validation Pipeline (komutun geçerli olup olmadığını kontrol eder) ve Command Handler (gerçek koordinasyonu yapar). Domain katmanında Aggregate iş mantığını yürütür ve event üretir. Infrastructure katmanında Repository ve Event Publisher teknik detayları yönetir. Bu dört katmanın her birinin net sorumlulukları, sistemi hem test edilebilir hem de değişime açık kılar.

3. Command Tasarımı: Doğru Command Nesnesi Nasıl Olur?

3.1 Command’ın Anatomisi

// ─────────────────────────────────────────────────────────────────
// DOMAIN/APPLICATION KATMANI — Command Base
// Tüm Command'ların ortak özelliklerini tanımlar
// ─────────────────────────────────────────────────────────────────
public interface Command {
    /**
     * Idempotency için benzersiz istek kimliği.
     * Aynı Command iki kez gönderilirse (network retry),
     * requestId kontrolüyle ikinci işlem atlanır.
     */
    RequestId requestId();
}

// ─────────────────────────────────────────────────────────────────
// Sipariş Oluşturma Command'ı
// ─────────────────────────────────────────────────────────────────
public record PlaceOrderCommand(
    RequestId         requestId,        // Idempotency
    CustomerId        customerId,       // Kim sipariş veriyor?
    List<OrderItemRequest> items,       // Ne sipariş ediliyor?
    ShippingAddress   shippingAddress,  // Nereye teslim edilecek?
    PaymentMethod     paymentMethod,    // Nasıl ödeme yapılacak?
    LoyaltyPoints     loyaltyPointsToUse, // Puan kullanılacak mı?
    String            couponCode        // Kupon kodu (opsiyonel)
) implements Command {
    // Compact constructor - temel doğrulama
    public PlaceOrderCommand {
        Objects.requireNonNull(requestId,       "RequestId zorunludur");
        Objects.requireNonNull(customerId,      "CustomerId zorunludur");
        Objects.requireNonNull(shippingAddress, "Teslimat adresi zorunludur");
        Objects.requireNonNull(paymentMethod,   "Ödeme yöntemi zorunludur");
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException(
                "Sipariş en az bir ürün içermelidir"
            );
        }
        // Derin kopyalama - dışarıdan liste değiştirilemez
        items = List.copyOf(items);
    }
}
// ─────────────────────────────────────────────────────────────────
// Sipariş İptal Command'ı
// ─────────────────────────────────────────────────────────────────
public record CancelOrderCommand(
    RequestId          requestId,
    OrderId            orderId,
    CancellationReason reason,
    String             customerNote  // Opsiyonel müşteri notu
) implements Command {
    public CancelOrderCommand {
        Objects.requireNonNull(requestId, "RequestId zorunludur");
        Objects.requireNonNull(orderId,   "OrderId zorunludur");
        Objects.requireNonNull(reason,    "İptal nedeni zorunludur");
    }
}
// ─────────────────────────────────────────────────────────────────
// Teslimat Adresi Güncelleme Command'ı
// ─────────────────────────────────────────────────────────────────
public record UpdateShippingAddressCommand(
    RequestId       requestId,
    OrderId         orderId,
    CustomerId      customerId,       // Yetki kontrolü için
    ShippingAddress newAddress
) implements Command { }
// ─────────────────────────────────────────────────────────────────
// Ödeme Onaylama Command'ı (payment service'ten gelir)
// ─────────────────────────────────────────────────────────────────
public record ConfirmPaymentCommand(
    RequestId     requestId,
    OrderId       orderId,
    PaymentId     paymentId,
    Money         confirmedAmount,
    PaymentMethod paymentMethod
) implements Command { }

3.2 Command Tasarım İlkeleri

Bu diyagram, Command tasarımında uyulması gereken beş temel ilkeyi ve her ilkenin doğru/yanlış örneklerini göstermektedir. Immutability ilkesi, Java record kullanımını zorunlu kılar — mutable Command'lar handler çalışırken veri değişimi riskini beraberinde getirir. Typed alanlar ilkesi, primitive kullanımının yarattığı tip güvensizliğini ortadan kaldırır. Minimum gerekli veri ilkesi, Fat Command anti-pattern'inden kaçınır. RequestId ilkesi, network retry'lardan kaynaklanan mükerrer işlemleri önler. İş kavramları taşıma ilkesi, Ubiquitous Language'ı Command düzeyinde yaşatır.

4. Command Handler: Koordinasyonun Ustası

4.1 Command Handler’ın Tek Sorumluluğu

Command Handler, bir use case’in tüm koordinasyonunu yönetir. Ama “koordinasyon” kelimesi dikkatli kullanılmalıdır: Handler, iş mantığı yazmaz — iş mantığını orchestrate eder. İş mantığı Domain Service’te ve Aggregate’te yaşar; Handler ise doğru nesneleri doğru sırayla bir araya getirir.

// ─────────────────────────────────────────────────────────────────
// APPLICATION KATMANI — PlaceOrderCommandHandler
// Use case koordinasyonu — iş mantığı değil!
// ─────────────────────────────────────────────────────────────────
@Service
@Transactional
@RequiredArgsConstructor
@Slf4j
public class PlaceOrderCommandHandler
        implements CommandHandler<PlaceOrderCommand, OrderId> {

// Repository'ler - domain nesnelerini yüklemek için
    private final CustomerRepository        customerRepository;
    private final CampaignRepository        campaignRepository;
    private final LoyaltyAccountRepository  loyaltyAccountRepository;
    private final OrderHistoryRepository    orderHistoryRepository;
    // Domain Service'ler - iş mantığı burada yaşıyor
    private final PricingService            pricingService;
    private final FraudDetectionService     fraudDetectionService;
    // Factory - Aggregate yaratımı için
    private final OrderFactory              orderFactory;
    // Repository - kaydetmek için
    private final OrderRepository           orderRepository;
    // Event Publisher - domain event'lerini yayımlamak için
    private final DomainEventPublisher      eventPublisher;
    // Idempotency kontrolü
    private final ProcessedCommandRepository processedCommandRepository;
    @Override
    public OrderId handle(PlaceOrderCommand command) {
        log.info("PlaceOrderCommand işleniyor: requestId={}, customerId={}",
            command.requestId(), command.customerId());
        // ── Adım 0: Idempotency Kontrolü ─────────────────────────
        Optional<ProcessedCommand> existing =
            processedCommandRepository.findByRequestId(command.requestId());
        if (existing.isPresent()) {
            log.warn("Duplicate command atlandı: requestId={}",
                command.requestId());
            return existing.get().resultAsOrderId();
        }
        // ── Adım 1: Gerekli Domain Nesnelerini Yükle ─────────────
        Customer customer = customerRepository
            .findById(command.customerId())
            .orElseThrow(() -> new CustomerNotFoundException(
                command.customerId()
            ));
        List<Campaign> activeCampaigns =
            campaignRepository.findActiveForCustomer(command.customerId());
        LoyaltyAccount loyaltyAccount =
            loyaltyAccountRepository.findByCustomerId(command.customerId())
                                    .orElse(LoyaltyAccount.empty(command.customerId()));
        CustomerOrderHistory orderHistory =
            orderHistoryRepository.findByCustomerId(command.customerId());
        // ── Adım 2: Domain Factory ile Order Yarat ───────────────
        // Factory tüm iş kuralı doğrulamalarını yapar
        // (fiyatlandırma, fraud, invariant'lar)
        Order order = orderFactory.create(
            command.customerId(),
            customer,
            command.items(),
            command.shippingAddress(),
            activeCampaigns,
            command.loyaltyPointsToUse(),
            orderHistory,
            command.paymentMethod()
        );
        // ── Adım 3: Kaydet ────────────────────────────────────────
        orderRepository.save(order);
        // ── Adım 4: Domain Event'lerini Yayımla ──────────────────
        List<DomainEvent> events = order.domainEvents();
        eventPublisher.publishAll(events);
        order.clearDomainEvents();
        // ── Adım 5: Idempotency Kaydı Oluştur ────────────────────
        processedCommandRepository.save(new ProcessedCommand(
            command.requestId(),
            PlaceOrderCommand.class.getSimpleName(),
            order.id().toString(),
            Instant.now()
        ));
        log.info("PlaceOrderCommand tamamlandı: orderId={}", order.id());
        return order.id();
    }
}
// ─────────────────────────────────────────────────────────────────
// CommandHandler Generic Interface
// ─────────────────────────────────────────────────────────────────
public interface CommandHandler<C extends Command, R> {
    R handle(C command);
}
// ─────────────────────────────────────────────────────────────────
// CancelOrderCommandHandler
// ─────────────────────────────────────────────────────────────────
@Service
@Transactional
@RequiredArgsConstructor
public class CancelOrderCommandHandler
        implements CommandHandler<CancelOrderCommand, Void> {
    private final OrderRepository      orderRepository;
    private final DomainEventPublisher eventPublisher;
    @Override
    public Void handle(CancelOrderCommand command) {
        // Siparişi yükle
        Order order = orderRepository
            .findById(command.orderId())
            .orElseThrow(() -> new OrderNotFoundException(command.orderId()));
        // Yetki kontrolü - bu müşterinin siparişi mi?
        if (!order.customerId().equals(command.customerId())) {
            throw new UnauthorizedOrderAccessException(
                "Bu sipariş bu müşteriye ait değil"
            );
        }
        // Domain operasyonu - iş mantığı Order'da
        order.cancel(command.reason());
        // Kaydet ve event yayımla
        orderRepository.save(order);
        eventPublisher.publishAll(order.domainEvents());
        order.clearDomainEvents();
        return null;
    }
}

4.2 Command Handler Akış Diyagramı

Bu diyagram, PlaceOrderCommandHandler.handle() metodunun her adımını ve her karar noktasını görsel olarak haritalamaktadır. Akış altı temel aşamadan geçer: idempotency kontrolü, domain nesnelerini yükleme, Factory ile Aggregate yaratma, kaydetme, event yayımlama ve idempotency kaydı. Her noktada olası exception senaryoları da gösterilmektedir. Özellikle dikkat çeken iki nokta vardır: idempotency kontrolü en başta yapılır (gereksiz işlemi önlemek için) ve Factory'nin fraud değerlendirmesi FraudRiskOrderBlockedException fırlatabilir. Bu diyagram, hem geliştirici dokümantasyonu hem de code review referansı olarak kullanılabilir.

5. Validation Pipeline: Command’ı Doğrulamak

5.1 İki Seviyeli Validation: Teknik ve Domain

Bir Command’ın iki farklı seviyede doğrulanması gerekir. Birincisi teknik validation: alanların null olmadığı, format gereksinimlerini karşıladığı, tip uyumluluğunun sağlandığı kontrolü. İkincisi domain validation: iş kurallarına uygunluk kontrolü — müşteri aktif mi? Ürün satışta mı? Kota aşıldı mı?

Bu iki seviye, farklı katmanlarda ve farklı araçlarla gerçekleştirilir.

Bu diyagram, iki seviyeli validation’ın her birinin ne doğruladığını, ne zaman çalıştığını, nasıl implement edildiğini ve örnek senaryolarını karşılaştırmalı olarak göstermektedir. Teknik validation, format ve yapısal uygunluğu kontrol eder; Spring Boot’un Bean Validation entegrasyonu sayesinde Handler’a girmeden önce otomatik çalışır. Domain validation ise iş kuralı uygunluğunu kontrol eder; bu kontroller Handler içinde, Aggregate metod guard’larında veya Domain Service’lerde gerçekleşir ve domain-specific exception’lar fırlatır.

5.2 Validation Pipeline Implementasyonu

// ─────────────────────────────────────────────────────────────────
// LEVEL 1 — Bean Validation ile teknik doğrulama
// ─────────────────────────────────────────────────────────────────

// PlaceOrderCommand'daki validation annotasyonları
public record PlaceOrderCommand(
    @NotNull(message = "RequestId zorunludur")
    RequestId requestId,
    @NotNull(message = "Müşteri kimliği zorunludur")
    CustomerId customerId,
    @NotNull @NotEmpty(message = "En az bir ürün seçilmelidir")
    List<@Valid OrderItemRequest> items,
    @NotNull(message = "Teslimat adresi zorunludur")
    @Valid ShippingAddress shippingAddress,
    @NotNull(message = "Ödeme yöntemi zorunludur")
    PaymentMethod paymentMethod,
    LoyaltyPoints loyaltyPointsToUse,  // Opsiyonel
    @Size(max = 50, message = "Kupon kodu en fazla 50 karakter olabilir")
    String couponCode
) implements Command { }
// OrderItemRequest validation
public record OrderItemRequest(
    @NotNull ProductId productId,
    @NotNull
    @Size(min = 1, max = 200, message = "Ürün adı 1-200 karakter olmalıdır")
    String productName,
    @NotNull @Valid Money unitPrice,
    @NotNull @Valid Quantity quantity
) { }
// ─────────────────────────────────────────────────────────────────
// LEVEL 2 - Domain validation pipeline
// ─────────────────────────────────────────────────────────────────
// Validation pipeline interface
@FunctionalInterface
public interface CommandValidator<C extends Command> {
    void validate(C command);
}
// Müşteri aktiflik doğrulayıcı
@Component
@RequiredArgsConstructor
public class CustomerActiveValidator
        implements CommandValidator<PlaceOrderCommand> {
    private final CustomerRepository customerRepository;
    @Override
    public void validate(PlaceOrderCommand command) {
        Customer customer = customerRepository
            .findById(command.customerId())
            .orElseThrow(() -> new CustomerNotFoundException(
                command.customerId()
            ));
        if (customer.status() == CustomerStatus.BLOCKED) {
            throw new CustomerBlockedException(
                "Müşteri hesabı bloke edilmiştir: " + command.customerId()
            );
        }
        if (customer.status() == CustomerStatus.SUSPENDED) {
            throw new CustomerSuspendedException(
                "Müşteri hesabı askıya alınmıştır: " + command.customerId()
            );
        }
    }
}
// Günlük sipariş limiti doğrulayıcı
@Component
@RequiredArgsConstructor
public class DailyOrderLimitValidator
        implements CommandValidator<PlaceOrderCommand> {
    private static final int MAX_DAILY_ORDERS = 10;
    private final OrderRepository orderRepository;
    @Override
    public void validate(PlaceOrderCommand command) {
        int todayOrderCount = orderRepository
            .countTodayOrdersByCustomer(command.customerId());
        if (todayOrderCount >= MAX_DAILY_ORDERS) {
            throw new DailyOrderLimitExceededException(
                String.format(
                    "Günlük sipariş limiti aşıldı. Maksimum: %d, Bugünkü: %d",
                    MAX_DAILY_ORDERS, todayOrderCount
                )
            );
        }
    }
}
// Ürün satış durumu doğrulayıcı
@Component
@RequiredArgsConstructor
public class ProductAvailabilityValidator
        implements CommandValidator<PlaceOrderCommand> {
    private final CatalogServiceClient catalogClient;
    @Override
    public void validate(PlaceOrderCommand command) {
        List<ProductId> unavailableProducts = command.items().stream()
            .map(OrderItemRequest::productId)
            .filter(productId -> !catalogClient.isProductSellable(productId))
            .collect(toList());
        if (!unavailableProducts.isEmpty()) {
            throw new ProductsNotAvailableException(
                "Aşağıdaki ürünler satışta değil: " + unavailableProducts
            );
        }
    }
}
// ─────────────────────────────────────────────────────────────────
// Validation Orchestrator - tüm validator'ları sırayla çalıştırır
// ─────────────────────────────────────────────────────────────────
@Component
@RequiredArgsConstructor
public class PlaceOrderCommandValidationPipeline {
    // Spring, tüm CommandValidator<PlaceOrderCommand> bean'lerini inject eder
    private final List<CommandValidator<PlaceOrderCommand>> validators;
    public void validate(PlaceOrderCommand command) {
        validators.forEach(validator -> validator.validate(command));
    }
}

5.3 Validation Pipeline Akışı

Bu diyagram, PlaceOrderCommand'ın validation pipeline'ını katmanlar ve olası çıktılarla birlikte göstermektedir. İlk katman teknik validation'dır: altı Bean Validation kontrolü otomatik olarak çalışır ve herhangi biri başarısız olursa 400 Bad Request döner. İkinci katman domain validation'dır: üç iş kuralı kontrolü sırayla çalışır ve herhangi biri başarısız olursa 422 Unprocessable Entity döner. Her iki katmandan da başarıyla geçen Command, Handler'a ulaşır ve işleme alınır. Bu pipeline, invalid Command'ların hiçbir zaman Aggregate'e ulaşmamasını garanti eder.

6. GoF Bağlantısı: Command + Mediator Pattern

6.1 Command Bus: Mediator olarak

GoF’un Mediator pattern’i, nesnelerin birbirleriyle doğrudan iletişim kurması yerine merkezi bir aracı üzerinden iletişim kurmasını sağlar. CQRS mimarisinde Command Bus, tam olarak bu rolü oynar: bir Command geldiğinde, doğru Handler’a yönlendirmeyi Command Bus yönetir.

Bu tasarım, Presentation katmanının Application katmanındaki Handler’ları bilmesine gerek kalmaz. OrderController, PlaceOrderCommandHandler'ı inject almaz; sadece Command Bus'a bir Command gönderir. Command Bus, hangi Handler'ın hangi Command tipiyle ilgilendiğini bilir ve yönlendirmeyi yapar.

Bu diyagram, Command Bus’ın Mediator rolünü net biçimde göstermektedir. Üç farklı göndericiden (REST Controller, Kafka Consumer, Scheduler) gelen Command’lar, doğrudan Handler’lara değil Command Bus’a iletilir. Command Bus, Handler Registry’sinden doğru Handler’ı bulur ve yönlendirmeyi yapar. Controller, PlaceOrderCommandHandler'ı inject almak zorunda değildir; sadece Command Bus'a ihtiyacı vardır. Bu tasarım, yeni bir Handler eklendiğinde Controller'ın değişmemesini garanti eder.

6.2 Command Bus Implementasyonu

// ─────────────────────────────────────────────────────────────────
// APPLICATION KATMANI — CommandBus (Mediator implementasyonu)
// ─────────────────────────────────────────────────────────────────
public interface CommandBus {
    <C extends Command, R> R dispatch(C command);
}

@Component
@RequiredArgsConstructor
@Slf4j
public class SpringCommandBus implements CommandBus {
    // Spring, tüm CommandHandler bean'lerini inject eder
    private final ApplicationContext context;
    // Tip bazlı handler cache
    private final Map<Class<?>, CommandHandler<?, ?>> handlerCache =
        new ConcurrentHashMap<>();
    @Override
    @SuppressWarnings("unchecked")
    public <C extends Command, R> R dispatch(C command) {
        Class<?> commandType = command.getClass();
        CommandHandler<C, R> handler = (CommandHandler<C, R>)
            handlerCache.computeIfAbsent(commandType, this::findHandler);
        log.debug("Command dispatch ediliyor: {} → {}",
            commandType.getSimpleName(),
            handler.getClass().getSimpleName());
        return handler.handle(command);
    }
    private CommandHandler<?, ?> findHandler(Class<?> commandType) {
        // Spring context'ten ilgili Handler'ı bul
        Map<String, ?> handlers = context.getBeansOfType(CommandHandler.class);
        return handlers.values().stream()
            .filter(handler -> isHandlerFor(handler, commandType))
            .findFirst()
            .map(h -> (CommandHandler<?, ?>) h)
            .orElseThrow(() -> new NoHandlerFoundException(
                "Command için handler bulunamadı: " + commandType.getSimpleName()
            ));
    }
    private boolean isHandlerFor(Object handler, Class<?> commandType) {
        return Arrays.stream(handler.getClass().getGenericInterfaces())
            .filter(t -> t instanceof ParameterizedType)
            .map(t -> (ParameterizedType) t)
            .filter(t -> t.getRawType() == CommandHandler.class)
            .anyMatch(t -> t.getActualTypeArguments()[0] == commandType);
    }
}
// ─────────────────────────────────────────────────────────────────
// PRESENTATION KATMANI - OrderController (Command Bus kullanır)
// ─────────────────────────────────────────────────────────────────
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
    private final CommandBus commandBus;
    private final OrderCommandMapper mapper;
    // Handler'ları inject almıyor - sadece CommandBus biliyor
    @PostMapping
    public ResponseEntity<OrderResponse> placeOrder(
            @RequestBody @Valid PlaceOrderRequest request,
            @RequestHeader("X-Request-Id") String requestId) {
        // HTTP Request → Command dönüşümü
        PlaceOrderCommand command = mapper.toCommand(request, requestId);
        // Command Bus'a gönder - Handler'ı bilmiyoruz
        OrderId orderId = commandBus.dispatch(command);
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(new OrderResponse(orderId.toString()));
    }
    @DeleteMapping("/{orderId}")
    public ResponseEntity<Void> cancelOrder(
            @PathVariable String orderId,
            @RequestBody CancelOrderRequest request,
            @RequestHeader("X-Request-Id") String requestId,
            @RequestHeader("X-Customer-Id") String customerId) {
        CancelOrderCommand command = new CancelOrderCommand(
            RequestId.of(requestId),
            OrderId.of(orderId),
            CustomerId.of(customerId),
            CancellationReason.valueOf(request.reason()),
            request.customerNote()
        );
        commandBus.dispatch(command);
        return ResponseEntity.noContent().build();
    }
}

7. Command’ın Undo/Redo Boyutu: Compensating Command

7.1 Compensating Command: Saga’nın Temeli

GoF Command pattern’inin en önemli özelliklerinden biri undo/redo desteğidir. DDD bağlamında bu özellik, Compensating Command kavramıyla hayata geçer. Bir Command başarısız olduğunda ya da geri alınması gerektiğinde, telafi edici bir Command yayımlanır.

Bu kavram, Saga pattern’inin temelini oluşturur (Makale 22'de detaylı incelenecek). Şimdilik temel ilişkiyi anlayalım.

Bu diyagram, Command ve Compensating Command arasındaki simetrik ilişkiyi göstermektedir. İleri yönde dört Command sırayla çalışır: sipariş oluşturma, stok rezervasyonu, ödeme işleme ve sipariş onaylama. Ödeme başarısız olduğunda, telafi zinciri ters yönde devreye girer: önce sipariş iptal edilir, ödeme iade edilir, stok serbest bırakılır ve sipariş temizlenir. Her Command’ın bir telafi karşılığı vardır ve Saga pattern bu zincirleri orchestrate eder. Bu ilişki, GoF Command’ın “undo” yeteneğinin dağıtık sistemlere nasıl taşındığını göstermektedir.

8. Query Side: CQRS’nin Diğer Yarısı

8.1 Query Nesnesi ve Query Handler

CQRS’de Query, sistemin mevcut durumunu sorgular — hiçbir şeyi değiştirmez. Bu prensip, Query Handler’ların @Transactional(readOnly = true) ile işaretlenmesini ve read-optimized modellerin kullanılmasını mümkün kılar.

// ─────────────────────────────────────────────────────────────────
// Query nesneleri — yalnızca sorgulama parametrelerini taşır
// ─────────────────────────────────────────────────────────────────
public interface Query<R> {
    // R: Query'nin döneceği sonuç tipi
}

public record OrderSummaryQuery(
    CustomerId customerId,
    OrderStatus statusFilter,
    int pageNumber,
    int pageSize
) implements Query<Page<OrderSummaryDto>> { }
public record OrderDetailQuery(
    OrderId   orderId,
    CustomerId requestingCustomerId  // Yetki kontrolü için
) implements Query<OrderDetailDto> { }
public record OrderStatisticsQuery(
    CustomerId customerId,
    Instant    from,
    Instant    to
) implements Query<OrderStatisticsDto> { }
// ─────────────────────────────────────────────────────────────────
// Query Handler - read-only, optimize edilmiş sorgular
// ─────────────────────────────────────────────────────────────────
@Service
@Transactional(readOnly = true)  // Yazma kilitlerini engeller
@RequiredArgsConstructor
public class OrderSummaryQueryHandler
        implements QueryHandler<OrderSummaryQuery, Page<OrderSummaryDto>> {
    // Read Model Repository - Write Model'den farklı olabilir!
    private final OrderReadModelRepository readModelRepository;
    @Override
    public Page<OrderSummaryDto> handle(OrderSummaryQuery query) {
        // Denormalize edilmiş read model'i sorgula
        // JOIN yok, projection doğrudan DTO döner
        return readModelRepository.findByCustomerIdAndStatus(
            query.customerId(),
            query.statusFilter(),
            PageRequest.of(query.pageNumber(), query.pageSize())
        );
    }
}
// Read Model - yazma modeline gerek yok, sadece okuma
@Entity
@Table(name = "order_summary_view")  // Materialized view veya projection tablosu
public class OrderSummaryReadModel {
    @Id
    private UUID orderId;
    private UUID customerId;
    private String status;
    private BigDecimal totalAmount;
    private String currency;
    private int itemCount;
    private String shippingCity;
    private Instant createdAt;
    private Instant lastUpdatedAt;
    // Getters...
}

8.2 Write Model ve Read Model Senkronizasyonu

Bu diyagram, Write Model ve Read Model arasındaki senkronizasyon mekanizmasını göstermektedir. Sol tarafta Write Side akışı çalışır: Command gelir, Handler koordine eder, Aggregate iş mantığını yürütür, normalized tabloya kaydedilir ve Domain Event yayımlanır. Ortada Projection Handler, bu event’i dinler ve denormalize edilmiş order_summary_view tablosunu günceller. Sağ tarafta Read Side akışı çalışır: Query gelir, Handler doğrudan denormalize tablodan okur ve DTO döndürür. Bu ayrım sayesinde okuma sorguları JOIN'siz, index-optimized ve cache-friendly hale gelir. Aralarındaki eventual consistency genellikle milisaniyeler mertebesindedir ve çoğu iş senaryosunda kabul edilebilirdir.

9. Command’ın Kuyruğa Alınması: Asenkron Yürütme

9.1 Asenkron Command İşleme

Bazı Command’lar anında işlenmesi gerekmeyen, uzun süren veya yüksek hacimli işlemlerdir. Bu senaryolarda Command’ı kuyruğa alarak asenkron işlemek daha uygun olabilir. GoF Command’ın “kuyruğa alma” yeteneği burada devreye girer.

Bu diyagram, senkron ve asenkron Command işleme arasındaki farkı göstermektedir. Senkron işlemede Controller, Command Bus’a Command gönderir, Handler hemen çalışır ve sonuç döner. Bu yaklaşım, müşterinin anında orderId görmesi gereken PlaceOrderCommand için uygundur. Asenkron işlemede ise Command Bus Command'ı kuyruğa yazar ve hemen 202 Accepted döner. Async Consumer kuyruğu okur ve Handler'ı çalıştırır. Bu yaklaşım, binlerce siparişi toplu iptal eden BatchOrderCancelCommand gibi yoğun işlemler için uygundur.

10. Trendyol Örneği: Tam CQRS Command Side Mimarisi

10.1 Ordering Context — Command Kataloğu

Bu diyagram, Trendyol’un ordering context’indeki tam CQRS Command ve Query kataloğunu göstermektedir. On Command, dört kategoride organize edilmiştir: sipariş yönetimi (4), ödeme yönetimi (3), kargo yönetimi (2) ve iade yönetimi (3). Her Command’ın hangi Handler’a yönlendirildiği ve hangi Domain Event’i ürettiği açıkça belirtilmiştir. Dört Query ise read-optimized Handler’larla işlenir ve DTO döndürür. Command Bus, tüm bu Command ve Handler çiftlerini birbirine bağlayan Mediator olarak merkezi bir konum işgal eder.

11. Anti-Pattern’ler: Command Pattern Yanlış Kullanımı

11.1 Anti-Pattern Kataloğu

Bu diyagram, Command pattern kullanımındaki beş kritik anti-pattern’i kod örnekleri ve somut sonuçlarıyla göstermektedir. Command’da iş mantığı bulundurmak, Command’ın tek sorumluluğunu (veri taşıma) ihlal eder. Mutable Command, asenkron senaryolarda race condition yaratır. God Command, tüm use case’leri tek bir Command’a sıkıştırarak okunabilirliği yok eder. Command’dan event yayımlamak, Handler’ı bypass ederek transaction yönetimini ortadan kaldırır. Command ile Query karıştırmak ise CQS prensibini ve CQRS’nin temel ayrımını çiğner.

12. Sonuç: İsteği Nesneye Dönüştürmenin Mimari Gücü

GoF Command pattern’i 1994'te şunu öğretti: bir isteği nesneye dönüştürmek, o isteği kuyruğa almayı, loglamayı, geri almayı ve tekrar oynatmayı mümkün kılar. Bu, basit bir teknik numara gibi görünür. Ama DDD bu öğretiyi, yazılım mimarisinin en güçlü kavramlarından biriyle birleştirdi: Command Query Responsibility Segregation.

CQRS şunu söyledi: sistemi değiştiren her istek bir Command, sistemden okuyan her istek bir Query’dir. Bu iki sorumluluk ayrıldığında, yazma modeli (güçlü tutarlılık, iş kuralları, Aggregate) ve okuma modeli (hız, denormalizasyon, caching) birbirinden bağımsız olarak optimize edilebilir.

PlaceOrderCommand bir iş talebini — "bu müşteri bu ürünleri bu adrese göndermek istiyor" — açık, doğrulanabilir ve izlenebilir bir nesne olarak ifade eder. PlaceOrderCommandHandler bu talebi koordine eder: gerekli domain nesnelerini yükler, Factory aracılığıyla Aggregate'i oluşturur, kaydeder ve event'leri yayımlar. Validation Pipeline, bu talebin sisteme girmeden önce hem teknik hem de iş kuralı açısından geçerli olduğunu garanti eder. Command Bus (Mediator), Presentation katmanının Handler'lardan habersiz kalmasını sağlar.

Ve bu zincirin her halkası bağımsız olarak test edilebilir, bağımsız olarak evrilebilir, bağımsız olarak optimize edilebilir. GoF Command’ın vaadi, DDD bağlamında tam anlamıyla gerçekleşmiş olur.

Serinin Bir Sonraki Makalesinde

**Makale 13: Specification Pattern — İş Kurallarının Şiiri**

“Ücretsiz kargoya uygun mu?” “Fraud riski yüksek mi?” “Premium üye mi?” Bu sorular iş kurallarıdır ve çoğu sistemde if-else’lerin içine gömülü, dağınık biçimde yaşar. Specification pattern, her iş kuralını isimli, test edilebilir, composable bir nesneye dönüştürür. Eric Evans’ın DDD kitabındaki orijinal tanımından, GoF Composite + Strategy kombinasyonuna, JPA Criteria entegrasyonundan Trendyol’un gerçek specification kütüphanesine derinlemesine inceliyoruz.

**İçindekiler… « Önceki [Makale 11: Observer Pattern — Domain Events’e Köprü] » Sonraki **[Makale 13: Specification Pattern — İş Kurallarının Şiiri]


메타데이터
post_id
107dacba943c
slug
dddnin-kalbinde-design-patterns-makale-12-command-pattern-cqrs-nin-temeli-107dacba943c
url
https://medium.com/@sahinyelkenci/dddnin-kalbinde-design-patterns-makale-12-command-pattern-cqrs-nin-temeli-107dacba943c
canonical_url
https://medium.com/@sahinyelkenci/dddnin-kalbinde-design-patterns-makale-12-command-pattern-cqrs-nin-temeli-107dacba943c
author_url
https://medium.com/@sahinyelkenci
status
ok
fetched_at
2026-07-19 18:06:16