DDD’nin Kalbinde Design Patterns — Makale 4: Entity ve Value Object — Kimlik ve Değer
İçindekiler

Entity ve Value Object — Kimlik ve Değer
DDD’nin Kalbinde Design Patterns — Makale 4: Entity ve Value Object — Kimlik ve Değer
İçindekiler
» Makale 4: Entity ve Value Object — Kimlik ve Değer Arasındaki İnce Çizgi » Giriş: İki Varoluş Biçimi » 1. Kimlik mi, Değer mi? Temel Ayrımın Anatomisi » 2. Entity: Derinlemesine İnceleme » 3. Value Object: Derinlemesine İnceleme » 4. GoF Flyweight Pattern ve Value Object İlişkisi » 5. Sınır Vakalar: Zor Kararlar » 6. Veritabanı Yansıması: Entity ve Value Object Nasıl Saklanır? » 7. Yaygın Hatalar ve Anti-Pattern’ler » 8. Test Stratejisi: Entity ve Value Object Nasıl Test Edilir? » 9. Trendyol Örneği: E-Ticaret Sisteminde Entity ve Value Object Haritası » 10. Sonuç: İki Varoluş Biçiminin Mimari Önemi » Serinin Bir Sonraki Makalesinde
Giriş: İki Varoluş Biçimi

Gerçek dünyada nesneler iki temel biçimde var olur. Bazı nesneler, kim oldukları sayesinde var olur — kimliği, geçmişi, sürekliliği olan varlıklardır. Bir insan, aynı kişidir — saçını kestirse, kilo alsa, farklı bir şehre taşınsa bile. Onu tanımlayan şey fiziksel özellikleri değil, kimliğidir. Bazı nesneler ise ne oldukları sayesinde var olur — değeriyle, özelliğiyle, içeriğiyle tanımlanırlar. Bir 100 TL banknotunu düşünün: kâğıdın seri numarasının önemi yoktur. O banknotun değeri 100 TL’dir ve başka bir 100 TL banknotu ile birebir eşdeğerdir.
Yazılım dünyasında bu iki varoluş biçimi, DDD’nin en temel iki yapı taşına karşılık gelir: Entity (Varlık) ve Value Object (Değer Nesnesi). Bu iki kavramı doğru ayırt etmek, sistem tasarımında bir onlarca kritik kararın temelini oluşturur: equality nasıl tanımlanır? Nesne değiştirilebilir mi, değiştirilemez mi? Veritabanında nasıl saklanır? Test nasıl yazılır? Paylaşılabilir mi, paylaşılmamalı mı?
Ve bu kararlar yüzeysel görünse de, yanlış yapıldığında zincirleme hatalar üretir. Bir Value Object’e ID vermek, immutability’yi kaybetmek, equality’yi yanlış tanımlamak, ya da bir Entity’yi değeri gibi davranmak — her biri, zamanla kodun bakımını zorlaştıran, böceklerin saklandığı köşeler yaratır.
Bu makale bu iki kavramı, GoF’un Flyweight pattern’i ile bağlantısını, Java 17'nin sunduğu modern araçları ve Trendyol gibi bir e-ticaret sistemindeki somut uygulamalarını derinlemesine inceliyor.
1. Kimlik mi, Değer mi? Temel Ayrımın Anatomisi
1.1 Kimlikle Var Olmak: Entity

Bir Entity, yaşam döngüsü boyunca süreklilik gösteren, kimliğiyle var olan bir iş nesnesidir. Entity’nin en temel özelliği şudur: iki Entity, tüm özellikleri birebir aynı olsa bile, farklı kimliklere sahiplerse farklı Entity’lerdir.
Bu soyut tanımı somutlaştıralım. Trendyol’da iki müşteri düşünelim: her ikisinin de adı “Ali Yılmaz”, her ikisi de İstanbul’da yaşıyor, her ikisi de aynı telefon numarasını kullanıyor (pek mümkün değil ama örnek olsun). Bu iki müşteri aynı mı? Hayır. Çünkü farklı müşteri ID’lerine sahipler. Birinin sipariş geçmişi, birinin ödeme yöntemi, birinin iletişim tercihleri birbirinden bağımsızdır. Kimlik, onları birbirinden ayıran tek gerçektir.
Aynı şekilde, bir sipariş düşünelim. Sipariş #12345 ve sipariş #67890 aynı ürünleri, aynı müşteri için, aynı tutarla içerebilir. Ama bunlar iki farklı siparişdir. Biri iptal edilebilir, diğeri kargoda olabilir. Kimlik onları birbirinden ayırır ve o kimlik değişmez — sipariş durumu değişse, adres güncellense, tutarı değişse bile, o sipariş hâlâ o siparişdir.
Bu diyagram, Entity’nin en temel özelliğini görselleştirmektedir: bir Entity’nin durumu (state) zaman içinde değişebilir, ama kimliği (identity) asla değişmez. Sipariş #12345, PENDING durumundayken de DELIVERED durumundayken de aynı siparişdir. Adresi İstanbul’dayken de Ankara’ya güncellendiğinde de aynı siparişdir. Bu sürekliliği sağlayan tek şey kimliğidir. Ve iki Entity’nin eşitliği, ID’lerinin eşitliğiyle belirlenir — başka hiçbir alana bakılmaz.
1.2 Değerle Var Olmak: Value Object

Bir Value Object, kimliği olmayan, yalnızca değeriyle tanımlanan bir kavramı temsil eder. Value Object’in en temel özelliği şudur: iki Value Object, tüm özellikleri aynıysa, kesin olarak eşdeğerdir — ve bu eşdeğerlik mutlak ve değiştirilemezdir.
100 TL değerinde bir Money nesnesi düşünelim. Bu nesne hangi değişkende tutulursa tutulsun, kim tarafından oluşturulursa oluşturulsun, 100 TL olan başka bir Money nesnesiyle tamamen eşdeğerdir. Kimliği yoktur, geçmişi yoktur, yaşam döngüsü yoktur. Sadece değeri vardır: 100 TL.
Aynı şekilde, İstanbul, Kadıköy, Moda Caddesi No:5 Daire:3 adresi bir Value Object'tir. Bu adres, sistemde kaç kez kullanılırsa kullanılsın, her kullanımda aynı şeyi temsil eder. Adresin değişmesi gerektiğinde — müşteri taşındı, örneğin — var olan adres nesnesini değiştirmezsiniz; yeni bir adres nesnesi oluşturursunuz. Bu, immutability'nin özüdür.
Bu diyagram, Value Object’in üç temel özelliğini bir arada göstermektedir. Eşitlik bölümü, iki Money(100, TRY) nesnesinin ID'ye bakılmaksızın eşit kabul edilmesi gerektiğini göstermektedir — bu, Entity eşitliğinden köklü biçimde farklıdır. Immutability bölümü, bir Value Object'in "değiştirilmediğini", bunun yerine yeni bir nesne oluşturulduğunu göstermektedir; mutation yasaktır. Kimlik yok bölümü ise Value Object'in veritabanında ayrı bir tablo olarak değil, sahibinin (Entity'nin) sütunu olarak saklandığını göstermektedir — bu karar, veri modelini de doğrudan etkiler.
1.3 Karar Anı: Entity mi, Value Object mı?

Bu soruyu yanıtlamak için tek bir soru yeterlidir çoğu zaman: “Bu nesnenin geçmişi ve sürekliliği önemli mi?”
Eğer “bu nesne değişse bile, aynı nesne olmaya devam etmeli mi?” sorusunun cevabı evet ise, o nesne bir Entity’dir. Eğer “bu nesnenin değeri değişirse, benim için artık farklı bir nesnedir” cevabı doğruysa, o nesne bir Value Object’tir.
Bu karar ağacı, Entity/Value Object ayrımını yaparken sorulması gereken beş soruyu sistematik biçimde sıralamaktadır. Her soru, bir sonrakine giden bir eleme mekanizması işlevi görür. Pratikte çoğu karar ilk iki soruyla netleşir; ancak sınır vakalarda (örneğin adres — müşterinin adresi Entity mi, Value Object mi?) sonraki sorular kritik yol gösterici olur.
2. Entity: Derinlemesine İnceleme
2.1 Identity: Kimliği Doğru Modellemek

Entity’nin kimliği, onun en kritik özelliğidir. Ve bu kimliği doğru modellemek, görünenden çok daha derin bir karardır.
Primitive ID Anti-Pattern: En yaygın hata, ID’yi Long veya String gibi primitive tiplerle temsil etmektir. Bu yaklaşım çalışır, ama ciddi bir zayıflık taşır: tip sistemi, yanlış ID'nin yanlış yere geçirilmesini engelleyemez.
// ❌ Primitive ID — tip sistemi bizi koruyamıyor
public class Order {
private Long id; // Long
private Long customerId; // Long — ama hangisi sipariş ID'si, hangisi müşteri?
}
// Bu çağrı derleniyor - ama yanlış!
orderRepository.findById(customer.getId()); // Müşteri ID'si, sipariş yerine
// Derleyici hata vermez. Runtime'da yanlış sonuç döner.
// ❌ String ID - daha da tehlikeli
public void processOrder(String orderId, String customerId) {
// Sıralama yanlışsa kim fark eder?
orderService.process(customerId, orderId); // Argümanlar yer değiştirdi
}
// ✅ Typed ID — Value Object olarak ID
// Her Entity türü için ayrı ID tipi — tip sistemi bizi korur
public record OrderId(UUID value) {
public OrderId {
Objects.requireNonNull(value, "OrderId değeri null olamaz");
}
// Factory method - okunabilir yaratım
public static OrderId generate() {
return new OrderId(UUID.randomUUID());
}
public static OrderId of(String value) {
return new OrderId(UUID.fromString(value));
}
@Override
public String toString() {
return value.toString();
}
}
public record CustomerId(UUID value) {
public CustomerId {
Objects.requireNonNull(value, "CustomerId değeri null olamaz");
}
public static CustomerId generate() { return new CustomerId(UUID.randomUUID()); }
}
// Artık yanlış geçirme imkânsız - derleyici yakalar
public class Order {
private final OrderId id;
private final CustomerId customerId; // OrderId ile karıştırılamaz!
}
// Bu çağrı derleme hatası verir - CustomerId, OrderId bekleyen yere geçirilemez
// orderRepository.findById(customer.id()); // DERLEME HATASI ✅
Typed ID kullanımı, “primitive obsession” anti-pattern’inin ID boyutunu çözer. OrderId, CustomerId, ProductId — her biri farklı bir tip olduğundan, tip sistemi yanlış ID geçirmeyi derleme zamanında engeller. Bu, runtime hatalarını en pahalı maliyetinin — production'da yaşanmasının — önüne geçer.
2.2 Entity Equality: ID Üzerinden Eşitlik

Entity’nin equals() ve hashCode() metodları yalnızca ID üzerinden çalışmalıdır. Diğer alanlar equality tanımına dahil edilmemelidir — çünkü bir Entity'nin durumu değiştiğinde eşitliğin değişmesi, koleksiyonlarda ve cache'lerde beklenmedik davranışlara yol açar.
public class Order {
private final OrderId id;
private final CustomerId customerId;
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
private ShippingAddress shippingAddress;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// ✅ Equality: YALNIZCA ID üzerinden
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Order other)) return false;
return Objects.equals(this.id, other.id);
}
// ✅ HashCode: YALNIZCA ID üzerinden
@Override
public int hashCode() {
return Objects.hash(id);
}
// ❌ YANLIŞ: Tüm alanları dahil etmek
// @Override
// public boolean equals(Object obj) {
// // status değişince hashCode değişir
// // Bu nesneyi bir HashSet'e koyarsanız ve sonra status değişirse,
// // nesneyi bulamazsınız - kaybolur!
// return Objects.equals(id, other.id)
// && Objects.equals(status, other.status)
// && Objects.equals(totalAmount, other.totalAmount);
// }
// ────────────────────────────────────────────────────────────
// Entity'nin davranışları: iş fiilleriyle
// ────────────────────────────────────────────────────────────
// İş kuralı: sipariş iptal edilebilir mi?
public boolean isCancellable() {
return this.status == OrderStatus.PENDING
|| this.status == OrderStatus.CONFIRMED;
}
// İş operasyonu: siparişi iptal et, Domain Event döndür
public List<DomainEvent> cancel(CancellationReason reason) {
if (!isCancellable()) {
throw new OrderCannotBeCancelledException(
String.format(
"Sipariş %s durumunda olduğu için iptal edilemez. " +
"Yalnızca PENDING veya CONFIRMED siparişler iptal edilebilir.",
this.status
)
);
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = LocalDateTime.now();
return List.of(new OrderCancelledEvent(
this.id,
this.customerId,
reason,
Instant.now()
));
}
// İş operasyonu: ödeme onaylandı
public List<DomainEvent> confirmPayment(PaymentId paymentId) {
if (this.status != OrderStatus.PENDING) {
throw new InvalidOrderStateException(
"Ödeme onayı yalnızca PENDING siparişlerde yapılabilir."
);
}
this.status = OrderStatus.CONFIRMED;
this.updatedAt = LocalDateTime.now();
return List.of(new PaymentConfirmedEvent(
this.id,
paymentId,
this.totalAmount,
Instant.now()
));
}
// İş hesabı: toplam tutarı hesapla
public Money calculateTotalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO_TRY, Money::add);
}
}
2.3 Entity’nin Yaşam Döngüsü: State Machine Olarak
Entity’ler çoğunlukla bir durum makinesi (State Machine) davranışı sergiler. Order'ın durumları — PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED — belirli geçiş kurallarına tabidir. Bu geçiş kuralları, Entity'nin içinde yaşamalıdır.

Bu state machine diyagramı, Order Entity'sinin olası tüm durumlarını ve bu durumlar arasındaki geçişleri göstermektedir. Her geçiş bir iş operasyonuna karşılık gelir: confirmPayment(), cancel(), dispatch(), markAsDelivered(), requestReturn(), approveReturn(), rejectReturn(). Bu geçişlerin her biri yalnızca belirli kaynak durumlardan gerçekleşebilir: cancel() yalnızca PENDING veya CONFIRMED durumundan çağrılabilir; SHIPPED bir siparişi iptal etmek mümkün değildir. Bu kurallar Entity'nin içinde, her bir metodun başındaki guard condition'larla uygulanır.
3. Value Object: Derinlemesine İnceleme
3.1 Immutability: Neden Değişmezlik Zorunludur?

Value Object’in immutability kuralı, çoğu zaman “güzel ama zorunlu mu?” diye sorgulanır. Cevap kesindir: zorunludur. Ve bu zorunluluğun arkasında derin nedenler yatar.
Neden 1 — Paylaşım Güvenliği: Value Object’ler paylaşılabilir olmalıdır. Aynı Money(100, TRY) nesnesi, birden fazla OrderItem tarafından referans alınabilir. Ama bu paylaşım, yalnızca nesne immutable olduğunda güvenlidir. Mutable bir Money nesnesi paylaşıldığında, birinin değiştirmesi diğerini de etkiler — bu, son derece sinsi ve bulmak zor bir hata kaynağıdır.
Neden 2 — HashSet/HashMap Güvenliği: Mutable bir nesneyi bir HashSet'e koyar, sonra değiştirirseniz, artık onu bulamamazsınız — çünkü hashCode değişmiştir ama Set'in iç yapısı eski hashCode'a göre organize edilmiştir. Immutable Value Object'lerde bu risk yoktur.
Neden 3 — Temporal Reasoning Kolaylığı: Immutable nesneler hakkında zamana bağlı düşünmek gerekmez. “Bu adres şu an ne durumda?” sorusu yoktur — adres her zaman oluşturulduğu andaki adrestir. Değişmişse, yeni bir nesne oluşturulmuştur.
Neden 4 — Thread Safety: Immutable nesneler doğası gereği thread-safe’dir. Aynı Money nesnesini birden fazla thread okuyabilir; senkronizasyona gerek yoktur.
3.2 Java 17 ile Value Object: record Anahtar Kelimesi

Java 16'da stable hale gelen record anahtar kelimesi, Value Object implementasyonunu dramatik biçimde basitleştirmektedir. Bir record, otomatik olarak şunları sağlar: immutable alanlar (final), equals() (tüm alanlara göre), hashCode() (tüm alanlara göre), toString() ve compact constructor (validation için).
// ─────────────────────────────────────────────────────────────────
// MONEY — Para birimi farkındalığı olan Value Object
// ─────────────────────────────────────────────────────────────────
public record Money(BigDecimal amount, Currency currency) {
// Compact constructor - validation burada
public Money {
Objects.requireNonNull(amount, "Para miktarı null olamaz");
Objects.requireNonNull(currency, "Para birimi null olamaz");
if (amount.scale() > 2) {
throw new InvalidMoneyException(
"Para miktarı en fazla 2 ondalık basamak içerebilir: " + amount
);
}
// Miktarı normalize et (trailing zero'ları temizle)
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
// ─── Factory methods ───────────────────────────────────────
public static Money of(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
public static Money ofTRY(BigDecimal amount) {
return new Money(amount, Currency.TRY);
}
public static Money ofTRY(String amount) {
return ofTRY(new BigDecimal(amount));
}
public static final Money ZERO_TRY = ofTRY(BigDecimal.ZERO);
// ─── Aritmetik operasyonlar - her biri yeni nesne döner ────
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) {
assertSameCurrency(other);
BigDecimal result = this.amount.subtract(other.amount);
if (result.compareTo(BigDecimal.ZERO) < 0) {
throw new NegativeMoneyException(
"Para miktarı negatif olamaz. Sonuç: " + result
);
}
return new Money(result, this.currency);
}
public Money multiply(BigDecimal multiplier) {
return new Money(
this.amount.multiply(multiplier).setScale(2, RoundingMode.HALF_UP),
this.currency
);
}
public Money reduceBy(Percentage percentage) {
BigDecimal reduction = this.amount.multiply(percentage.value())
.divide(BigDecimal.valueOf(100), 2, HALF_UP);
return new Money(this.amount.subtract(reduction), this.currency);
}
// ─── Karşılaştırma ─────────────────────────────────────────
public boolean isGreaterThan(Money other) {
assertSameCurrency(other);
return this.amount.compareTo(other.amount) > 0;
}
public boolean isGreaterThanOrEqual(Money other) {
assertSameCurrency(other);
return this.amount.compareTo(other.amount) >= 0;
}
public boolean isPositive() {
return this.amount.compareTo(BigDecimal.ZERO) > 0;
}
public boolean isZero() {
return this.amount.compareTo(BigDecimal.ZERO) == 0;
}
// ─── Yardımcı ──────────────────────────────────────────────
private void assertSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException(
String.format(
"Farklı para birimleri toplanamaz: %s + %s",
this.currency, other.currency
)
);
}
}
@Override
public String toString() {
return String.format("%s %s", amount.toPlainString(), currency.code());
}
}
// ─────────────────────────────────────────────────────────────────
// ADDRESS - Teslimat adresi Value Object
// ─────────────────────────────────────────────────────────────────
public record ShippingAddress(
String recipientName,
String phoneNumber,
String streetAddress,
String district,
String city,
String postalCode,
String country
) {
public ShippingAddress {
Objects.requireNonNull(recipientName, "Alıcı adı zorunludur");
Objects.requireNonNull(city, "Şehir zorunludur");
Objects.requireNonNull(country, "Ülke zorunludur");
if (recipientName.isBlank()) {
throw new InvalidAddressException("Alıcı adı boş olamaz");
}
if (phoneNumber != null && !phoneNumber.matches("^(\\+90|0)?[0-9]{10}$")) {
throw new InvalidPhoneNumberException(
"Geçersiz telefon numarası formatı: " + phoneNumber
);
}
}
// Adres değişince yeni nesne - mutation yok
public ShippingAddress withCity(String newCity) {
return new ShippingAddress(
recipientName, phoneNumber, streetAddress,
district, newCity, postalCode, country
);
}
public ShippingAddress withDistrict(String newDistrict) {
return new ShippingAddress(
recipientName, phoneNumber, streetAddress,
newDistrict, city, postalCode, country
);
}
// Formatlanmış tam adres
public String formatted() {
return String.format(
"%s\n%s%s%s, %s %s",
recipientName,
streetAddress != null ? streetAddress + "\n" : "",
district != null ? district + ", " : "",
city,
postalCode != null ? postalCode : "",
country
);
}
}
3.3 Value Object Koleksiyonu: Birden Fazla VO Bir Arada

Value Object’ler tek başlarına güçlüdür, ama birbirleriyle birleştiğinde domain’in çok daha zengin ve anlamlı bir ifadesini sağlarlar.
Bu diyagram, tek bir Order Entity'sinin içinde yaşayan tüm Value Object'leri ve Entity'leri görsel olarak haritalamaktadır. Dikkat çeken nokta şudur: Order Entity'sinin büyük bölümü Value Object'lerden oluşmaktadır. OrderId ve CustomerId birer typed ID'dir. Money nesneleri alt toplam, indirim, kargo ücreti ve toplam tutarı temsil eder — her biri immutable ve bağımsız. ShippingAddress ve BillingAddress teslimat ve fatura adreslerini kapsüller. OrderStatus mevcut durumu temsil eder. İçteki OrderItem'lar ise Entity'dir — çünkü her birinin kimliği ve sürekliliği önemlidir. Bu yapı, Order Entity'sinin hem zengin iş anlambilimi taşıdığını hem de her kavramın kendi sorumluluk sınırında yaşadığını göstermektedir.
4. GoF Flyweight Pattern ve Value Object İlişkisi
4.1 Flyweight Pattern Nedir?

GoF’un Flyweight pattern’i, çok sayıda küçük nesnenin bellek maliyetini azaltmak için içsel (intrinsic) state’i paylaşılan havuzda tutan, dışsal (extrinsic) state’i ise kullanıcıya bırakan bir pattern’dir.
Klasik örnek: bir metin işlemcisinde her karakter için ayrı bir nesne oluşturmak yerine, her harf için tek bir Character nesnesi oluşturup bunu paylaşmak. 'A' harfi, metinde kaç kez geçerse geçsin, bellekte yalnızca bir kez tutulur.
Bu diyagram, Flyweight pattern’in temel mekanizmasını göstermektedir. Flyweight olmadan, aynı içerik ve özelliklere sahip üç nesne bellekte üç kez yer kaplar. Flyweight ile ise yalnızca bir nesne oluşturulur ve tüm referanslar bu tek nesneyi paylaşır. Dışsal state (pozisyon bilgisi gibi bağlama özgü bilgi) her referansın kendinde tutulur; içsel state (font, boyut, renk gibi paylaşılan bilgi) ise havuzda tek bir yerde yaşar.
4.2 Flyweight ile Value Object: Derin Bağ

Value Object ve Flyweight pattern arasındaki bağ, hem kavramsal hem de pratik düzeyde güçlüdür. Bu iki yapının paylaştığı dört temel özellik vardır:
Bu diyagram, Flyweight ve Value Object arasındaki üç ortak noktayı ve tek temel farkı göstermektedir. Her ikisi de paylaşılabilir, içsel state açısından değişmez ve kimlik yerine değerle tanımlanır. Temel fark motivasyondadır: Flyweight, hafıza optimizasyonu için tasarlanmıştır; Value Object ise bir domain kavramını ifade etmek için. Bu ayrım şunu göstermektedir: DDD, GoF’un teknik pattern’ini alıp domain motivasyonuyla yeniden tanımlamıştır. Value Object, Flyweight’in domain-aware, kavramsal evrimi olarak görülebilir.
4.3 Pratik Flyweight Uygulaması: Currency Havuzu

Bazı Value Object’ler gerçekten Flyweight gibi davranır — örneğin Currency:
// Currency — klasik Flyweight uygulaması
public final class Currency {
// Flyweight Factory: havuz üzerinden erişim
private static final Map<String, Currency> INSTANCES = new ConcurrentHashMap<>();
public static final Currency TRY = of("TRY", "Türk Lirası", "₺");
public static final Currency USD = of("USD", "Amerikan Doları", "$");
public static final Currency EUR = of("EUR", "Euro", "€");
private final String code;
private final String name;
private final String symbol;
private Currency(String code, String name, String symbol) {
this.code = code;
this.name = name;
this.symbol = symbol;
}
// Flyweight Factory Method: aynı kod için her zaman aynı instance
public static Currency of(String code, String name, String symbol) {
return INSTANCES.computeIfAbsent(code,
k -> new Currency(code, name, symbol));
}
public static Currency ofCode(String code) {
Currency currency = INSTANCES.get(code);
if (currency == null) {
throw new UnknownCurrencyException("Bilinmeyen para birimi: " + code);
}
return currency;
}
// Equality: reference equality yeterli - Flyweight garantisi
@Override
public boolean equals(Object obj) {
return this == obj; // Aynı instance ise aynı currency
}
@Override
public int hashCode() { return System.identityHashCode(this); }
public String code() { return code; }
public String symbol() { return symbol; }
@Override
public String toString() { return code; }
}
Bu Currency implementasyonu, Flyweight ve Value Object'in buluşmasının mükemmel bir örneğidir. Currency.TRY her çağrıldığında aynı instance döner. Binlerce Money nesnesi Currency.TRY'yi referans alsa da bellekte yalnızca tek bir Currency.TRY nesnesi yaşar. Bu hem hafıza verimliliği hem de domain anlamı taşıyan bir tasarımdır.
5. Sınır Vakalar: Zor Kararlar
5.1 “Adres Entity mi, Value Object mi?”

Bu soru, DDD topluluğunda en sık tartışılan sınır vakasıdır. Cevap: bağlama göre değişir.
Müşterinin adres defterini düşünelim. “Ev adresim”, “İş adresim” gibi kayıtlı adresleri olan bir müşteri, bu adresleri yönetmek istiyor. Bu adresleri düzenleyebiliyor, silebiliyor, birini varsayılan yapabiliyor. Bu durumda adresin kimliği önemlidir — hangi adres düzenlendi, hangi adres silindi? Bu bağlamda adres bir Entity’dir.
Ama sipariş içindeki teslimat adresi düşünelim. Sipariş verildiğinde müşterinin adresi kopyalanır ve siparişe eklenir. Bu adres, müşterinin daha sonra adresini değiştirmesinden etkilenmemelidir. Bu bağlamda adres bir Value Object’tir.
Bu diyagram, aynı gerçek dünya kavramının — adres — iki farklı Bounded Context’te nasıl farklı modellendiğini göstermektedir. Adres defteri context’inde adres bir Entity’dir çünkü kimliğinin takip edilmesi gerekir. Sipariş context’inde ise adres bir Value Object’tir çünkü sipariş anındaki değerin dondurulması ve değişmemesi gerekmektedir. Bu, DDD’nin en güçlü içgörülerinden birini yansıtmaktadır: aynı kavram, farklı context’lerde farklı şeydir.
5.2 Para Birimi Dönüşümü: Mutable mi, Yeni Nesne mi?

Bir başka sınır vaka: döviz dönüşümü. Money(100, USD)'yi Money(xxx, TRY)'ye çevirmek gerekiyor. Bunu nasıl modellemelisiniz?
// ❌ Yanlış — mutation ile dönüşüm
public class Money {
private BigDecimal amount; // mutable!
private Currency currency; // mutable!
public void convertTo(Currency targetCurrency, ExchangeRate rate) {
this.amount = this.amount.multiply(rate.rate());
this.currency = targetCurrency;
// Orijinal değer kayboldu! Kim 100 USD'yi 3200 TRY yaptı?
}
}
// ✅ Doğru - yeni nesne ile dönüşüm
public record Money(BigDecimal amount, Currency currency) {
public Money convertTo(Currency targetCurrency, ExchangeRate rate) {
if (this.currency.equals(targetCurrency)) return this; // Dönüşüm gerekmez
BigDecimal convertedAmount = this.amount
.multiply(rate.rate())
.setScale(2, HALF_UP);
return new Money(convertedAmount, targetCurrency);
// Orijinal Money(100, USD) dokunulmaz kaldı!
// Yeni Money(3200, TRY) döndü.
}
}
// Kullanım - zincirleme işlem açık ve iz bırakır
Money usdAmount = Money.ofUSD("100");
ExchangeRate rate = ExchangeRate.of(Currency.USD, Currency.TRY, new BigDecimal("32.0"));
Money tryAmount = usdAmount.convertTo(Currency.TRY, rate);
// usdAmount hâlâ Money(100, USD) - dokunulmamış
// tryAmount yeni Money(3200, TRY) - yeni nesne
6. Veritabanı Yansıması: Entity ve Value Object Nasıl Saklanır?
6.1 Entity → Tablo, Value Object → Kolon
Entity’ler veritabanında genellikle ayrı bir tablo olarak temsil edilirken, Value Object’ler sahibinin (Entity’nin) tablosunda kolonlar olarak temsil edilir. Bu iki strateji, JPA’da farklı annotasyonlarla hayata geçirilir.

Bu diyagram, Entity’ler ile Value Object’lerin veritabanına yansımasını göstermektedir. Order ve OrderItem, ayrı tablolara sahip Entity'lerdir. ShippingAddress ve BillingAddress Value Object'leri ise orders tablosunun kolonları olarak saklanmaktadır — ayrı bir tablo değil. Money Value Object'i de (amount + currency) her kullanıldığı yerde iki kolon olarak temsil edilmektedir. Bu yaklaşım, gereksiz JOIN'leri ortadan kaldırır ve veri okuma performansını artırır.
6.2 JPA ile Value Object: @Embeddable

JPA, Value Object’leri @Embeddable annotasyonu ile destekler. Ama dikkat: @Embeddable sınıf, JPA entity'sidir — Domain'deki Value Object değil. Bu ayrım kritiktir.
// ─────────────────────────────────────────────────────────────────
// INFRASTRUCTURE KATMANI — JPA entity'leri burada
// Domain Value Object'leri burada değil!
// ─────────────────────────────────────────────────────────────────
// Infrastructure: JPA Embeddable - Domain VO'sunun persistence karşılığı
@Embeddable
public class MoneyJpaEmbeddable {
@Column(name = "amount", nullable = false, precision = 19, scale = 2)
private BigDecimal amount;
@Column(name = "currency", nullable = false, length = 3)
private String currency;
// JPA için no-arg constructor
protected MoneyJpaEmbeddable() {}
public MoneyJpaEmbeddable(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
@Embeddable
public class ShippingAddressJpaEmbeddable {
@Column(name = "shipping_recipient", nullable = false)
private String recipientName;
@Column(name = "shipping_street")
private String streetAddress;
@Column(name = "shipping_district")
private String district;
@Column(name = "shipping_city", nullable = false)
private String city;
@Column(name = "shipping_postal")
private String postalCode;
protected ShippingAddressJpaEmbeddable() {}
// constructor, getters...
}
// Infrastructure: JPA Entity - Domain Entity'sinin persistence karşılığı
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id
@Column(name = "id", columnDefinition = "UUID")
private UUID id;
@Column(name = "customer_id", columnDefinition = "UUID", nullable = false)
private UUID customerId;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private OrderStatusJpa status;
// ShippingAddress VO → Embedded (prefix ile kolon adı)
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "recipientName",
column = @Column(name = "shipping_recipient")),
@AttributeOverride(name = "city",
column = @Column(name = "shipping_city"))
})
private ShippingAddressJpaEmbeddable shippingAddress;
// Money VO → Embedded (total_amount, total_currency kolonları)
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "amount",
column = @Column(name = "total_amount")),
@AttributeOverride(name = "currency",
column = @Column(name = "total_currency"))
})
private MoneyJpaEmbeddable totalAmount;
@OneToMany(mappedBy = "order", cascade = ALL, orphanRemoval = true)
private List<OrderItemJpaEntity> items = new ArrayList<>();
protected OrderJpaEntity() {}
// constructor, getters...
}
// Infrastructure: OrderMapper - Domain ↔ JPA dönüşümü
@Component
public class OrderMapper {
public Order toDomain(OrderJpaEntity entity) {
return Order.reconstitute(
new OrderId(entity.getId()),
new CustomerId(entity.getCustomerId()),
mapStatus(entity.getStatus()),
mapMoney(entity.getTotalAmount()),
mapShippingAddress(entity.getShippingAddress()),
entity.getItems().stream()
.map(this::toItemDomain)
.collect(toList()),
entity.getCreatedAt()
);
}
public OrderJpaEntity toEntity(Order domain) {
OrderJpaEntity entity = new OrderJpaEntity();
entity.setId(domain.id().value());
entity.setCustomerId(domain.customerId().value());
entity.setStatus(mapStatus(domain.status()));
entity.setTotalAmount(mapMoney(domain.totalAmount()));
entity.setShippingAddress(mapAddress(domain.shippingAddress()));
return entity;
}
private MoneyJpaEmbeddable mapMoney(Money money) {
return new MoneyJpaEmbeddable(money.amount(), money.currency().code());
}
private Money mapMoney(MoneyJpaEmbeddable embeddable) {
return Money.of(
embeddable.getAmount(),
Currency.ofCode(embeddable.getCurrency())
);
}
}
7. Yaygın Hatalar ve Anti-Pattern’ler
7.1 Hata Kataloğu

Bu diyagram, Entity ve Value Object kullanımında en sık karşılaşılan beş anti-pattern’i, kod örnekleri ve sonuçlarıyla birlikte göstermektedir. Her anti-pattern’in somut bir sonucu vardır: gereksiz JOIN maliyeti, habersiz state değişimi, runtime hata, thread güvensizliği veya dağınık domain mantığı. Bu anti-pattern’leri tanımak, code review süreçlerinde ve yeni kod yazarken kaçınılması gereken tuzakları önceden görmek için kritik öneme sahiptir.
8. Test Stratejisi: Entity ve Value Object Nasıl Test Edilir?

8.1 Value Object Test: Hızlı ve İzole
Value Object’ler, yazılması en kolay birim testleri sunar: hiçbir bağımlılık yoktur, mock’a gerek yoktur, sadece nesne oluştur ve davranışını doğrula.
class MoneyTest {
// ─── Eşitlik Testleri ────────────────────────────────────────
@Test
void ayni_deger_ve_para_birimi_esit_olmali() {
Money money1 = Money.ofTRY("100.00");
Money money2 = Money.ofTRY("100.00");
assertThat(money1).isEqualTo(money2);
assertThat(money1.hashCode()).isEqualTo(money2.hashCode());
}
@Test
void farkli_para_birimleri_esit_olmamali() {
Money tryMoney = Money.of(new BigDecimal("100"), Currency.TRY);
Money usdMoney = Money.of(new BigDecimal("100"), Currency.USD);
assertThat(tryMoney).isNotEqualTo(usdMoney);
}
// ─── Immutability Testleri ───────────────────────────────────
@Test
void toplama_orijinal_nesneyi_degistirmemeli() {
Money original = Money.ofTRY("100.00");
Money added = original.add(Money.ofTRY("50.00"));
// Orijinal değişmemiş
assertThat(original.amount()).isEqualByComparingTo("100.00");
// Yeni nesne doğru değeri taşıyor
assertThat(added.amount()).isEqualByComparingTo("150.00");
// İki ayrı nesne
assertThat(original).isNotSameAs(added);
}
// ─── Validation Testleri ─────────────────────────────────────
@Test
void null_amount_exception_firlitmali() {
assertThatThrownBy(() -> new Money(null, Currency.TRY))
.isInstanceOf(NullPointerException.class)
.hasMessageContaining("Para miktarı null olamaz");
}
@Test
void farkli_para_birimi_toplama_exception_firlitmali() {
Money tryMoney = Money.ofTRY("100.00");
Money usdMoney = Money.of(new BigDecimal("50"), Currency.USD);
assertThatThrownBy(() -> tryMoney.add(usdMoney))
.isInstanceOf(CurrencyMismatchException.class);
}
// ─── İş Kuralı Testleri ──────────────────────────────────────
@Test
void indirim_uygulamasi_dogru_hesaplamali() {
Money originalPrice = Money.ofTRY("200.00");
Percentage discount = Percentage.of(10); // %10
Money discountedPrice = originalPrice.reduceBy(discount);
assertThat(discountedPrice.amount()).isEqualByComparingTo("180.00");
assertThat(discountedPrice.currency()).isEqualTo(Currency.TRY);
}
}
8.2 Entity Test: İş Kurallarına Odaklanma
Entity testleri, iş kurallarını ve durum geçişlerini doğrular. Bağımlılıklar mock’lanır ama entity’nin kendisi gerçektir.
class OrderTest {
private Order buildPendingOrder() {
return Order.create(
OrderId.generate(),
new CustomerId(UUID.randomUUID()),
List.of(
new OrderItem(ProductId.generate(), Quantity.of(1), Money.ofTRY("250.00"))
),
new ShippingAddress("Ali Yılmaz", null, "Moda Cad.", "Kadıköy", "İstanbul", "34710", "TR")
);
}
// ─── State Geçiş Testleri ────────────────────────────────────
@Test
void pending_siparis_iptal_edilebilmeli() {
Order order = buildPendingOrder();
List<DomainEvent> events = order.cancel(
CancellationReason.CUSTOMER_REQUEST
);
assertThat(order.status()).isEqualTo(OrderStatus.CANCELLED);
assertThat(events).hasSize(1);
assertThat(events.get(0)).isInstanceOf(OrderCancelledEvent.class);
}
@Test
void shipped_siparis_iptal_edilememeli() {
Order order = buildPendingOrder();
order.confirmPayment(PaymentId.generate());
order.dispatch(TrackingCode.of("TY123456"));
assertThatThrownBy(() -> order.cancel(CancellationReason.CUSTOMER_REQUEST))
.isInstanceOf(OrderCannotBeCancelledException.class)
.hasMessageContaining("SHIPPED");
}
// ─── Kimlik Testi ────────────────────────────────────────────
@Test
void ayni_id_ile_farkli_state_olan_siparisler_esit_olmali() {
OrderId sharedId = OrderId.generate();
Order order1 = Order.reconstitute(sharedId, /* PENDING ... */);
Order order2 = Order.reconstitute(sharedId, /* SHIPPED ... */);
// Aynı ID → eşit (state farklı olsa bile)
assertThat(order1).isEqualTo(order2);
assertThat(order1.hashCode()).isEqualTo(order2.hashCode());
}
@Test
void farkli_id_ile_ayni_state_olan_siparisler_esit_olmamali() {
Order order1 = buildPendingOrder();
Order order2 = buildPendingOrder();
// Farklı ID → eşit değil (state aynı olsa bile)
assertThat(order1).isNotEqualTo(order2);
}
}
9. Trendyol Örneği: E-Ticaret Sisteminde Entity ve Value Object Haritası
9.1 Trendyol Domain’inde Tüm Entity ve VO’lar

Bu diyagram, Trendyol’un dört ana Bounded Context’inde yaşayan Entity ve Value Object’lerin kapsamlı bir haritasını sunmaktadır. Her context’te Aggregate Root olarak işaretlenen Entity’ler (AR) özellikle dikkat çekmektedir: bunlar her context’in dışarıyla iletişim kurduğu tek giriş noktasıdır. Value Object listelerinde dikkat çeken nokta, Money'nin hem Sipariş hem de Ödeme context'inde bağımsız olarak tanımlandığıdır — her context kendi Money'sini tanımlar ve bunlar birbirinden bağımsızdır. Quantity hem Sipariş hem de Stok context'inde var olur ama her biri kendi validasyonunu ve iş kuralını taşır.
10. Sonuç: İki Varoluş Biçiminin Mimari Önemi

Bu makalede Entity ve Value Object arasındaki ayrımın yalnızca teknik bir tasarım kararı olmadığını gördük. Bu ayrım, sisteminizin iş dünyasını ne kadar doğru anladığının ve ne kadar doğru ifade ettiğinin bir göstergesidir.
Bir siparişin neden Entity olduğunu anlayan geliştirici, o siparişe karşı her teknik kararı — kimlik yönetimi, veritabanı tasarımı, eşitlik semantiği, test stratejisi — tutarlı biçimde verir. Bir para değerinin neden Value Object olduğunu anlayan geliştirici, immutability’yi zorunluluk olarak değil, doğal bir ifade biçimi olarak benimser.
GoF’un Flyweight pattern’inin bu ayrımla olan bağı ise şunu gösterir: yazılım mühendisliğinin teknik zemin üzerine kurduğu yapılar, zamanla domain’in ihtiyaçlarına göre evrilir. Flyweight hafıza optimizasyonu için doğdu; Value Object ise aynı temel içgörüyü — paylaşılabilirlik ve değişmezlik — domain kavramlarını ifade etmek için kullandı.
Bu serinin ilerleyen makalelerinde, Entity ve Value Object’lerin daha büyük yapılara — Aggregate’lere — nasıl birleştiğini inceleyeceğiz. Aggregate, Entity ve Value Object’lerin tutarlılık garantisiyle bir arada yaşadığı en güçlü DDD yapısıdır.
Serinin Bir Sonraki Makalesinde
**Makale 5: Aggregate — Tutarlılığın Mimarı**
Birden fazla Entity ve Value Object’i bir arada yönetmek, tutarlılık problemlerini beraberinde getirir. Bir Order siparişine yeni bir kalem eklendiğinde stok kontrolü kim yapar? İptal edildiğinde hangi nesneler birlikte değişmeli? Aggregate pattern, bu soruların cevabını "consistency boundary" kavramıyla verir. GoF'un Facade ve Composite pattern'lerinin domain-aware evrimi olan Aggregate'i, Aggregate Root'un tek giriş noktası kuralını ve optimistic locking ile versioning konularını derinlemesine inceliyoruz.
**İçindekiler… « Önceki [Makale 3: Ubiquitous Language — Ortak Dilin İnşası] » Sonraki **[Makale 5: Aggregate — Tutarlılığın Kalesi]
메타데이터
- post_id
- 53592d725319
- slug
- dddnin-kalbinde-design-patterns-makale-4-entity-ve-value-object-kimlik-ve-değer-53592d725319
- url
- https://medium.com/@sahinyelkenci/dddnin-kalbinde-design-patterns-makale-4-entity-ve-value-object-kimlik-ve-de%C4%9Fer-53592d725319
- canonical_url
- https://medium.com/@sahinyelkenci/dddnin-kalbinde-design-patterns-makale-4-entity-ve-value-object-kimlik-ve-de%C4%9Fer-53592d725319
- author_url
- https://medium.com/@sahinyelkenci
- status
- ok
- fetched_at
- 2026-06-09 15:37:30