Skip to content

CQRS (Komut ve Sorgu Sorumluluğu Ayrımı) ​

CQRS, yazma (Komut) ve okuma (Sorgu) işlemlerini ayrı modellerde yönetme desenidir. Bu bölümde Spring Boot ile CQRS'in detaylı uygulamasını inceleyeceğiz.

Hızlı Karar ​

DurumCQRS KullanDikkat
Okuma ve yazma modeli belirgin farklıEvetİki modelin senkronizasyonu gerekir
Yüksek okuma trafiği, karmaşık sorgularEvetProjection gecikmesi kullanıcıya anlatılmalı
Basit CRUD ekranıHayırEk handler/model bakım borcu olur
Audit ve event geçmişi önemliEvent Sourcing ile birlikte değerlendirOperasyon karmaşıklığı artar

Üretim Kontrol Listesi ​

  • Problem: Ayrı okuma/yazma modeli gerçek bir karmaşıklığı azaltıyor mu?
  • Çözüm: Command validation, query model güncelleme, projection rebuild ve idempotency net mi?
  • Trade-off: Okuma ve yazma bağımsız optimize edilir; eventual consistency ve çift model maliyeti eklenir.
  • Hata durumu: Projection lag, duplicate command, failed handler ve read model drift senaryoları ele alınmalı.
  • Ölçüm: Command latency, query latency, projection lag, handler error rate ve rebuild süresi izlenmeli.
  • Güvenlik/maliyet: Read model hassas veriyi çoğaltabilir; ekstra storage ve worker maliyeti planlanmalı.

CQRS Mimarisi ​

Genel Mimari Diyagramı ​

Komut ve Sorgu Akış Diyagramı ​

Olay Tabanlı Okuma Modeli Güncelleme Diyagramı ​

Temel Mimari Bileşenler ​

CQRS Yapılandırması:

java
@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.command.repository",
    entityManagerFactoryRef = "commandEntityManagerFactory",
    transactionManagerRef = "commandTransactionManager"
)
@EnableSecondaryJpaRepositories(
    basePackages = "com.example.query.repository",
    entityManagerFactoryRef = "queryEntityManagerFactory",
    transactionManagerRef = "queryTransactionManager"
)
public class CqrsConfiguration {
    
    @Primary
    @Bean(name = "commandDataSource")
    @ConfigurationProperties("spring.datasource.command")
    public DataSource commandDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean(name = "queryDataSource")
    @ConfigurationProperties("spring.datasource.query")
    public DataSource queryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Primary
    @Bean(name = "commandEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean commandEntityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("commandDataSource") DataSource dataSource) {
        
        return builder
            .dataSource(dataSource)
            .packages("com.example.command.entity")
            .persistenceUnit("command")
            .properties(jpaProperties())
            .build();
    }
    
    @Bean(name = "queryEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean queryEntityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("queryDataSource") DataSource dataSource) {
        
        return builder
            .dataSource(dataSource)
            .packages("com.example.query.entity")
            .persistenceUnit("query")
            .properties(jpaProperties())
            .build();
    }
    
    @Primary
    @Bean(name = "commandTransactionManager")
    public PlatformTransactionManager commandTransactionManager(
            @Qualifier("commandEntityManagerFactory") 
            EntityManagerFactory commandEntityManagerFactory) {
        return new JpaTransactionManager(commandEntityManagerFactory);
    }
    
    @Bean(name = "queryTransactionManager")
    public PlatformTransactionManager queryTransactionManager(
            @Qualifier("queryEntityManagerFactory") 
            EntityManagerFactory queryEntityManagerFactory) {
        return new JpaTransactionManager(queryEntityManagerFactory);
    }
    
    private Map<String, Object> jpaProperties() {
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
        properties.put("hibernate.show_sql", false);
        properties.put("hibernate.format_sql", true);
        return properties;
    }
}

Komut ve Sorgu Arayüzleri ​

java
// Komut arayüzü
public interface Command {
    String getCommandId();
    Instant getTimestamp();
}

// Sorgu arayüzü
public interface Query<T> {
    String getQueryId();
    Class<T> getResponseType();
}

// Komut Yöneticisi arayüzü
public interface CommandHandler<T extends Command> {
    void handle(T command);
}

// Sorgu Yöneticisi arayüzü
public interface QueryHandler<Q extends Query<R>, R> {
    R handle(Q query);
}

Komut Tarafı Uygulaması ​

Komut Modeli ​

java
// Sipariş Komutları
public class CreateOrderCommand implements Command {
    private final String commandId;
    private final Instant timestamp;
    private final String customerId;
    private final List<OrderItem> items;
    private final String shippingAddress;
    
    public CreateOrderCommand(String customerId, List<OrderItem> items, String shippingAddress) {
        this.commandId = UUID.randomUUID().toString();
        this.timestamp = Instant.now();
        this.customerId = customerId;
        this.items = items;
        this.shippingAddress = shippingAddress;
    }
    
    // Getter'lar
}

public class ConfirmOrderCommand implements Command {
    private final String commandId;
    private final Instant timestamp;
    private final String orderId;
    
    public ConfirmOrderCommand(String orderId) {
        this.commandId = UUID.randomUUID().toString();
        this.timestamp = Instant.now();
        this.orderId = orderId;
    }
    
    // Getter'lar
}

public class CancelOrderCommand implements Command {
    private final String commandId;
    private final Instant timestamp;
    private final String orderId;
    private final String reason;
    
    public CancelOrderCommand(String orderId, String reason) {
        this.commandId = UUID.randomUUID().toString();
        this.timestamp = Instant.now();
        this.orderId = orderId;
        this.reason = reason;
    }
    
    // Getter'lar
}

Komut Varlıkları (Yazma Modeli) ​

java
@Entity
@Table(name = "orders")
public class OrderEntity {
    
    @Id
    private String orderId;
    
    @Column(nullable = false)
    private String customerId;
    
    @Column(nullable = false)
    private BigDecimal totalAmount;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @Column(nullable = false)
    private String shippingAddress;
    
    @Column(nullable = false)
    private Instant createdAt;
    
    private Instant confirmedAt;
    private Instant cancelledAt;
    private String cancellationReason;
    
    @Version
    private Long version;
    
    // Yapıcılar, getter'lar, setter'lar
}

@Entity
@Table(name = "order_items")
public class OrderItemEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String orderId;
    
    @Column(nullable = false)
    private String productId;
    
    @Column(nullable = false)
    private String productName;
    
    @Column(nullable = false)
    private BigDecimal unitPrice;
    
    @Column(nullable = false)
    private Integer quantity;
    
    // Yapıcılar, getter'lar, setter'lar
}

Komut Yöneticileri (Command Handlers) ​

java
@Component
@Transactional("commandTransactionManager")
@Slf4j
public class OrderCommandHandler implements 
    CommandHandler<CreateOrderCommand>,
    CommandHandler<ConfirmOrderCommand>,
    CommandHandler<CancelOrderCommand> {
    
    private final OrderCommandRepository orderRepository;
    private final OrderItemCommandRepository orderItemRepository;
    private final ApplicationEventPublisher eventPublisher;
    private final MeterRegistry meterRegistry;
    
    public OrderCommandHandler(OrderCommandRepository orderRepository,
                             OrderItemCommandRepository orderItemRepository,
                             ApplicationEventPublisher eventPublisher,
                             MeterRegistry meterRegistry) {
        this.orderRepository = orderRepository;
        this.orderItemRepository = orderItemRepository;
        this.eventPublisher = eventPublisher;
        this.meterRegistry = meterRegistry;
    }
    
    @Override
    public void handle(CreateOrderCommand command) {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            // Komutu doğrula
            validateCreateOrderCommand(command);
            
            // Toplam tutarı hesapla
            BigDecimal totalAmount = command.getItems().stream()
                .map(item -> item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
            
            // Sipariş varlığını oluştur
            OrderEntity order = new OrderEntity();
            order.setOrderId(UUID.randomUUID().toString());
            order.setCustomerId(command.getCustomerId());
            order.setTotalAmount(totalAmount);
            order.setStatus(OrderStatus.PENDING);
            order.setShippingAddress(command.getShippingAddress());
            order.setCreatedAt(command.getTimestamp());
            
            orderRepository.save(order);
            
            // Sipariş kalemlerini oluştur
            List<OrderItemEntity> orderItems = command.getItems().stream()
                .map(item -> {
                    OrderItemEntity orderItem = new OrderItemEntity();
                    orderItem.setOrderId(order.getOrderId());
                    orderItem.setProductId(item.getProductId());
                    orderItem.setProductName(item.getProductName());
                    orderItem.setUnitPrice(item.getUnitPrice());
                    orderItem.setQuantity(item.getQuantity());
                    return orderItem;
                })
                .collect(Collectors.toList());
            
            orderItemRepository.saveAll(orderItems);
            
            // Alan olayını yayınla
            OrderCreatedEvent event = new OrderCreatedEvent(
                order.getOrderId(),
                command.getCustomerId(),
                totalAmount,
                command.getItems()
            );
            eventPublisher.publishEvent(event);
            
            meterRegistry.counter("command.order_created").increment();
            log.info("Sipariş başarıyla oluşturuldu: {}", order.getOrderId());
            
        } catch (Exception e) {
            meterRegistry.counter("command.order_creation_failed").increment();
            log.error("Sipariş oluşturulurken hata oluştu", e);
            throw new CommandHandlingException("Sipariş oluşturulurken hata oluştu", e);
        } finally {
            sample.stop(Timer.builder("command.create_order_duration")
                .register(meterRegistry));
        }
    }
    
    @Override
    public void handle(ConfirmOrderCommand command) {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            Optional<OrderEntity> optionalOrder = orderRepository.findById(command.getOrderId());
            
            if (optionalOrder.isEmpty()) {
                throw new OrderNotFoundException("Sipariş bulunamadı: " + command.getOrderId());
            }
            
            OrderEntity order = optionalOrder.get();
            
            if (order.getStatus() != OrderStatus.PENDING) {
                throw new InvalidOrderStateException(
                    "Sipariş mevcut durumda onaylanamaz: " + order.getStatus());
            }
            
            order.setStatus(OrderStatus.CONFIRMED);
            order.setConfirmedAt(command.getTimestamp());
            
            orderRepository.save(order);
            
            // Alan olayını yayınla
            OrderConfirmedEvent event = new OrderConfirmedEvent(
                order.getOrderId(),
                command.getTimestamp()
            );
            eventPublisher.publishEvent(event);
            
            meterRegistry.counter("command.order_confirmed").increment();
            log.info("Sipariş başarıyla onaylandı: {}", order.getOrderId());
            
        } catch (Exception e) {
            meterRegistry.counter("command.order_confirmation_failed").increment();
            log.error("Sipariş onaylanırken hata oluştu: {}", command.getOrderId(), e);
            throw new CommandHandlingException("Sipariş onaylanırken hata oluştu", e);
        } finally {
            sample.stop(Timer.builder("command.confirm_order_duration")
                .register(meterRegistry));
        }
    }
    
    @Override
    public void handle(CancelOrderCommand command) {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            Optional<OrderEntity> optionalOrder = orderRepository.findById(command.getOrderId());
            
            if (optionalOrder.isEmpty()) {
                throw new OrderNotFoundException("Sipariş bulunamadı: " + command.getOrderId());
            }
            
            OrderEntity order = optionalOrder.get();
            
            if (order.getStatus() == OrderStatus.CANCELLED || 
                order.getStatus() == OrderStatus.DELIVERED) {
                throw new InvalidOrderStateException(
                    "Sipariş mevcut durumda iptal edilemez: " + order.getStatus());
            }
            
            order.setStatus(OrderStatus.CANCELLED);
            order.setCancelledAt(command.getTimestamp());
            order.setCancellationReason(command.getReason());
            
            orderRepository.save(order);
            
            // Alan olayını yayınla
            OrderCancelledEvent event = new OrderCancelledEvent(
                order.getOrderId(),
                command.getReason(),
                command.getTimestamp()
            );
            eventPublisher.publishEvent(event);
            
            meterRegistry.counter("command.order_cancelled").increment();
            log.info("Sipariş başarıyla iptal edildi: {}", order.getOrderId());
            
        } catch (Exception e) {
            meterRegistry.counter("command.order_cancellation_failed").increment();
            log.error("Sipariş iptal edilirken hata oluştu: {}", command.getOrderId(), e);
            throw new CommandHandlingException("Sipariş iptal edilirken hata oluştu", e);
        } finally {
            sample.stop(Timer.builder("command.cancel_order_duration")
                .register(meterRegistry));
        }
    }
    
    private void validateCreateOrderCommand(CreateOrderCommand command) {
        if (command.getCustomerId() == null || command.getCustomerId().trim().isEmpty()) {
            throw new InvalidCommandException("Müşteri ID'si gereklidir");
        }
        
        if (command.getItems() == null || command.getItems().isEmpty()) {
            throw new InvalidCommandException("Sipariş kalemleri gereklidir");
        }
        
        if (command.getShippingAddress() == null || command.getShippingAddress().trim().isEmpty()) {
            throw new InvalidCommandException("Gönderim adresi gereklidir");
        }
        
        // Ek iş kuralları doğrulamaları
        for (OrderItem item : command.getItems()) {
            if (item.getQuantity() <= 0) {
                throw new InvalidCommandException("Kalem miktarı sıfırdan büyük olmalıdır");
            }
            if (item.getUnitPrice().compareTo(BigDecimal.ZERO) <= 0) {
                throw new InvalidCommandException("Kalem fiyatı sıfırdan büyük olmalıdır");
            }
        }
    }
}

Sorgu Tarafı Uygulaması ​

Sorgu Modeli ​

java
// Sipariş Sorguları
public class GetOrderByIdQuery implements Query<OrderDto> {
    private final String queryId;
    private final String orderId;
    
    public GetOrderByIdQuery(String orderId) {
        this.queryId = UUID.randomUUID().toString();
        this.orderId = orderId;
    }
    
    @Override
    public Class<OrderDto> getResponseType() {
        return OrderDto.class;
    }
    
    // Getter'lar
}

public class GetOrdersByCustomerQuery implements Query<List<OrderSummaryDto>> {
    private final String queryId;
    private final String customerId;
    private final int page;
    private final int size;
    
    public GetOrdersByCustomerQuery(String customerId, int page, int size) {
        this.queryId = UUID.randomUUID().toString();
        this.customerId = customerId;
        this.page = page;
        this.size = size;
    }
    
    @Override
    public Class<List<OrderSummaryDto>> getResponseType() {
        return (Class<List<OrderSummaryDto>>) (Class<?>) List.class;
    }
    
    // Getter'lar
}

public class GetOrderStatisticsQuery implements Query<OrderStatisticsDto> {
    private final String queryId;
    private final Instant startDate;
    private final Instant endDate;
    
    public GetOrderStatisticsQuery(Instant startDate, Instant endDate) {
        this.queryId = UUID.randomUUID().toString();
        this.startDate = startDate;
        this.endDate = endDate;
    }
    
    @Override
    public Class<OrderStatisticsDto> getResponseType() {
        return OrderStatisticsDto.class;
    }
    
    // Getter'lar
}

Sorgu Varlıkları (Okuma Modeli) ​

java
@Entity
@Table(name = "order_read_model")
public class OrderReadModelEntity {
    
    @Id
    private String orderId;
    
    @Column(nullable = false)
    private String customerId;
    
    @Column(nullable = false)
    private String customerName;
    
    @Column(nullable = false)
    private String customerEmail;
    
    @Column(nullable = false)
    private BigDecimal totalAmount;
    
    @Column(nullable = false)
    private String status;
    
    @Column(nullable = false)
    private String shippingAddress;
    
    @Column(nullable = false)
    private Instant createdAt;
    
    private Instant confirmedAt;
    private Instant cancelledAt;
    private String cancellationReason;
    
    @Column(nullable = false)
    private Integer itemCount;
    
    @Column(nullable = false)
    private Instant lastUpdated;
    
    // Yapıcılar, getter'lar, setter'lar
}

@Entity
@Table(name = "order_item_read_model")
public class OrderItemReadModelEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String orderId;
    
    @Column(nullable = false)
    private String productId;
    
    @Column(nullable = false)
    private String productName;
    
    @Column(nullable = false)
    private String productCategory;
    
    @Column(nullable = false)
    private BigDecimal unitPrice;
    
    @Column(nullable = false)
    private Integer quantity;
    
    @Column(nullable = false)
    private BigDecimal totalPrice;
    
    // Yapıcılar, getter'lar, setter'lar
}

Sorgu Yöneticileri (Query Handlers) ​

java
@Component
@Transactional(value = "queryTransactionManager", readOnly = true)
@Slf4j
public class OrderQueryHandler implements 
    QueryHandler<GetOrderByIdQuery, OrderDto>,
    QueryHandler<GetOrdersByCustomerQuery, List<OrderSummaryDto>>,
    QueryHandler<GetOrderStatisticsQuery, OrderStatisticsDto> {
    
    private final OrderQueryRepository orderQueryRepository;
    private final OrderItemQueryRepository orderItemQueryRepository;
    private final MeterRegistry meterRegistry;
    
    public OrderQueryHandler(OrderQueryRepository orderQueryRepository,
                           OrderItemQueryRepository orderItemQueryRepository,
                           MeterRegistry meterRegistry) {
        this.orderQueryRepository = orderQueryRepository;
        this.orderItemQueryRepository = orderItemQueryRepository;
        this.meterRegistry = meterRegistry;
    }
    
    @Override
    public OrderDto handle(GetOrderByIdQuery query) {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            Optional<OrderReadModelEntity> optionalOrder = 
                orderQueryRepository.findById(query.getOrderId());
            
            if (optionalOrder.isEmpty()) {
                throw new OrderNotFoundException("Sipariş bulunamadı: " + query.getOrderId());
            }
            
            OrderReadModelEntity order = optionalOrder.get();
            List<OrderItemReadModelEntity> items = 
                orderItemQueryRepository.findByOrderId(query.getOrderId());
            
            OrderDto dto = mapToOrderDto(order, items);
            
            meterRegistry.counter("query.get_order_by_id").increment();
            log.debug("Sipariş alındı: {}", query.getOrderId());
            
            return dto;
            
        } finally {
            sample.stop(Timer.builder("query.get_order_by_id_duration")
                .register(meterRegistry));
        }
    }
    
    @Override
    public List<OrderSummaryDto> handle(GetOrdersByCustomerQuery query) {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            Pageable pageable = PageRequest.of(query.getPage(), query.getSize());
            Page<OrderReadModelEntity> orders = 
                orderQueryRepository.findByCustomerId(query.getCustomerId(), pageable);
            
            List<OrderSummaryDto> summaries = orders.getContent().stream()
                .map(this::mapToOrderSummaryDto)
                .collect(Collectors.toList());
            
            meterRegistry.counter("query.get_orders_by_customer").increment();
            log.debug("Müşteri için {} sipariş alındı: {}", 
                summaries.size(), query.getCustomerId());
            
            return summaries;
            
        } finally {
            sample.stop(Timer.builder("query.get_orders_by_customer_duration")
                .register(meterRegistry));
        }
    }
    
    @Override
    public OrderStatisticsDto handle(GetOrderStatisticsQuery query) {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            OrderStatisticsProjection stats = orderQueryRepository
                .getOrderStatistics(query.getStartDate(), query.getEndDate());
            
            OrderStatisticsDto dto = new OrderStatisticsDto();
            dto.setTotalOrders(stats.getTotalOrders());
            dto.setTotalRevenue(stats.getTotalRevenue());
            dto.setAverageOrderValue(stats.getAverageOrderValue());
            dto.setConfirmedOrders(stats.getConfirmedOrders());
            dto.setCancelledOrders(stats.getCancelledOrders());
            dto.setPendingOrders(stats.getPendingOrders());
            
            meterRegistry.counter("query.get_order_statistics").increment();
            log.debug("Sipariş istatistikleri alındı, dönem: {} - {}", 
                query.getStartDate(), query.getEndDate());
            
            return dto;
            
        } finally {
            sample.stop(Timer.builder("query.get_order_statistics_duration")
                .register(meterRegistry));
        }
    }
    
    private OrderDto mapToOrderDto(OrderReadModelEntity order, 
                                  List<OrderItemReadModelEntity> items) {
        OrderDto dto = new OrderDto();
        dto.setOrderId(order.getOrderId());
        dto.setCustomerId(order.getCustomerId());
        dto.setCustomerName(order.getCustomerName());
        dto.setCustomerEmail(order.getCustomerEmail());
        dto.setTotalAmount(order.getTotalAmount());
        dto.setStatus(order.getStatus());
        dto.setShippingAddress(order.getShippingAddress());
        dto.setCreatedAt(order.getCreatedAt());
        dto.setConfirmedAt(order.getConfirmedAt());
        dto.setCancelledAt(order.getCancelledAt());
        dto.setCancellationReason(order.getCancellationReason());
        
        List<OrderItemDto> itemDtos = items.stream()
            .map(this::mapToOrderItemDto)
            .collect(Collectors.toList());
        dto.setItems(itemDtos);
        
        return dto;
    }
    
    private OrderItemDto mapToOrderItemDto(OrderItemReadModelEntity item) {
        OrderItemDto dto = new OrderItemDto();
        dto.setProductId(item.getProductId());
        dto.setProductName(item.getProductName());
        dto.setProductCategory(item.getProductCategory());
        dto.setUnitPrice(item.getUnitPrice());
        dto.setQuantity(item.getQuantity());
        dto.setTotalPrice(item.getTotalPrice());
        return dto;
    }
    
    private OrderSummaryDto mapToOrderSummaryDto(OrderReadModelEntity order) {
        OrderSummaryDto dto = new OrderSummaryDto();
        dto.setOrderId(order.getOrderId());
        dto.setTotalAmount(order.getTotalAmount());
        dto.setStatus(order.getStatus());
        dto.setCreatedAt(order.getCreatedAt());
        dto.setItemCount(order.getItemCount());
        return dto;
    }
}

Olay Tabanlı CQRS ​

Okuma Modeli Güncellemeleri için Olay Yöneticileri ​

java
@Component
@Slf4j
public class OrderReadModelUpdater {
    
    private final OrderQueryRepository orderQueryRepository;
    private final OrderItemQueryRepository orderItemQueryRepository;
    private final CustomerService customerService;
    private final ProductService productService;
    private final MeterRegistry meterRegistry;
    
    public OrderReadModelUpdater(OrderQueryRepository orderQueryRepository,
                               OrderItemQueryRepository orderItemQueryRepository,
                               CustomerService customerService,
                               ProductService productService,
                               MeterRegistry meterRegistry) {
        this.orderQueryRepository = orderQueryRepository;
        this.orderItemQueryRepository = orderItemQueryRepository;
        this.customerService = customerService;
        this.productService = productService;
        this.meterRegistry = meterRegistry;
    }
    
    @EventListener
    @Async
    @Transactional("queryTransactionManager")
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            // Müşteri bilgilerini al
            CustomerDto customer = customerService.getCustomer(event.getCustomerId());
            
            // Sipariş okuma modelini oluştur
            OrderReadModelEntity orderReadModel = new OrderReadModelEntity();
            orderReadModel.setOrderId(event.getAggregateId());
            orderReadModel.setCustomerId(event.getCustomerId());
            orderReadModel.setCustomerName(customer.getName());
            orderReadModel.setCustomerEmail(customer.getEmail());
            orderReadModel.setTotalAmount(event.getTotalAmount());
            orderReadModel.setStatus("PENDING");
            orderReadModel.setShippingAddress(event.getShippingAddress());
            orderReadModel.setCreatedAt(event.getTimestamp());
            orderReadModel.setItemCount(event.getItems().size());
            orderReadModel.setLastUpdated(Instant.now());
            
            orderQueryRepository.save(orderReadModel);
            
            // Sipariş kalemleri okuma modellerini oluştur
            List<OrderItemReadModelEntity> itemReadModels = event.getItems().stream()
                .map(item -> {
                    ProductDto product = productService.getProduct(item.getProductId());
                    
                    OrderItemReadModelEntity itemReadModel = new OrderItemReadModelEntity();
                    itemReadModel.setOrderId(event.getAggregateId());
                    itemReadModel.setProductId(item.getProductId());
                    itemReadModel.setProductName(item.getProductName());
                    itemReadModel.setProductCategory(product.getCategory());
                    itemReadModel.setUnitPrice(item.getUnitPrice());
                    itemReadModel.setQuantity(item.getQuantity());
                    itemReadModel.setTotalPrice(
                        item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
                    
                    return itemReadModel;
                })
                .collect(Collectors.toList());
            
            orderItemQueryRepository.saveAll(itemReadModels);
            
            meterRegistry.counter("read_model.order_created").increment();
            log.info("Okuma modeli güncellendi, sipariş oluşturuldu: {}", event.getAggregateId());
            
        } catch (Exception e) {
            meterRegistry.counter("read_model.update_errors",
                "event_type", "OrderCreatedEvent").increment();
            log.error("OrderCreatedEvent için okuma modeli güncellenirken hata oluştu", e);
        }
    }
    
    @EventListener
    @Async
    @Transactional("queryTransactionManager")
    public void handleOrderConfirmed(OrderConfirmedEvent event) {
        try {
            Optional<OrderReadModelEntity> optionalOrder = 
                orderQueryRepository.findById(event.getAggregateId());
            
            if (optionalOrder.isPresent()) {
                OrderReadModelEntity order = optionalOrder.get();
                order.setStatus("CONFIRMED");
                order.setConfirmedAt(event.getConfirmedAt());
                order.setLastUpdated(Instant.now());
                
                orderQueryRepository.save(order);
                
                meterRegistry.counter("read_model.order_confirmed").increment();
                log.info("Okuma modeli güncellendi, sipariş onaylandı: {}", event.getAggregateId());
            } else {
                log.warn("Onay için okuma modelinde sipariş bulunamadı: {}", event.getAggregateId());
            }
            
        } catch (Exception e) {
            meterRegistry.counter("read_model.update_errors",
                "event_type", "OrderConfirmedEvent").increment();
            log.error("OrderConfirmedEvent için okuma modeli güncellenirken hata oluştu", e);
        }
    }
    
    @EventListener
    @Async
    @Transactional("queryTransactionManager")
    public void handleOrderCancelled(OrderCancelledEvent event) {
        try {
            Optional<OrderReadModelEntity> optionalOrder = 
                orderQueryRepository.findById(event.getAggregateId());
            
            if (optionalOrder.isPresent()) {
                OrderReadModelEntity order = optionalOrder.get();
                order.setStatus("CANCELLED");
                order.setCancelledAt(event.getTimestamp());
                order.setCancellationReason(event.getReason());
                order.setLastUpdated(Instant.now());
                
                orderQueryRepository.save(order);
                
                meterRegistry.counter("read_model.order_cancelled").increment();
                log.info("Okuma modeli güncellendi, sipariş iptal edildi: {}", event.getAggregateId());
            } else {
                log.warn("İptal için okuma modelinde sipariş bulunamadı: {}", event.getAggregateId());
            }
            
        } catch (Exception e) {
            meterRegistry.counter("read_model.update_errors",
                "event_type", "OrderCancelledEvent").increment();
            log.error("OrderCancelledEvent için okuma modeli güncellenirken hata oluştu", e);
        }
    }
}

Komut ve Sorgu Bus ​

java
@Component
public class CommandBus {
    
    private final Map<Class<? extends Command>, CommandHandler<? extends Command>> handlers = new HashMap<>();
    private final MeterRegistry meterRegistry;
    
    public CommandBus(List<CommandHandler<? extends Command>> commandHandlers,
                     MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        registerHandlers(commandHandlers);
    }
    
    @SuppressWarnings("unchecked")
    public <T extends Command> void dispatch(T command) {
        CommandHandler<T> handler = (CommandHandler<T>) handlers.get(command.getClass());
        
        if (handler == null) {
            throw new CommandHandlerNotFoundException(
                "Komut için hiçbir yöneticisi bulunamadı: " + command.getClass().getSimpleName());
        }
        
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            log.info("Komut gönderiliyor: {} ID'si ile", 
                command.getClass().getSimpleName(), command.getCommandId());
            
            handler.handle(command);
            
            meterRegistry.counter("command_bus.dispatched",
                "command_type", command.getClass().getSimpleName()).increment();
            
        } catch (Exception e) {
            meterRegistry.counter("command_bus.dispatch_errors",
                "command_type", command.getClass().getSimpleName()).increment();
            log.error("Komut gönderilirken hata oluştu: {}", command.getClass().getSimpleName(), e);
            throw e;
        } finally {
            sample.stop(Timer.builder("command_bus.dispatch_duration")
                .tag("command_type", command.getClass().getSimpleName())
                .register(meterRegistry));
        }
    }
    
    private void registerHandlers(List<CommandHandler<? extends Command>> commandHandlers) {
        for (CommandHandler<? extends Command> handler : commandHandlers) {
            Type[] genericInterfaces = handler.getClass().getGenericInterfaces();
            for (Type genericInterface : genericInterfaces) {
                if (genericInterface instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
                    if (CommandHandler.class.equals(parameterizedType.getRawType())) {
                        Type commandType = parameterizedType.getActualTypeArguments()[0];
                        handlers.put((Class<? extends Command>) commandType, handler);
                        break;
                    }
                }
            }
        }
    }
}

@Component
public class QueryBus {
    
    private final Map<Class<? extends Query<?>>, QueryHandler<? extends Query<?>, ?>> handlers = new HashMap<>();
    private final MeterRegistry meterRegistry;
    
    public QueryBus(List<QueryHandler<? extends Query<?>, ?>> queryHandlers,
                   MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        registerHandlers(queryHandlers);
    }
    
    @SuppressWarnings("unchecked")
    public <Q extends Query<R>, R> R dispatch(Q query) {
        QueryHandler<Q, R> handler = (QueryHandler<Q, R>) handlers.get(query.getClass());
        
        if (handler == null) {
            throw new QueryHandlerNotFoundException(
                "Sorgu için hiçbir yöneticisi bulunamadı: " + query.getClass().getSimpleName());
        }
        
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            log.debug("Sorgu gönderiliyor: {} ID'si ile", 
                query.getClass().getSimpleName(), query.getQueryId());
            
            R result = handler.handle(query);
            
            meterRegistry.counter("query_bus.dispatched",
                "query_type", query.getClass().getSimpleName()).increment();
            
            return result;
            
        } catch (Exception e) {
            meterRegistry.counter("query_bus.dispatch_errors",
                "query_type", query.getClass().getSimpleName()).increment();
            log.error("Sorgu gönderilirken hata oluştu: {}", query.getClass().getSimpleName(), e);
            throw e;
        } finally {
            sample.stop(Timer.builder("query_bus.dispatch_duration")
                .tag("query_type", query.getClass().getSimpleName())
                .register(meterRegistry));
        }
    }
    
    private void registerHandlers(List<QueryHandler<? extends Query<?>, ?>> queryHandlers) {
        for (QueryHandler<? extends Query<?>, ?> handler : queryHandlers) {
            Type[] genericInterfaces = handler.getClass().getGenericInterfaces();
            for (Type genericInterface : genericInterfaces) {
                if (genericInterface instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
                    if (QueryHandler.class.equals(parameterizedType.getRawType())) {
                        Type queryType = parameterizedType.getActualTypeArguments()[0];
                        handlers.put((Class<? extends Query<?>>) queryType, handler);
                        break;
                    }
                }
            }
        }
    }
}

CQRS ile REST Controller ​

java
@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {
    
    private final CommandBus commandBus;
    private final QueryBus queryBus;
    
    public OrderController(CommandBus commandBus, QueryBus queryBus) {
        this.commandBus = commandBus;
        this.queryBus = queryBus;
    }
    
    @PostMapping
    public ResponseEntity<Void> createOrder(@RequestBody @Valid CreateOrderRequest request) {
        CreateOrderCommand command = new CreateOrderCommand(
            request.getCustomerId(),
            request.getItems(),
            request.getShippingAddress()
        );
        
        commandBus.dispatch(command);
        
        return ResponseEntity.status(HttpStatus.ACCEPTED).build();
    }
    
    @PutMapping("/{orderId}/confirm")
    public ResponseEntity<Void> confirmOrder(@PathVariable String orderId) {
        ConfirmOrderCommand command = new ConfirmOrderCommand(orderId);
        commandBus.dispatch(command);
        return ResponseEntity.ok().build();
    }
    
    @PutMapping("/{orderId}/cancel")
    public ResponseEntity<Void> cancelOrder(@PathVariable String orderId,
                                          @RequestBody @Valid CancelOrderRequest request) {
        CancelOrderCommand command = new CancelOrderCommand(orderId, request.getReason());
        commandBus.dispatch(command);
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/{orderId}")
    public ResponseEntity<OrderDto> getOrder(@PathVariable String orderId) {
        GetOrderByIdQuery query = new GetOrderByIdQuery(orderId);
        OrderDto order = queryBus.dispatch(query);
        return ResponseEntity.ok(order);
    }
    
    @GetMapping("/customer/{customerId}")
    public ResponseEntity<List<OrderSummaryDto>> getOrdersByCustomer(
            @PathVariable String customerId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        GetOrdersByCustomerQuery query = new GetOrdersByCustomerQuery(customerId, page, size);
        List<OrderSummaryDto> orders = queryBus.dispatch(query);
        return ResponseEntity.ok(orders);
    }
    
    @GetMapping("/statistics")
    public ResponseEntity<OrderStatisticsDto> getOrderStatistics(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant endDate) {
        
        GetOrderStatisticsQuery query = new GetOrderStatisticsQuery(startDate, endDate);
        OrderStatisticsDto statistics = queryBus.dispatch(query);
        return ResponseEntity.ok(statistics);
    }
}

Bu kapsamlı CQRS uygulaması, Spring Boot ekosisteminde yazma ve okuma işlemlerinin ayrı optimizasyonuna olanak tanıyarak yüksek performans ve ölçeklenebilirlik sağlamaktadır.

Eren Demir tarafından oluşturulmuştur.