Domain-Driven Design, karmaşık yazılım sistemlerini daha iyi anlamak ve yönetmek için kullanılan güçlü bir yaklaşımdır. Bu rehber, DDD'nin temel prensiplerini ve uygulamalarını bir e-ticaret domaini üzerinden detaylı olarak ele almaktadır.
📑 İçindekiler
-
Domain-Driven Design (DDD) Rehberi
- İçindekiler
- Giriş
- DDD'nin Temel Prensipleri
- Sık Sorulan Sorular ve Best Practices
- DDD ve Clean Architecture Entegrasyonu
- Kaynaklar
🎯 Giriş
Yazılım geliştirmede "domain," yani alan, bir sistemin çalıştığı iş bağlamını ve kurallarını ifade eder. Örneğin, bir e-ticaret platformu geliştirdiğimizi düşünelim. Bu platformda sipariş yönetimi, kullanıcı hesapları, ödeme işlemleri ve ürün envanteri gibi çeşitli domainler bulunur. Her domain, kendine özgü iş kurallarına ve veri yapısına sahiptir.
Bu makalede, DDD'yi daha iyi anlamak için bir e-ticaret platformu domainini kullanacağız. Örneğin, sipariş yönetimi domainini ele alarak, DDD'nin temel prensiplerini ve bileşenlerini bu örnek üzerinden açıklayacağız.
Domain-Driven Design (DDD), karmaşık yazılım sistemlerini daha iyi anlamak ve yönetmek için kullanılan bir yaklaşımdır. Eric Evans tarafından tanıtılan bu yaklaşım, yazılım geliştirme sürecinde iş kurallarına ve domain modeline odaklanarak daha sürdürülebilir ve esnek sistemler oluşturmayı amaçlar.
🏗️ DDD'nin Temel Prensipleri
1. Ubiquitous Language (Ortak Dil)
Ortak dil, geliştiriciler, iş analistleri, domain uzmanları ve diğer paydaşlar arasında iletişimi güçlendiren ve yanlış anlamaları önleyen ortak bir terminolojidir. Bu dil, tüm ekip üyeleri tarafından anlaşılır ve sistemin her yerinde tutarlı bir şekilde kullanılır.
Ortak Dil Nasıl Oluşturulur?
- Tartışmalar ve Toplantılar: Domain uzmanları, geliştiriciler ve iş birimleri düzenli olarak bir araya gelerek sistemde kullanılacak terimleri belirler. Örneğin, "Sipariş" (Order), "Sipariş Kalemi" (Order Item), "Teslimat Adresi" (Delivery Address) gibi kavramlar üzerinde anlaşmaya varılır.
- Dokümantasyon ve Tanımlamalar: Belirlenen ortak dil, sistem dokümantasyonunda ve modelleme araçlarında açıkça tanımlanır.
- Kodda Kullanım: Ortak dil yalnızca sözlü olarak değil, kod içinde de kullanılır. Değişken adları, metot isimleri ve sınıf isimleri bu terminolojiye uygun şekilde yazılır. Örneğin:
data class Order(
val orderId: String,
val items: List<OrderItem>,
val deliveryAddress: Address,
val status: OrderStatus,
val customerId: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
fun addItem(item: OrderItem): Order {
return copy(
items = items + item,
updatedAt = LocalDateTime.now()
)
}
}
- Geri Bildirim ve Sürekli İyileştirme: Ortak dil zaman içinde gelişebilir. Yeni iş kuralları eklendikçe veya gereksiz terimler fark edildikçe, ekip dili güncelleyebilir.
2. Bounded Context (Sınırlandırılmış Bağlam)
Sistemin farklı bölümlerini net sınırlarla ayırarak her bir bağlamın kendi iç tutarlılığını korumasını sağlar. E-ticaret örneğinde, "Sipariş Yönetimi" (Order Management) bağlamı ile "Ödeme" (Payment) bağlamı birbirinden bağımsız ele alınmalıdır.
Bounded Context Nasıl Oluşturulur?
- İş Gereksinimleri ve Bağlam Analizi: Her bağlamın işlevi net bir şekilde tanımlanır. Örneğin, "Sipariş Yönetimi" bağlamı yalnızca siparişlerin oluşturulması ve takibini içerirken, "Ödeme" bağlamı ödeme işlemleriyle ilgilenir.
- Bağımsız Modüller Tasarlama: Her bağlam kendi veri modeline ve iş kurallarına sahip olmalıdır. Sipariş yönetimi bağlamında "Sipariş" (Order) nesnesi bulunurken, ödeme bağlamında "Ödeme" (Payment) nesnesi bulunur.
// Sipariş Yönetimi Bağlamı
data class Order(
val orderId: String,
val items: List<OrderItem>,
val status: OrderStatus,
val customerId: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
fun addItem(item: OrderItem): Order {
return copy(
items = items + item,
updatedAt = LocalDateTime.now()
)
}
fun calculateTotal(): Money {
return items.sumOf { it.price * it.quantity }
}
}
// Ödeme Bağlamı
data class Payment(
val paymentId: String,
val amount: Money,
val status: PaymentStatus,
val orderId: String,
val paymentMethod: PaymentMethod,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
fun processPayment(): Payment {
return copy(
status = PaymentStatus.COMPLETED,
updatedAt = LocalDateTime.now()
)
}
fun refund(): Payment {
return copy(
status = PaymentStatus.REFUNDED,
updatedAt = LocalDateTime.now()
)
}
}
- Bağlamlar Arasında İletişim: Bağlamlar arasındaki veri akışı tanımlanmalıdır. Örneğin, ödeme tamamlandığında sipariş bağlamına "Ödeme Tamamlandı" (Payment Completed) olayı iletilir.
// Domain Events
sealed interface DomainEvent {
val eventId: String
val timestamp: LocalDateTime
}
data class PaymentCompletedEvent(
override val eventId: String = UUID.randomUUID().toString(),
override val timestamp: LocalDateTime = LocalDateTime.now(),
val paymentId: String,
val orderId: String,
val amount: Money
) : DomainEvent
// Event Publisher Interface
interface EventPublisher {
fun publish(event: DomainEvent)
}
// Ödeme Bağlamı - Event Yayınlama
data class Payment(
val paymentId: String,
val amount: Money,
val status: PaymentStatus,
val orderId: String,
val paymentMethod: PaymentMethod,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
fun processPayment(eventPublisher: EventPublisher): Payment {
val updatedPayment = copy(
status = PaymentStatus.COMPLETED,
updatedAt = LocalDateTime.now()
)
eventPublisher.publish(PaymentCompletedEvent(
paymentId = paymentId,
orderId = orderId,
amount = amount
))
return updatedPayment
}
}
// Sipariş Bağlamı - Event Dinleme
data class Order(
val orderId: String,
val items: List<OrderItem>,
val status: OrderStatus,
val customerId: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
fun handlePaymentCompleted(event: PaymentCompletedEvent): Order {
require(event.orderId == orderId) { "Event orderId does not match current order" }
return copy(
status = OrderStatus.PAID,
updatedAt = event.timestamp
)
}
}
3. Entities (Varlıklar) ve Value Objects (Değer Nesneleri)
Kalıcı kimliği olan nesneler (Entities) ile kimliği olmayan değişmez nesneleri (Value Objects) birbirinden ayırır. Örneğin, "Sipariş" bir entity iken, "Adres" bir value object olarak düşünülebilir.
Entities ve Value Objects Nasıl Ayrılır?
- Kimlik Bazlı mı, Değer Bazlı mı? Eğer bir nesne eşsiz bir kimliğe sahipse ve zamanla değişiyorsa, bu bir entity'dir. Örneğin:
data class Order(
val orderId: String, // Entity'nin kimliği
val items: List<OrderItem>,
val status: OrderStatus,
val customerId: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
// Entity'ler immutable olmalıdır, bu yüzden state değişiklikleri yeni nesne döndürür
fun addItem(item: OrderItem): Order {
return copy(
items = items + item,
updatedAt = LocalDateTime.now()
)
}
}
Eğer nesne kimlik taşımıyorsa ve değişmezse, bir value object olarak modellenmelidir:
data class Address(val street: String, val city: String) // Value Object
-
İş Kurallarına Uygunluk: Eğer bir nesne bağımsız bir iş mantığına sahipse, genellikle entity'dir. Eğer yalnızca veri taşıyorsa ve değiştirilmeden tekrar kullanılabiliyorsa, value object olmalıdır. Value object'ler genellikle:
- İmmutable'dır (değişmez)
- Kimlik taşımaz
- İş mantığı içerebilir (örn: Money sınıfındaki matematiksel operasyonlar)
- Primitive tipleri type-safe bir şekilde sarmalayabilir (value class kullanarak)
4. Domain Events (Domain Olayları)
Sistem içinde gerçekleşen önemli olayları temsil eder ve diğer bileşenlerin bunlara tepki vermesine olanak tanır. "Sipariş Oluşturuldu" (Order Created) gibi bir olay, stok yönetimi veya bildirim sistemleri tarafından dinlenebilir.
Domain Event Nasıl Kullanılır?
- Olayı Tanımlama:
// Domain Events
sealed interface DomainEvent {
val eventId: String
val timestamp: LocalDateTime
}
data class OrderCreatedEvent(
override val eventId: String = UUID.randomUUID().toString(),
override val timestamp: LocalDateTime = LocalDateTime.now(),
val orderId: String,
val customerId: String,
val totalAmount: Money
) : DomainEvent
data class OrderProcessingStartedEvent(
override val eventId: String = UUID.randomUUID().toString(),
override val timestamp: LocalDateTime = LocalDateTime.now(),
val orderId: String,
val paymentId: String
) : DomainEvent
data class OrderCompletedEvent(
override val eventId: String = UUID.randomUUID().toString(),
override val timestamp: LocalDateTime = LocalDateTime.now(),
val orderId: String,
val deliveryDate: LocalDateTime
) : DomainEvent
- Event Yayınlama ve Dinleme:
// Event Publisher Interface
interface EventPublisher {
fun publish(event: DomainEvent)
}
// Event Handler Interface
interface EventHandler<T : DomainEvent> {
fun handle(event: T)
}
// Event Handler Implementasyonları
class StockUpdateHandler : EventHandler<OrderCreatedEvent> {
override fun handle(event: OrderCreatedEvent) {
// Stok güncelleme işlemleri
}
}
class NotificationHandler : EventHandler<OrderProcessingStartedEvent> {
override fun handle(event: OrderProcessingStartedEvent) {
// Müşteriye bildirim gönderme işlemleri
}
}
class AnalyticsHandler : EventHandler<OrderCompletedEvent> {
override fun handle(event: OrderCompletedEvent) {
// Analitik verilerini güncelleme işlemleri
}
}
// Event Bus
class EventBus(
private val handlers: Map<Class<out DomainEvent>, List<EventHandler<out DomainEvent>>>
) : EventPublisher {
@Suppress("UNCHECKED_CAST")
override fun publish(event: DomainEvent) {
val eventHandlers = handlers[event::class.java] as? List<EventHandler<DomainEvent>>
?: return
eventHandlers.forEach { handler ->
(handler as? EventHandler<DomainEvent>)?.handle(event)
}
}
}
-
Event Kullanım Kuralları:
- Domain Event'ler immutable olmalıdır
- Event'ler geçmiş zaman kullanılarak isimlendirilmelidir (OrderCreated, PaymentCompleted)
- Event'ler domain kavramlarını içermelidir
- Event'ler iş kurallarını yansıtmalıdır
- Event'ler atomik olmalıdır (tek bir işlemi temsil etmelidir)
- Event'ler tutarlı olmalıdır (aynı event farklı yerlerde farklı şekilde yorumlanmamalıdır)
5. Aggregates (Kümeler)
İş mantığını gruplayarak veri tutarlılığını sağlayan bileşenlerdir. Sipariş sistemi içinde "Sipariş" (Order) bir aggregate olabilir ve "Sipariş Kalemi" (Order Item) ile sıkı bir ilişkiye sahiptir.
Aggregates Nasıl Tasarlanır?
- Tutarlılık Sınırlarını Belirleme: Hangi nesnelerin birlikte yönetileceğini belirleyin. Örneğin, "Sipariş" (Order) ve "Sipariş Kalemi" (Order Item) aynı aggregate içinde olabilir.
// Aggregate Root: Order
data class Order(
val orderId: String,
private val items: MutableList<OrderItem>,
val status: OrderStatus,
val customerId: String,
val deliveryAddress: Address,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
) {
// Aggregate Root, içindeki nesnelerin tutarlılığını sağlar
fun addItem(item: OrderItem): Order {
require(status == OrderStatus.DRAFT) { "Can only add items to draft orders" }
require(item.quantity > 0) { "Item quantity must be greater than 0" }
return copy(
items = (items + item).toMutableList(),
updatedAt = LocalDateTime.now()
)
}
fun removeItem(productId: String): Order {
require(status == OrderStatus.DRAFT) { "Can only remove items from draft orders" }
return copy(
items = items.filter { it.productId != productId }.toMutableList(),
updatedAt = LocalDateTime.now()
)
}
fun updateItemQuantity(productId: String, newQuantity: Int): Order {
require(status == OrderStatus.DRAFT) { "Can only update items in draft orders" }
require(newQuantity > 0) { "Quantity must be greater than 0" }
return copy(
items = items.map {
if (it.productId == productId) it.copy(quantity = newQuantity)
else it
}.toMutableList(),
updatedAt = LocalDateTime.now()
)
}
fun calculateTotal(): Money {
return items.sumOf { it.price * it.quantity }
}
fun submit(): Order {
require(status == OrderStatus.DRAFT) { "Can only submit draft orders" }
require(items.isNotEmpty()) { "Cannot submit empty orders" }
return copy(
status = OrderStatus.SUBMITTED,
updatedAt = LocalDateTime.now()
)
}
}
// Aggregate içindeki Entity: OrderItem
data class OrderItem(
val productId: String,
val name: String,
val price: Money,
val quantity: Int
)
// Aggregate içindeki Value Object: Address
data class Address(
val street: String,
val city: String,
val country: String,
val postalCode: String
)
- Tekil Kök Varlık (Aggregate Root) Kullanma: Tüm işlemler bir kök varlık üzerinden gerçekleştirilmelidir. Örneğin, yeni bir sipariş kalemi eklenirken doğrudan "Order" nesnesine eklenmelidir.
// Aggregate Root üzerinden işlemler
val order = Order(
orderId = "ORDER-001",
items = mutableListOf(),
status = OrderStatus.DRAFT,
customerId = "CUST-001",
deliveryAddress = Address(
street = "123 Main St",
city = "Istanbul",
country = "Turkey",
postalCode = "34000"
)
)
// Aggregate Root üzerinden item ekleme
val updatedOrder = order.addItem(
OrderItem(
productId = "PROD-001",
name = "Laptop",
price = Money(BigDecimal("999.99")),
quantity = 1
)
)
// Aggregate Root üzerinden sipariş gönderme
val submittedOrder = updatedOrder.submit()
-
Tutarlılık Kuralları:
- Aggregate Root (Order) içindeki tüm nesnelerin tutarlılığından sorumludur
- Aggregate dışındaki nesneler sadece Aggregate Root'un referansını tutabilir
- Aggregate içindeki nesnelere doğrudan erişim yoktur, her şey Aggregate Root üzerinden yapılır
- Aggregate içindeki işlemler atomik olmalıdır (ya hepsi başarılı olur ya da hiçbiri olmaz)
6. Repositories (Depolar)
Verilerin saklanması ve erişilmesi işlemlerini yöneten bileşenlerdir. Siparişleri yönetmek için bir "Sipariş Repository" (Order Repository) kullanılabilir.
Repository Nasıl Kullanılır?
- Repository Interface'i:
interface OrderRepository {
fun save(order: Order): Order
fun findById(orderId: String): Order?
fun findByCustomerId(customerId: String): List<Order>
fun findPendingOrders(): List<Order>
fun findOrdersByDateRange(startDate: LocalDateTime, endDate: LocalDateTime): List<Order>
fun findOrdersByStatus(status: OrderStatus): List<Order>
}
- Repository Implementasyonu:
// InMemory Repository implementasyonu
class InMemoryOrderRepository : OrderRepository {
private val orders = mutableMapOf<String, Order>()
private val customerOrders = mutableMapOf<String, MutableSet<String>>()
private val statusOrders = mutableMapOf<OrderStatus, MutableSet<String>>()
override fun save(order: Order): Order {
orders[order.orderId] = order
customerOrders.getOrPut(order.customerId) { mutableSetOf() }.add(order.orderId)
statusOrders.getOrPut(order.status) { mutableSetOf() }.add(order.orderId)
return order
}
override fun findById(orderId: String): Order? {
return orders[orderId]
}
override fun findByCustomerId(customerId: String): List<Order> {
return customerOrders[customerId]?.mapNotNull { orderId -> orders[orderId] } ?: emptyList()
}
override fun findPendingOrders(): List<Order> {
return statusOrders[OrderStatus.PENDING]?.mapNotNull { orderId -> orders[orderId] } ?: emptyList()
}
override fun findOrdersByDateRange(startDate: LocalDateTime, endDate: LocalDateTime): List<Order> {
return orders.values.filter { order ->
order.createdAt in startDate..endDate
}
}
override fun findOrdersByStatus(status: OrderStatus): List<Order> {
return statusOrders[status]?.mapNotNull { orderId -> orders[orderId] } ?: emptyList()
}
// Repository'nin iç durumunu sıfırlama (test için)
fun clear() {
orders.clear()
customerOrders.clear()
statusOrders.clear()
}
}
// Repository kullanım örneği
class OrderService(private val orderRepository: OrderRepository) {
fun createOrder(customerId: String, items: List<OrderItem>, address: Address): Order {
val order = Order(
orderId = UUID.randomUUID().toString(),
items = items,
status = OrderStatus.DRAFT,
customerId = customerId,
deliveryAddress = address
)
return orderRepository.save(order)
}
fun getCustomerOrders(customerId: String): List<Order> {
return orderRepository.findByCustomerId(customerId)
}
fun getPendingOrders(): List<Order> {
return orderRepository.findPendingOrders()
}
}
-
Repository Kullanım Kuralları:
- Repository'ler sadece Aggregate Root'lar için kullanılır
- Repository'ler domain modelini korur
- Repository metodları domain odaklı olmalıdır
- Repository'ler transaction yönetiminden sorumludur
- Repository'ler caching stratejilerini yönetebilir
- Repository'ler bulk operasyonları desteklemelidir
7. Services (Servisler)
Domain servisleri, tek bir aggregate root veya entity üzerinde gerçekleştirilemeyen iş mantığını içeren bileşenlerdir. Özellikle birden fazla aggregate root arasında koordinasyon gerektiren ve harici sistemlerle etkileşim içeren işlemler için kullanılır.
Services Nasıl Kullanılır?
- Domain Servisleri Tanımlama:
// Sipariş İşleme Servisi
class OrderProcessingService(
private val orderRepository: OrderRepository,
private val paymentService: PaymentService,
private val inventoryService: InventoryService,
private val eventPublisher: EventPublisher
) {
suspend fun processOrder(orderId: String): Order {
val order = orderRepository.findById(orderId)
?: throw OrderNotFoundException(orderId)
// Stok kontrolü
order.items.forEach { item ->
if (!inventoryService.hasEnoughStock(item.productId, item.quantity)) {
throw InsufficientStockException(item.productId)
}
}
// Ödeme işlemi
val payment = paymentService.processPayment(
amount = order.calculateTotal(),
orderId = order.orderId
)
// Stok güncelleme
order.items.forEach { item ->
inventoryService.reserveStock(item.productId, item.quantity)
}
// Sipariş durumunu güncelleme
val updatedOrder = order.copy(
status = OrderStatus.PROCESSING,
updatedAt = LocalDateTime.now()
)
// Event yayınlama
eventPublisher.publish(OrderProcessingStartedEvent(
orderId = order.orderId,
paymentId = payment.paymentId
))
return orderRepository.save(updatedOrder)
}
}
// Ödeme Servisi (Harici Sistem Entegrasyonu)
class PaymentService {
suspend fun processPayment(amount: Money, orderId: String): Payment {
// Harici ödeme sistemi entegrasyonu
val paymentResult = externalPaymentGateway.process(
amount = amount,
orderId = orderId
)
return Payment(
paymentId = paymentResult.transactionId,
amount = amount,
status = PaymentStatus.COMPLETED,
orderId = orderId,
paymentMethod = paymentResult.method,
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now()
)
}
}
// Stok Servisi (Harici Sistem Entegrasyonu)
class InventoryService {
suspend fun hasEnoughStock(productId: String, quantity: Int): Boolean {
// Stok kontrolü
return externalInventorySystem.checkStock(productId, quantity)
}
suspend fun reserveStock(productId: String, quantity: Int) {
// Stok rezervasyonu
externalInventorySystem.reserve(productId, quantity)
}
}
-
Servis Kullanım Kuralları:
- Domain servisleri, aggregate root'lar veya entity'ler üzerinde gerçekleştirilemeyen iş mantığını içerir
- Servisler, birden fazla aggregate root arasında koordinasyon sağlar
- Harici sistemlerle etkileşim gerektiren işlemler servisler üzerinden yapılır
- Servisler, domain modelinin tutarlılığını korumalıdır
- Servisler, iş kurallarını uygulamalı ve domain olaylarını yayınlamalıdır
Application Services:
Application Services, kullanıcı arayüzü ile domain katmanı arasında bir köprü görevi görür. Use case'leri koordine eder ve domain servislerini kullanarak iş akışlarını yönetir.
// Application Service
class OrderApplicationService(
private val orderRepository: OrderRepository,
private val orderProcessingService: OrderProcessingService,
private val eventPublisher: EventPublisher
) {
// Use case: Yeni sipariş oluşturma
suspend fun createOrder(command: CreateOrderCommand): OrderResponse {
// Command validasyonu
require(command.items.isNotEmpty()) { "Order must contain at least one item" }
// Domain servislerini kullanarak iş akışını yönetme
val order = orderProcessingService.createOrder(
customerId = command.customerId,
items = command.items.map { it.toOrderItem() },
address = command.deliveryAddress.toAddress()
)
// Event yayınlama
eventPublisher.publish(OrderCreatedEvent(order.orderId))
// Response mapping
return order.toOrderResponse()
}
// Use case: Sipariş işleme
suspend fun processOrder(command: ProcessOrderCommand): OrderResponse {
// Domain servisini kullanarak siparişi işleme
val order = orderProcessingService.processOrder(command.orderId)
// Event yayınlama
eventPublisher.publish(OrderProcessingStartedEvent(
orderId = order.orderId,
paymentId = order.paymentId
))
// Response mapping
return order.toOrderResponse()
}
// Use case: Sipariş sorgulama
suspend fun getOrderDetails(query: GetOrderDetailsQuery): OrderDetailsResponse {
val order = orderRepository.findById(query.orderId)
?: throw OrderNotFoundException(query.orderId)
return order.toOrderDetailsResponse()
}
}
// Commands (Use case input)
data class CreateOrderCommand(
val customerId: String,
val items: List<CreateOrderItemCommand>,
val deliveryAddress: DeliveryAddressCommand
)
data class CreateOrderItemCommand(
val productId: String,
val quantity: Int
)
data class DeliveryAddressCommand(
val street: String,
val city: String,
val country: String,
val postalCode: String
)
data class ProcessOrderCommand(
val orderId: String
)
// Queries (Use case input)
data class GetOrderDetailsQuery(
val orderId: String
)
// Responses (Use case output)
data class OrderResponse(
val orderId: String,
val status: OrderStatus,
val totalAmount: Money,
val items: List<OrderItemResponse>
)
data class OrderItemResponse(
val productId: String,
val name: String,
val quantity: Int,
val price: Money
)
data class OrderDetailsResponse(
val orderId: String,
val status: OrderStatus,
val customerId: String,
val totalAmount: Money,
val items: List<OrderItemResponse>,
val deliveryAddress: DeliveryAddressResponse,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime
)
data class DeliveryAddressResponse(
val street: String,
val city: String,
val country: String,
val postalCode: String
)
// Extension functions for mapping
private fun CreateOrderItemCommand.toOrderItem(): OrderItem {
return OrderItem(
productId = productId,
name = "Product Name", // Bu bilgi product service'den alınmalı
price = Money(BigDecimal("0")), // Bu bilgi product service'den alınmalı
quantity = quantity
)
}
private fun DeliveryAddressCommand.toAddress(): Address {
return Address(
street = street,
city = city,
country = country,
postalCode = postalCode
)
}
private fun Order.toOrderResponse(): OrderResponse {
return OrderResponse(
orderId = orderId,
status = status,
totalAmount = calculateTotal(),
items = items.map { it.toOrderItemResponse() }
)
}
private fun Order.toOrderDetailsResponse(): OrderDetailsResponse {
return OrderDetailsResponse(
orderId = orderId,
status = status,
customerId = customerId,
totalAmount = calculateTotal(),
items = items.map { it.toOrderItemResponse() },
deliveryAddress = deliveryAddress.toDeliveryAddressResponse(),
createdAt = createdAt,
updatedAt = updatedAt
)
}
private fun OrderItem.toOrderItemResponse(): OrderItemResponse {
return OrderItemResponse(
productId = productId,
name = name,
quantity = quantity,
price = price
)
}
private fun Address.toDeliveryAddressResponse(): DeliveryAddressResponse {
return DeliveryAddressResponse(
street = street,
city = city,
country = country,
postalCode = postalCode
)
}
-
Application Service Kullanım Kuralları:
- Application Service'ler use case'leri koordine eder
- Command ve Query pattern'ini kullanır (CQRS)
- Domain servislerini ve repository'leri kullanır
- Transaction yönetiminden sorumludur
- Input validasyonu yapar
- Domain modelini dış dünyaya açmaz (DTO kullanır)
- Event yayınlama ve loglama gibi cross-cutting concern'leri yönetir
❓ Sık Sorulan Sorular ve Best Practices
- Domain Service mi, Repository mi?
Soru: Bir işlem için Domain Service mi yoksa Repository mi kullanmalıyım?
Yanıt: Bu seçim, işlemin kapsamına ve karmaşıklığına bağlıdır:
Repository Kullanımı
- Tek bir aggregate root üzerinde CRUD operasyonları
- Basit sorgulama işlemleri
- Veri erişimi ve saklama işlemleri
// Repository kullanımı uygun olan durum
class OrderRepository {
fun save(order: Order)
fun findById(orderId: String): Order?
fun findByCustomerId(customerId: String): List<Order>
}
Domain Service Kullanımı
- Birden fazla aggregate root arasında koordinasyon
- Karmaşık iş kuralları
- Harici sistemlerle etkileşim
// Domain Service kullanımı uygun olan durum
class OrderProcessingService(
private val orderRepository: OrderRepository,
private val paymentService: PaymentService,
private val inventoryService: InventoryService
) {
suspend fun processOrder(orderId: String): Order {
// Birden fazla aggregate root ve harici sistem koordinasyonu
val order = orderRepository.findById(orderId)
val payment = paymentService.processPayment(order.calculateTotal())
inventoryService.reserveStock(order.items)
// ...
}
}
- Domain Service vs Application Service
Soru: Domain Service ile Application Service arasındaki fark nedir?
Yanıt: İki servis türü farklı sorumluluklara sahiptir:
-
Domain Service:
- Domain mantığını içerir
- İş kurallarını uygular
- Domain olaylarını yayınlar
- Teknik detaylardan bağımsızdır
// Domain Service örneği
class OrderProcessingService {
fun processOrder(order: Order): Order {
// Domain mantığı
if (!order.canBeProcessed()) {
throw OrderCannotBeProcessedException()
}
// İş kuralları
order.validateStockAvailability()
// Domain olayları
eventPublisher.publish(OrderProcessingStartedEvent(order.id))
return order
}
}
-
Application Service:
- Use case'leri koordine eder
- Input validasyonu yapar
- DTO dönüşümlerini yönetir
- Transaction yönetiminden sorumludur
// Application Service örneği
class OrderApplicationService {
suspend fun processOrder(command: ProcessOrderCommand): OrderResponse {
// Input validasyonu
validateCommand(command)
// Transaction yönetimi
return transactionManager.executeInTransaction {
// Domain servisini kullanma
val order = orderProcessingService.processOrder(command.orderId)
// DTO dönüşümü
order.toOrderResponse()
}
}
}
- Value Object vs Entity
Soru: Bir nesneyi Value Object mi yoksa Entity mi olarak tasarlamalıyım?
Yanıt: Bu karar şu kriterlere göre verilmelidir:
-
Entity Olmalı:
- Benzersiz kimliği varsa
- Zamanla değişiyorsa
- Takip edilmesi gerekiyorsa
// Entity örneği
data class Order(
val orderId: String, // Benzersiz kimlik
var status: OrderStatus, // Değişebilir durum
val createdAt: LocalDateTime // Takip edilmesi gereken bilgi
)
-
Value Object Olmalı:
- Kimliği yoksa
- Değişmezse
- Sadece değer taşıyorsa
// Value Object örneği
data class Address(
val street: String,
val city: String,
val country: String
)
- Aggregate Root Seçimi
Soru: Bir aggregate root'u nasıl belirlemeliyim?
Yanıt: Aggregate root seçimi şu kriterlere göre yapılmalıdır:
-
İş Mantığı Bütünlüğü:
- Hangi nesnelerin birlikte değişmesi gerekiyor?
- Hangi nesneler birbirine bağımlı?
-
Tutarlılık Sınırları:
- Hangi nesnelerin tutarlılığı birlikte sağlanmalı?
- Hangi nesneler birbirinden bağımsız olabilir?
-
Performans ve Ölçeklenebilirlik:
- Aggregate boyutu ne kadar olmalı?
- Concurrent işlemler nasıl yönetilecek?
// Aggregate Root örneği
data class Order(
val orderId: String,
private val items: MutableList<OrderItem>,
val customerId: String
) {
// Aggregate Root, içindeki nesnelerin tutarlılığını sağlar
fun addItem(item: OrderItem): Order {
require(item.quantity > 0) { "Quantity must be positive" }
return copy(items = (items + item).toMutableList())
}
}
- Domain Event Kullanımı
Soru: Domain Event'leri ne zaman ve nasıl kullanmalıyım?
Yanıt: Domain Event'ler şu durumlarda kullanılmalıdır:
-
Bağlamlar Arası İletişim:
- Farklı bounded context'ler arasında iletişim
- Loosely coupled sistemler
-
Asenkron İşlemler:
- Uzun süren işlemler
- Background task'lar
-
Audit ve Logging:
- İşlem takibi
- Sistem durumu izleme
// Domain Event kullanım örneği
class OrderService {
fun processOrder(order: Order) {
// İşlem
order.process()
// Event yayınlama
eventPublisher.publish(OrderProcessedEvent(
orderId = order.id,
processedAt = LocalDateTime.now()
))
}
}
- Repository vs DAO
Soru: Repository ile DAO (Data Access Object) arasındaki fark nedir?
Yanıt: İki pattern farklı amaçlara hizmet eder:
-
Repository:
- Domain odaklıdır
- Aggregate root'lar için kullanılır
- İş kurallarını destekler
// Repository örneği
interface OrderRepository {
fun save(order: Order)
fun findPendingOrders(): List<Order>
}
-
DAO:
- Veri erişim odaklıdır
- Tekil entity'ler için kullanılır
- CRUD operasyonlarına odaklanır
// DAO örneği
interface OrderDAO {
fun insert(order: OrderEntity)
fun update(order: OrderEntity)
fun delete(orderId: String)
fun findById(orderId: String): OrderEntity?
}
🏗️ DDD ve Clean Architecture Entegrasyonu
DDD ve Clean Architecture, birbirini tamamlayan iki güçlü yaklaşımdır. Clean Architecture, sistemin katmanlarını ve bağımlılık yönünü belirlerken, DDD bu katmanların içeriğini ve iş mantığını şekillendirir.
1. Katmanlı Mimari (Layered Architecture)
Proje Yapısı
com.example.ecommerce/
├── domain/ # Domain Layer (Merkez)
│ ├── model/ # Domain modelleri
│ │ ├── order/
│ │ │ ├── Order.kt
│ │ │ ├── OrderItem.kt
│ │ │ └── OrderStatus.kt
│ │ └── payment/
│ │ ├── Payment.kt
│ │ └── PaymentStatus.kt
│ ├── repository/ # Repository interfaces
│ │ ├── OrderRepository.kt
│ │ └── PaymentRepository.kt
│ └── service/ # Domain services
│ └── OrderProcessingService.kt
│
├── application/ # Application Layer (Use Cases)
│ ├── usecase/ # Use Case Implementations
│ │ ├── order/
│ │ │ ├── CreateOrderUseCase.kt
│ │ │ └── GetOrderDetailsUseCase.kt
│ │ └── payment/
│ │ └── ProcessPaymentUseCase.kt
│ └── dto/ # DTOs
│ └── OrderDto.kt
│
├── infrastructure/ # Infrastructure Layer
│ ├── persistence/ # Repository Implementations
│ │ ├── order/
│ │ │ ├── InMemoryOrderRepository.kt
│ │ │ └── RoomOrderRepository.kt
│ │ └── payment/
│ │ └── RoomPaymentRepository.kt
│ ├── external/ # External Service Implementations
│ │ ├── payment/
│ │ │ └── ExternalPaymentService.kt
│ │ └── inventory/
│ │ └── ExternalInventoryService.kt
│ └── messaging/ # Messaging Adapters
│ └── event/
│ └── EventBusAdapter.kt
│
└── presentation/ # Presentation Layer
├── mobile/ # Mobile UI
│ ├── viewmodel/
│ │ └── OrderViewModel.kt
│ └── ui/
│ └── OrderScreen.kt
└── web/ # Web UI
├── controller/
│ └── OrderController.kt
└── view/
└── OrderView.kt
Katmanların Açıklaması
-
Domain Layer (domain/)
- Sistemin çekirdeğini oluşturur
- İş mantığını ve kurallarını içerir
- Dış dünyadan bağımsızdır
- Hiçbir dış bağımlılık içermez
- Repository ve servis interface'lerini tanımlar
-
Application Layer (application/)
- Use case'leri koordine eder
- Domain servislerini kullanır
- Input/output dönüşümlerini yapar
- Sadece domain layer'a bağımlıdır
- Infrastructure layer'a doğrudan bağımlılığı yoktur
-
Infrastructure Layer (infrastructure/)
- Teknik detayları içerir
- Domain layer'daki interface'leri implemente eder
- Repository implementasyonları
- Harici servis entegrasyonları
- Sadece Domain layer'a bağımlıdır
-
Presentation Layer (presentation/)
- Kullanıcı arayüzü bileşenleri
- ViewModels
- UI state yönetimi
- Application layer'a bağımlıdır
Bağımlılık Yönü
Presentation Layer → Application Layer → Domain Layer ← Infrastructure Layer
2. Ports & Adapters (Hexagonal) Mimari
Proje Yapısı
com.example.ecommerce/
├── domain/ # Domain Layer (Merkez)
│ ├── model/ # Domain modelleri
│ │ ├── order/
│ │ │ ├── Order.kt
│ │ │ ├── OrderItem.kt
│ │ │ └── OrderStatus.kt
│ │ └── payment/
│ │ ├── Payment.kt
│ │ └── PaymentStatus.kt
│ ├── port/ # Ports (Interfaces)
│ │ ├── input/ # Input Ports (Use Cases)
│ │ │ ├── CreateOrderUseCase.kt
│ │ │ └── ProcessPaymentUseCase.kt
│ │ └── output/ # Output Ports (Repository & Service Interfaces)
│ │ ├── OrderRepository.kt
│ │ └── PaymentService.kt
│ └── service/ # Domain services
│ └── OrderProcessingService.kt
│
├── application/ # Application Layer (Use Cases)
│ ├── usecase/ # Use Case Implementations
│ │ ├── order/
│ │ │ ├── CreateOrderUseCaseImpl.kt
│ │ │ └── GetOrderDetailsUseCaseImpl.kt
│ │ └── payment/
│ │ └── ProcessPaymentUseCaseImpl.kt
│ └── dto/ # DTOs
│ └── OrderDto.kt
│
├── infrastructure/ # Infrastructure Layer (Adapters)
│ ├── persistence/ # Persistence Adapters
│ │ ├── order/
│ │ │ ├── InMemoryOrderRepository.kt
│ │ │ └── RoomOrderRepository.kt
│ │ └── payment/
│ │ └── RoomPaymentRepository.kt
│ ├── external/ # External Service Adapters
│ │ ├── payment/
│ │ │ └── ExternalPaymentService.kt
│ │ └── inventory/
│ │ └── ExternalInventoryService.kt
│ └── messaging/ # Messaging Adapters
│ └── event/
│ └── EventBusAdapter.kt
│
└── presentation/ # Interface Layer (UI Adapters)
├── mobile/ # Mobile UI Adapters
│ ├── viewmodel/
│ │ └── OrderViewModel.kt
│ └── ui/
│ └── OrderScreen.kt
└── web/ # Web UI Adapters
├── controller/
│ └── OrderController.kt
└── view/
└── OrderView.kt
Ports & Adapters Mimarisinin Özellikleri
-
Domain Layer (Merkez)
- İş mantığının merkezi
- Dış dünyadan bağımsız
- Port'ları tanımlar (interface'ler)
- Hiçbir dış bağımlılık içermez
-
Ports (Interface'ler)
- Input Ports: Use case'leri tanımlar
- Output Ports: Repository ve servis interface'lerini tanımlar
- Domain layer'da tanımlanır
- Teknik detaylardan bağımsızdır
-
Adapters (Implementasyonlar)
- Primary Adapters: UI katmanı (Mobile, Web)
- Secondary Adapters: Altyapı katmanı (Persistence, External Services)
- Port'ları implemente eder
- Teknik detayları içerir
Bağımlılık Yönü
Primary Adapters (UI) → Domain Layer ← Secondary Adapters (Infrastructure)
Örnek Kod
// Domain Layer - Ports
interface OrderRepository { // Output Port
fun save(order: Order): Order
fun findById(orderId: String): Order?
}
interface CreateOrderUseCase { // Input Port
suspend fun execute(command: CreateOrderCommand): Order
}
// Application Layer - Use Case Implementation
class CreateOrderUseCaseImpl(
private val orderRepository: OrderRepository
) : CreateOrderUseCase {
override suspend fun execute(command: CreateOrderCommand): Order {
val order = Order(
orderId = UUID.randomUUID().toString(),
items = command.items.map { it.toOrderItem() },
status = OrderStatus.DRAFT,
customerId = command.customerId
)
return orderRepository.save(order)
}
}
// Infrastructure Layer - Secondary Adapter
class InMemoryOrderRepository : OrderRepository {
// Repository implementasyonu yukarıda detaylı olarak gösterilmiştir
}
// Presentation Layer - Primary Adapter
class OrderViewModel(
private val createOrderUseCase: CreateOrderUseCase
) {
suspend fun createOrder(command: CreateOrderCommand): Order {
return createOrderUseCase.execute(command)
}
}
📚 Kaynaklar
- Eric Evans - Domain-Driven Design: Tackling Complexity in the Heart of Software
- Vaughn Vernon - Implementing Domain-Driven Design
- Martin Fowler - Domain-Driven Design
- Robert C. Martin - Clean Architecture
- Alistair Cockburn - Hexagonal Architecture
Not: Bu makale, Domain-Driven Design prensiplerini ve uygulamalarını anlamak için bir başlangıç noktası olarak hazırlanmıştır. Daha detaylı bilgi için yukarıda belirtilen kaynaklara başvurulması önerilir.