2025-10-29
Domain-Driven Design: Giriş ve Temeller
Domain-Driven Design'a kapsamlı giriş - temel kavramlar, yapı taşları, stratejik desenler ve DDD'yi yazılım geliştirmede ne zaman ve nasıl uygulayacağınıza dair pratik rehber
Özet
Domain-Driven Design (DDD), kod yapısını business domain mantığıyla hizalayarak karmaşık yazılım sistemleri oluşturmanın stratejik bir yaklaşımıdır. Bu rehber, DDD’nin temel kavramlarını, yapı taşlarını ve stratejik desenlerini, bu prensipleri ne zaman ve nasıl etkili bir şekilde uygulayacağınızı gösteren pratik TypeScript örnekleriyle birlikte inceliyor.
Domain-Driven Design Nedir?
Domain-Driven Design, Eric Evans tarafından 2003’te tanıtılan, teknik uzmanlar ve domain uzmanları arasındaki işbirliğini vurgulayan bir yazılım geliştirme yaklaşımıdır. Ana fikir: kodunuz, hizmet ettiği business domain’i yansıtmalı ve domain uzmanlarının kullandığı dil ve kavramları kullanmalıdır.
DDD ile çalışmak bana bunun sadece teknik pattern’lerle ilgili olmadığını öğretti—geliştiriciler ve business paydaşları arasında ortak bir anlayış oluşturmakla ilgili. Ubiquitous Language bu anlayışın taşıyıcısıdır; kod ve domain aynı terimleri kullandığında yanlış anlaşılmalar azalır. Kodunuz business’ınızla aynı dili konuştuğunda, iletişim gelişir ve yazılım daha sürdürülebilir hale gelir.
DDD’nin odaklandığı noktalar:
- Ubiquitous Language: Geliştiriciler ve domain uzmanları arasında paylaşılan ortak bir kelime dağarcığı
- Model-Driven Design: Business domain’i yansıtan kod yapısı
- Bounded Context’ler: Sistemin farklı bölümleri arasındaki net sınırlar
- Strategic Design: Büyük sistemleri organize etmek için üst düzey pattern’ler
- Tactical Design: Domain mantığını uygulamak için somut yapı taşları
DDD’yi Ne Zaman Kullanmalı (Ne Zaman Kullanmamalı)
DDD güçlü bir yaklaşım ama evrensel bir çözüm değil. Ne zaman mantıklı olduğuna dair öğrendiklerim.
DDD Kullanın:
Karmaşık Business Logic: Uygulamanız sık değişen karmaşık business kurallarına sahipse, DDD bu karmaşıklığı yönetmeye yardımcı olur. Business mantığının controller’lar ve servisler arasında dağıldığı codebas’ler gördüm - DDD bu kaosa yapı getirir.
Uzun Vadeli Projeler: Yıllarca sürdüreceğiniz sistemler için, DDD modellemesine yapılan ön yatırım karşılığını verir. Açık domain modeli yeni geliştiricilerin adapte olmasını hızlandırır ve değişiklikler sırasında business kurallarını bozma riskini azaltır.
İşbirlikçi Ortamlar: Domain uzmanları mevcut ve işbirliği yapmaya istekliyse, DDD parlıyor. Paylaşılan dil ve model, geleneksel geliştirme yaklaşımlarının başarmakta zorlandığı bir uyum yaratır.
Birden Fazla Bounded Context: Farklı alt alanlara sahip sistemler (örneğin, envanter, ödeme, sevkiyat içeren e-ticaret) DDD’nin sınırları ve ilişkileri yönetmek için stratejik pattern’lerinden faydalanır.
DDD’yi Atlayın:
Basit CRUD Uygulamaları: Minimal business mantığı olan basit bir veri giriş sistemi oluşturuyorsanız, DDD gereksiz karmaşıklık ekler. Temel bir MVC veya katmanlı mimari yeterlidir.
Prototip ve MVP’ler: Hızlı doğrulama projeleri için, DDD’nin modelleme yükü sizi yavaşlatır. Önce geri bildirim alın, proje büyürse DDD’yi düşünün.
Veri Odaklı Sistemler: ETL pipeline’ları, raporlama araçları ve analitik sistemleri genellikle domain modellemesi yerine veri odaklı yaklaşımlarla daha iyi hizmet verir.
Domain Uzmanlığı Olmayan Küçük Ekipler: Domain uzmanlarına erişemiyorsanız veya ekibiniz modelleme yatırımını haklı çıkaramayacak kadar küçükse, daha basit yaklaşımlar daha iyi çalışır.
Temel Yapı Taşları
DDD’nin yapı taşlarını oluşturan tactical pattern’leri inceleyelim. Bunlar kodunuzda kullanacağınız somut implementasyonlar.
Entity’ler
Entity’ler, zaman içinde devam eden benzersiz bir kimliğe sahip nesnelerdir. Aynı veriye sahip ancak farklı ID’lere sahip iki entity farklı nesnelerdir.
// Entity: Benzersiz kimliğe sahip User
class User {
private constructor(
private readonly id: string,
private email: string,
private name: string,
private registeredAt: Date
) {}
static create(email: string, name: string): User {
// Validation mantığı
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
return new User(
crypto.randomUUID(),
email,
name,
new Date()
);
}
static reconstitute(
id: string,
email: string,
name: string,
registeredAt: Date
): User {
return new User(id, email, name, registeredAt);
}
changeEmail(newEmail: string): void {
if (!newEmail.includes('@')) {
throw new Error('Invalid email format');
}
this.email = newEmail;
}
getId(): string {
return this.id;
}
getEmail(): string {
return this.email;
}
equals(other: User): boolean {
return this.id === other.id;
}
}
Temel özellikler:
- Kimlik:
idalanı kullanıcıyı benzersiz şekilde tanımlar - Yaşam Döngüsü: Entity’ler oluşturulur, değiştirilir ve sonunda silinebilir
- Factory method’lar: Yeni entity’ler için
create(), storage’dan yükleme içinreconstitute() - Business mantığı:
changeEmail()gibi method’lar domain kurallarını uygular
Value Object’ler
Value Object’ler kimliği olmayan kavramları temsil eder. Aynı veriye sahip iki value object eşit kabul edilir.
// Value Object: Email adresi
class Email {
private constructor(private readonly value: string) {}
static create(email: string): Email {
if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error('Invalid email format');
}
return new Email(email.toLowerCase());
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
getDomain(): string {
return this.value.split('@')[1];
}
}
// Value Object: Para birimi ile Money
class Money {
private constructor(
private readonly amount: number,
private readonly currency: string
) {}
static create(amount: number, currency: string): Money {
if (amount < 0) {
throw new Error('Amount cannot be negative');
}
return new Money(amount, currency.toUpperCase());
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add money with different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
getAmount(): number {
return this.amount;
}
getCurrency(): string {
return this.currency;
}
}
Value object’ler:
- Immutable: Setter yok, operasyonlar yeni instance’lar döner
- Kendi kendini doğrulayan: Veri geçersizse oluşturma başarısız olur
- Değiştirilebilir: Onları değiştirmezsiniz, yeni instance’larla değiştirirsiniz
- Değere göre karşılaştırılabilir: Eşitlik kimliğe değil veriye dayalıdır
Aggregate’ler
Aggregate’ler, net bir sınır ve tek bir root entity’ye sahip entity ve value object kümeleridir. Aggregate root tutarlılık kurallarını uygular.
// Aggregate: OrderItem'ları olan Order
class OrderItem {
constructor(
private readonly productId: string,
private readonly productName: string,
private readonly price: Money,
private quantity: number
) {
if (quantity <= 0) {
throw new Error('Quantity must be positive');
}
}
getTotal(): Money {
return this.price.multiply(this.quantity);
}
changeQuantity(newQuantity: number): void {
if (newQuantity <= 0) {
throw new Error('Quantity must be positive');
}
this.quantity = newQuantity;
}
getProductId(): string {
return this.productId;
}
getQuantity(): number {
return this.quantity;
}
}
// Aggregate Root
class Order {
private items: OrderItem[] = [];
private status: 'draft' | 'confirmed' | 'shipped' | 'cancelled' = 'draft';
private constructor(
private readonly id: string,
private readonly customerId: string,
private readonly createdAt: Date
) {}
static create(customerId: string): Order {
return new Order(crypto.randomUUID(), customerId, new Date());
}
addItem(productId: string, productName: string, price: Money, quantity: number): void {
if (this.status !== 'draft') {
throw new Error('Cannot modify confirmed order');
}
// Item zaten var mı kontrol et
const existingItem = this.items.find(item => item.getProductId() === productId);
if (existingItem) {
existingItem.changeQuantity(existingItem.getQuantity() + quantity);
} else {
this.items.push(new OrderItem(productId, productName, price, quantity));
}
}
removeItem(productId: string): void {
if (this.status !== 'draft') {
throw new Error('Cannot modify confirmed order');
}
this.items = this.items.filter(item => item.getProductId() !== productId);
}
confirm(): void {
if (this.items.length === 0) {
throw new Error('Cannot confirm empty order');
}
if (this.status !== 'draft') {
throw new Error('Order already confirmed');
}
this.status = 'confirmed';
}
cancel(): void {
if (this.status === 'shipped') {
throw new Error('Cannot cancel shipped order');
}
this.status = 'cancelled';
}
getTotal(): Money {
if (this.items.length === 0) {
return Money.create(0, 'USD');
}
return this.items.reduce(
(total, item) => total.add(item.getTotal()),
Money.create(0, 'USD')
);
}
getId(): string {
return this.id;
}
getItems(): readonly OrderItem[] {
return [...this.items];
}
getStatus(): string {
return this.status;
}
}
Aggregate prensipleri:
- Tek Root: Dışarıdan sadece
Orderreferans edilir;OrderItemiçseldir - Tutarlılık Sınırı: Tüm business kuralları aggregate root tarafından uygulanır
- Transaction’al: Bir aggregate’e yapılan değişiklikler atomik olarak commit edilmelidir
- ID ile Referans: Diğer aggregate’ler buna ID ile referans verir, doğrudan nesne referansı ile değil
Repository’ler
Repository’ler, aggregate’lere erişim için bir soyutlama sağlar ve persistence detaylarını gizler.
// Repository interface'i
interface OrderRepository {
save(order: Order): Promise<void>;
findById(orderId: string): Promise<Order | null>;
findByCustomer(customerId: string): Promise<Order[]>;
delete(orderId: string): Promise<void>;
}
// Test için in-memory implementasyon
class InMemoryOrderRepository implements OrderRepository {
private orders = new Map<string, Order>();
async save(order: Order): Promise<void> {
this.orders.set(order.getId(), order);
}
async findById(orderId: string): Promise<Order | null> {
return this.orders.get(orderId) || null;
}
async findByCustomer(customerId: string): Promise<Order[]> {
return Array.from(this.orders.values()).filter(
order => order['customerId'] === customerId
);
}
async delete(orderId: string): Promise<void> {
this.orders.delete(orderId);
}
}
// PostgreSQL implementasyonu
class PostgresOrderRepository implements OrderRepository {
constructor(private db: any) {} // Veritabanı client'ınız
async save(order: Order): Promise<void> {
await this.db.transaction(async (trx: any) => {
// Order'ı kaydet
await trx('orders').insert({
id: order.getId(),
customer_id: order['customerId'],
status: order.getStatus(),
created_at: order['createdAt']
}).onConflict('id').merge();
// Order item'ları kaydet
await trx('order_items').where('order_id', order.getId()).delete();
const items = order.getItems().map(item => ({
order_id: order.getId(),
product_id: item.getProductId(),
quantity: item.getQuantity(),
// ... diğer alanlar
}));
if (items.length > 0) {
await trx('order_items').insert(items);
}
});
}
async findById(orderId: string): Promise<Order | null> {
const orderData = await this.db('orders')
.where('id', orderId)
.first();
if (!orderData) return null;
const itemsData = await this.db('order_items')
.where('order_id', orderId);
// Aggregate'i veriden yeniden oluştur
return this.reconstitute(orderData, itemsData);
}
async findByCustomer(customerId: string): Promise<Order[]> {
const ordersData = await this.db('orders')
.where('customer_id', customerId);
return Promise.all(
ordersData.map((data: any) => this.findById(data.id))
);
}
async delete(orderId: string): Promise<void> {
await this.db.transaction(async (trx: any) => {
await trx('order_items').where('order_id', orderId).delete();
await trx('orders').where('id', orderId).delete();
});
}
private reconstitute(orderData: any, itemsData: any[]): Order {
// Order aggregate'ini veritabanı verisinden yeniden oluştur
// Bu, Order üzerinde statik bir reconstitute method'u kullanır
// Implementation detayları kısalık için atlandı
throw new Error('Not implemented');
}
}
Repository pattern’leri:
- Collection benzeri interface: Bunu in-memory bir collection olarak düşünün
- Persistence ignorance: Domain katmanı veritabanı detaylarını bilmez
- Aggregate odaklı: Her aggregate root için bir repository
- Test edilebilirlik: Test için implementasyonları kolayca değiştirebilirsiniz
Domain Service’ler
Domain service’ler, bir entity veya value object’e doğal olarak uymayan business mantığını içerir. Domain nesneleri üzerinde stateless operasyonlardır.
// Domain Service: Fiyatlandırma hesaplaması
class PricingService {
calculateDiscount(order: Order, customer: Customer): Money {
const total = order.getTotal();
// VIP müşteriler %10 indirim alır
if (customer.isVIP()) {
return total.multiply(0.1);
}
// 500$'ın üzerindeki siparişler %5 indirim alır
if (total.getAmount() >= 500) {
return total.multiply(0.05);
}
return Money.create(0, total.getCurrency());
}
applySeasonalPricing(
basePrice: Money,
season: 'peak' | 'regular' | 'off-peak'
): Money {
switch (season) {
case 'peak':
return basePrice.multiply(1.3);
case 'off-peak':
return basePrice.multiply(0.7);
default:
return basePrice;
}
}
}
// Domain Service: Order fulfillment koordinasyonu
class OrderFulfillmentService {
constructor(
private inventoryService: InventoryService,
private shippingService: ShippingService
) {}
async fulfillOrder(order: Order): Promise<void> {
// Envanter doğrulaması
for (const item of order.getItems()) {
const available = await this.inventoryService.checkAvailability(
item.getProductId(),
item.getQuantity()
);
if (!available) {
throw new Error(`Product ${item.getProductId()} not available`);
}
}
// Envanter rezervasyonu
for (const item of order.getItems()) {
await this.inventoryService.reserve(
item.getProductId(),
item.getQuantity(),
order.getId()
);
}
// Sevkiyat ayarla
await this.shippingService.createShipment(order);
}
}
Domain service’leri ne zaman kullanmalı:
- Cross-aggregate operasyonlar: Birden fazla aggregate içeren mantık
- External sistem koordinasyonu: Birden fazla domain operasyonunu orkestre etme
- Karmaşık hesaplamalar: Birden fazla entity kullanan ama birine ait olmayan business mantığı
- Stateless operasyonlar: İç state yok, sadece dönüşümler
Stratejik Design Pattern’leri
Stratejik DDD pattern’leri büyük sistemleri organize etmeye ve karmaşıklığı daha üst düzeyde yönetmeye yardımcı olur.
Ubiquitous Language
Ubiquitous Language, geliştiriciler ve domain uzmanları arasındaki paylaşılan kelime dağarcığıdır. Bu dil kod, konuşmalar, dokümantasyon ve testlerde görünür.
İşte öğrendiğim: kodunuz business paydaşlarınızdan farklı terimler kullandığında, çeviri hataları sızar. Business buna “rezervasyon” derken kodunuz “booking” diyorsa, birisi gereksinimleri yanlış anlayacaktır.
Kötü Örnek (Genel teknik terimler):
class DataManager {
processRequest(data: any): any {
// "process" business terimleriyle ne anlama geliyor?
}
}
İyi Örnek (Ubiquitous Language):
class ReservationService {
confirmReservation(reservation: Reservation): void {
// Net business operasyonu
}
cancelReservation(reservationId: string): void {
// Business paydaşları bunu anlar
}
}
Pratikte, ubiquitous language oluşturmak şu anlama gelir:
- Domain uzmanlarıyla işbirlikçi modelleme oturumları
- Business ve teknoloji arasında paylaşılan terim sözlüğü
- Kod, doküman ve konuşmalarda tutarlı isimlendirme
- Anlayış derinleştikçe zaman içinde iyileştirme
Bounded Context’ler
Bounded Context, bir domain modelinin tanımlandığı ve uygulandığı açık bir sınırdır. Farklı context’ler aynı kavram için farklı modellere sahip olabilir.
Bir e-ticaret sisteminde “Customer”ı düşünün:
Kod’da bunlar farklı görünebilir:
// Sales Context - Satın almaya odaklı Customer
namespace SalesContext {
export class Customer {
constructor(
private readonly id: string,
private readonly email: string,
private shippingAddresses: Address[],
private orderHistory: Order[]
) {}
placeOrder(order: Order): void {
this.orderHistory.push(order);
}
getPreferredShippingAddress(): Address {
// Satış için business mantığı
return this.shippingAddresses[0];
}
}
}
// Support Context - Servis sorunlarına odaklı Customer
namespace SupportContext {
export class Customer {
constructor(
private readonly id: string,
private readonly email: string,
private tickets: SupportTicket[],
private preferredContactMethod: 'email' | 'phone'
) {}
createTicket(issue: string): SupportTicket {
const ticket = new SupportTicket(issue, this.id);
this.tickets.push(ticket);
return ticket;
}
getOpenTickets(): SupportTicket[] {
return this.tickets.filter(t => t.isOpen());
}
}
}
Bounded context’lerin faydaları:
- Odaklanmış modeller: Her context sadece ihtiyacı olanı içerir
- Bağımsız evrim: Context’ler diğerlerini etkilemeden değişebilir
- Net sahiplik: Ekipler belirli context’lere sahiptir
- Azaltılmış coupling: Bağımlılıklar context sınırlarında açıktır
Context Mapping
Context Mapping, bounded context’ler arasındaki ilişkileri tanımlar. Yaygın pattern’ler:
Customer/Supplier: Downstream context upstream’e bağlıdır. Ekipler değişiklikleri müzakere eder.
// Sales Context (Upstream)
interface OrderPlaced {
orderId: string;
items: { productId: string; quantity: number }[];
}
// Inventory Context (Downstream)
class InventoryService {
handleOrderPlaced(event: OrderPlaced): void {
// Siparişe dayalı envanter rezerve et
event.items.forEach(item => {
this.reserveStock(item.productId, item.quantity);
});
}
}
Anti-Corruption Layer: Modelinizi harici sistem kavramlarından korur.
// External legacy sistem farklı modele sahip
interface LegacyCustomerDTO {
cust_id: number;
cust_name: string;
cust_email: string;
// İhtiyacımız olmayan birçok alan
}
// Anti-Corruption Layer
class LegacyCustomerAdapter {
toDomainModel(dto: LegacyCustomerDTO): Customer {
return new Customer(
dto.cust_id.toString(),
Email.create(dto.cust_email),
dto.cust_name
);
}
toDTO(customer: Customer): LegacyCustomerDTO {
return {
cust_id: parseInt(customer.getId()),
cust_name: customer.getName(),
cust_email: customer.getEmail().getValue()
};
}
}
Shared Kernel: İki context domain modelinin bir alt kümesini paylaşır. Değişiklikler koordinasyon gerektirir.
// Sales ve Pricing arasında shared kernel
namespace SharedKernel {
export class Money {
// Paylaşılan implementasyon
}
export class ProductId {
// Paylaşılan value object
}
}
Yaygın Tuzaklar ve Nasıl Kaçınılır
DDD ile çalışırken, bu sorunlarla tekrar tekrar karşılaştım:
Anemic Domain Model’ler
Sorun: Entity’ler getter/setter’lara sahip veri containerları haline gelir, tüm mantık service’lerde.
// Anemic - Bunu yapmayın
class Order {
public id: string;
public items: OrderItem[];
public status: string;
// Sadece getter ve setter'lar, davranış yok
}
class OrderService {
placeOrder(order: Order): void {
// Order içinde değil, burada tüm business mantığı
if (order.items.length === 0) {
throw new Error('Empty order');
}
order.status = 'placed';
}
}
Çözüm: Davranışı domain modeline taşıyın.
// Zengin domain model
class Order {
private status: OrderStatus;
private items: OrderItem[];
place(): void {
if (this.items.length === 0) {
throw new Error('Cannot place empty order');
}
this.status = OrderStatus.Placed;
}
}
Basit Domain’leri Aşırı Mühendislik
Sorun: Basit CRUD operasyonlarına tam DDD uygulamak.
Basit bir adres defteri oluşturuyorsanız, aggregate’lere, repository’lere ve domain service’lere ihtiyacınız yok. Validation ile basit bir veri modeli yeterlidir. DDD’yi karmaşık business mantığı için saklayın.
Context Sınırlarını Görmezden Gelmek
Sorun: Tüm sistemi tek bir büyük model olarak ele almak.
Bu, her kullanım durumuna hizmet etmeye çalışan 50 özelliğe sahip Customer gibi god object’lere yol açar. Bunun yerine, “customer”ın sales, support ve billing context’lerinde farklı anlamlara geldiğini kabul edin.
Repository’yi Database Gateway Olarak Kullanmak
Sorun: Her kullanım durumu için sorgu methodları sunan repository’ler.
// Çok fazla sorgu methodu
interface OrderRepository {
findById(id: string): Promise<Order>;
findByCustomerId(customerId: string): Promise<Order[]>;
findByStatus(status: string): Promise<Order[]>;
findByDateRange(start: Date, end: Date): Promise<Order[]>;
findByCustomerAndStatus(customerId: string, status: string): Promise<Order[]>;
// ... 20 method daha
}
Çözüm: Repository’leri aggregate root’lara odaklı tutun. Sorgular için ayrı read model’ler kullanın.
// Basit repository
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
delete(id: string): Promise<void>;
}
// Okumalar için ayrı query service
interface OrderQueryService {
searchOrders(criteria: OrderSearchCriteria): Promise<OrderDTO[]>;
}
Büyük Aggregate’ler
Sorun: Çok büyüyen ve performans sorunlarına neden olan aggregate’ler.
Order aggregate’iniz customer detaylarını, sevkiyat bilgilerini, ödeme geçmişini ve ürün kataloglarını içeriyorsa, basit operasyonlar için çok fazla veri yüklersiniz.
Çözüm: Aggregate’leri küçük ve odaklı tutun. Diğer aggregate’lere ID ile referans verin.
class Order {
constructor(
private readonly id: string,
private readonly customerId: string, // ID ile referans
private items: OrderItem[]
) {}
}
Ubiquitous Language’ı Atlamak
Sorun: Geliştiriciler business dilini kullanmak yerine kendi teknik terimlerini yaratır.
Bu, hataların gizlendiği bir çeviri katmanı oluşturur. Kod “transaction processing” derken business “payment confirmation” dediğinde, yanlış anlamalar oluşur.
Çözüm: İşbirlikçi modelleme oturumlarına zaman yatırın. Kodunuz domain uzmanlarınızın kullandığı kesin terimleri kullanmalıdır.
Pratik Uygulama İpuçları
Pratikte işe yarayanlar:
Küçük Başlayın: Her şeyi birden ele almayın. Bir karmaşık subdomain seçin ve DDD’yi orada uygulayın. Genişletmeden önce ekibiniz için neyin işe yaradığını öğrenin.
Event Storming: Geliştiricilerin ve domain uzmanlarının yapışkan notlarla business süreçlerini haritaladığı işbirlikçi oturumlar düzenleyin. Bu, ubiquitous language’ı ve bounded context’leri doğal olarak ortaya çıkarır.
Test-Driven Development: Testleri business dilinde yazın. Bu, domain modelini güçlendirir ve kodun business niyetinden ayrıldığını yakalar.
describe('Order', () => {
it('boş siparişlerin onaylanmasını engellemeli', () => {
const order = Order.create('customer-123');
expect(() => order.confirm()).toThrow('Cannot confirm empty order');
});
it('birden fazla item ile doğru toplamı hesaplamalı', () => {
const order = Order.create('customer-123');
order.addItem('product-1', 'Widget', Money.create(10, 'USD'), 2);
order.addItem('product-2', 'Gadget', Money.create(15, 'USD'), 1);
expect(order.getTotal().getAmount()).toBe(35);
});
});
Aşamalı Refactoring: Her şeyi yeniden yazmanıza gerek yok. Domain mantığını service’lerden entity’lere kademeli olarak çıkarın. Her iyileştirme bir sonrakini kolaylaştırır.
Kod Olarak Dokümantasyon: Domain kavramlarını belgelemek için type’lar ve interface’ler kullanın. Kendi kendini belgeleyen kod, eskiyen harici doküman ihtiyacını azaltır.
Kaynaklar ve İleri Okuma
DDD anlayışınızı derinleştirmek için temel kaynaklar:
Kitaplar:
- “Domain-Driven Design” by Eric Evans - Orijinal mavi kitap. Yoğun ama kapsamlı. Yapı taşlarıyla ilgili Bölüm II ile başlayın.
- “Implementing Domain-Driven Design” by Vaughn Vernon - Daha pratik ve modern. Implementasyon rehberliği için mükemmel.
- “Domain-Driven Design Distilled” by Vaughn Vernon - Yoğunlaştırılmış giriş, hızlı başlamak için iyi.
Online Kaynaklar:
- Martin Fowler’ın makaleleri martinfowler.com’da - Temel pattern’lerin net açıklamaları
- DDD Community ddd-community.org’da - Kaynaklar ve etkinliklerle aktif topluluk
- Alberto Brandolini’nin EventStorming websitesi - İşbirlikçi modelleme tekniği
Pratik Örnekler:
- Microsoft’un .NET Microservices Architecture rehberi - Pratikte iyi DDD pattern’leri
- GitHub’da çeşitli dillerde DDD implementasyonlarını gösteren örnek projeler
Sonuç
Domain-Driven Design, yazılım sistemlerindeki karmaşıklığı yönetmek için güçlü araçlar sağlar. Tactical pattern’ler - entity’ler, value object’ler, aggregate’ler, repository’ler ve domain service’ler - size temiz domain modelleri için yapı taşları verir. Stratejik pattern’ler - ubiquitous language, bounded context’ler ve context mapping - büyük sistemleri organize etmeye yardımcı olur.
İşte en çok önemsediğim şeyleri öğrendim: DDD körü körüne pattern’leri uygulamakla ilgili değil. Business ve teknoloji arasında ortak bir anlayış oluşturmak, sonra bu anlayışı kodda ifade etmekle ilgili. Kodunuz business dilini konuştuğunda, modelleriniz domain uzmanlarının problemler hakkında düşünme şeklini yansıttığında, yazılımın anlaşılması, sürdürülmesi ve evrimleşmesi daha kolay hale gelir.
Bir karmaşık subdomain ile başlayın. Domain uzmanlarıyla işbirliği yapın. Birlikte ubiquitous language oluşturun. Tactical pattern’leri değer kattıkları yerde uygulayın. Sisteminiz büyüdükçe bounded context’leri tanıyın. DDD bir yolculuktur, bir hedef değil - bu prensipleri gerçek projelerde uygularken anlayışınız derinleşecektir.
İlgili yazılar
AWS CDK projelerinde service-based, domain-based, feature-based veya layer-based organizasyon patternlerini ne zaman kullanacağını öğren. Karar çerçeveleri, çalışan örnekler ve sürdürülebilir infrastructure code için migration stratejileri.
Singleton, Factory, Builder ve Prototype pattern'lerinin TypeScript'te nasıl evrildiğini keşfet. ES modüllerinin singleton'ları ne zaman değiştirdiğini, factory function'ların ne zaman class'lardan daha iyi olduğunu ve TypeScript'in type sisteminin oyunu nasıl değiştirdiğini öğren.
Decorator, Adapter, Facade, Composite ve Proxy patternlerinin React ve TypeScript'te nasıl evrildiğini keşfedin. HOC'ların ne zaman hook'lara yol verdiğini, adapterlerin third-party API'ları nasıl izole ettiğini ve facade'ların karmaşıklığı nasıl basitleştirdiğini öğrenin.
SOLID prensiplerininin modern JavaScript geliştirmede nasıl uygulanacağını öğrenin. TypeScript, React hooks ve fonksiyonel pattern'ler ile pratik örnekler - ayrıca ne zaman kullanmalı, ne zaman gereksiz.
Kural tabanlı chatbot'lardan otonom AI agent'larına mimari evrimi keşfet. ReAct, Plan-and-Execute ve çoklu-agent desenleri TypeScript implementasyonları ve pratik geçiş stratejileriyle öğren.