Projelerimde iş kurallarını ve bu kuralların kontrollerini her yeni servis metodunda tekrar tekrar yazdığımı fark ettim. Login kontrolü burada, ban kontrolü şurada, aynı validasyon farklı yerlerde. Bunun üzerine biraz araştırma yapınca daha önce tasarlanmış bir yaklaşım olduğunu gördüm.
Bu yaklaşımın iki farklı popüler yöntemi var: Anemic Domain Model ve Rich Domain Model.
Bu yazıda ikisini karşılaştıracağım. Hangisinin ne zaman mantıklı olduğunu, Spring Boot'ta nasıl uygulandığını gerçek örneklerle tartışmaya çalışacağım.
Örnek vermek gerekirse, bir kullanıcının login işlemlerini yöneten bir metodumuzda ban vb kontrolleri yapmamız gereksin:
public void login(String email, String password) {
User user = userRepository.findByEmail(email);
if (user.isBanned()) throw new BusinessException("Account is banned");
if (!user.isEnabled()) throw new BusinessException("Account is not active");
if (user.getLockoutTime() != null && user.getLockoutTime().isAfter(LocalDateTime.now())) {
throw new BusinessException("Account is locked");
}
// password check...
}
Sonra başka bir yerde de bu kontrolleri yapmamız gerekecek, mecburen aynı kontrolleri tekrar yazıyorsun:
public void updateProfile(Long userId) {
User user = userRepository.findById(userId);
if (user.isBanned()) throw new BusinessException("Account is banned");
if (!user.isEnabled()) throw new BusinessException("Account is not active");
// profile update...
}
Aynı kontroller iki yerde. Yarın yeni bir kural gelirse iki yeri de değiştirmem gerekiyor. Proje büyüdükçe bu sayı da doğal olarak artıyor.
Bu aslında rastgele bir hata değil. İki farklı tasarım yaklaşımının sonucu. Bunları anlamak için önce Domain Driven Design'dan kısaca bahsetmek gerekiyor.
Domain Driven Design (DDD) nedir?
DDD (Domain Driven Design), yazılımı iş mantığının etrafında tasarlama yaklaşımı diye özetleniyor. Eric Evans'ın 2003'te yazdığı kitapla popülerleşmiş. Temel fikir, kodun gerçek dünyadaki iş süreçlerini yansıtması gerektiği.
Örneğin bir e-ticaret uygulaması geliştiriyoruz.
![]()
Her adımın kendi kuralı var. DDD diyor ki: bu kuralları kodda da aynı şekilde yansıt.
Bu yaklaşım içinde iki farklı popüler model ortaya çıkmış. Bunlar yazının başlığında yer alan Anemic Domain Model ve Rich Domain Model. İkisi de aynı altyapıdan çıkan ama birbirinden farklı düşünce ve yaklaşım biçimleri.
İlk olarak Anemic Domain Model'i ele alalım.
Anemic Domain Model nedir?
Anemic Domain Model'de entity sadece veri taşır. İçinde getter ve setter vardır, başka bir şey yoktur. Yani tüm iş kuralları service katmanında.
Spring Boot'ta şöyle görünür:
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private String displayName;
private boolean isEnabled;
private boolean isBanned;
private int failedLoginAttempts;
private LocalDateTime lockoutTime;
}
@Data Lombok anotasyonu tüm field'lara otomatik getter ve setter üretiyor. Entity sadece veri taşıyor, başka bir şey yapmıyor.
Peki kurallar nerede? Service'te:
@Service
public class UserService {
public void login(String email, String password) {
User user = userRepository.findByEmail(email);
if (user.isBanned()) throw new BusinessException("Account is banned");
if (!user.isEnabled()) throw new BusinessException("Account is not active");
if (user.getLockoutTime() != null &&
user.getLockoutTime().isAfter(LocalDateTime.now())) {
throw new BusinessException("Account is locked");
}
// password check...
}
public void updateProfile(Long userId) {
User user = userRepository.findById(userId);
if (user.isBanned()) throw new BusinessException("Account is banned");
if (!user.isEnabled()) throw new BusinessException("Account is not active");
// profile update...
}
}
Aynı kontroller iki yerde. Proje büyüdükçe bu sayı artacak.
Bir de şu var — @Data olduğu için dışarıdan herkes şunu yapabilir:
user.setBanned(false); // ban kalktı, kural yok
user.setEnabled(true); // hesap aktif oldu, kural yok
user.setFailedLoginAttempts(0); // sayaç sıfırlandı, kural yok
Kim isterse istediği değeri set edebilir.
Yine de Anemic "kötü" denemez diyerek konudan sıyrılamayız. Çoğu zaman farkında olmadan kullanılan ve zamanla sorun çıkarabilen bir yaklaşım bu. Farkında olmadan diyorum çünkü tutorial'ların çoğunda bu öğretiliyor ve bir çoğumuz oradan gördüklerimizi uyguluyoruz. Bu aslında bir tercih meselesi, ancak ekip büyüdükçe ve özellikle yeni geliştiricilere işleyiş yeterli seviyede anlatılmazsa problem yaratabilecek bir yaklaşım.
Bu kısımdan sonra Anemic Domain Model'i (ADM) olarak kısaltacağım.
Rich Domain Model nedir?
Rich Domain Model'de entity ADM'dekinin tersine sadece veri taşımaz, kendi kurallarını da bilir. İş mantığı entity'nin içinde yer alır.
Aynı User entity'sini Rich ile yazsaydık şöyle görünürdü:
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private String displayName;
private boolean isEnabled;
private boolean isBanned;
private int failedLoginAttempts;
private LocalDateTime lockoutTime;
public User(String email, String password, String displayName) {
this.email = email;
this.password = password;
this.displayName = displayName;
this.isEnabled = false;
this.isBanned = false;
this.failedLoginAttempts = 0;
}
public void activate() {
if (this.isEnabled) throw new BusinessException("Account is already active");
this.isEnabled = true;
}
public void ban() {
if (this.isBanned) throw new BusinessException("Account is already banned");
this.isBanned = true;
}
public void recordFailedLogin() {
this.failedLoginAttempts++;
if (this.failedLoginAttempts >= 5) {
this.lockoutTime = LocalDateTime.now().plusMinutes(30);
}
}
public boolean canLogin() {
if (isBanned) return false;
if (!isEnabled) return false;
if (lockoutTime != null && lockoutTime.isAfter(LocalDateTime.now())) return false;
return true;
}
}
@Getter var ama @Setter yok. Dışarıdan setBanned(false) diyemeyiz. Sadece ban() metoduyla kural içinden değiştirilebilir.
Service artık daha temiz çünkü kontrolleri entity içine yazdık:
@Service
public class UserService {
public void login(String email, String password) {
User user = userRepository.findByEmail(email);
if (!user.canLogin()) throw new BusinessException("Account is not accessible");
// password check...
}
public void updateProfile(Long userId) {
User user = userRepository.findById(userId);
if (!user.canLogin()) throw new BusinessException("Account is not accessible");
// profile update...
}
}
Aynı kontrol artık tek yerde — canLogin() metodunda. Kim çağırırsa çağırsın kural çalışıyor. Yani tek tek "bu kullanıcı banlı mı, hesabı aktif mi" gibi farklı kontrolleri aklımızda tutmamıza gerek kalmıyor ve istemeden bir şeyi atlama riskini de engelliyoruz. Yarın yeni bir kontrol eklenmesi gerekirse sadece entity sınıfında ilgili kısıma ekleme yapıyoruz. Bu şekilde hem service şişmiyor, hem istemeden kural atlanamıyor, hem de iş yükümüz azalıyor.
Örneğin yarın "email doğrulanmamış kullanıcılar giriş yapamaz" kuralı eklememizsi gerekirse, tek yapmamız gereken entity içine ilgili eklemeleri yaptıktan sonra canLogin() metoduna şu satırı eklemek olacak:
public boolean canLogin() {
if (isBanned) return false;
if (!isEnabled) return false;
if (!isEmailVerified) return false; // yeni kural
if (lockoutTime != null && lockoutTime.isAfter(LocalDateTime.now())) return false;
return true;
}
Service'te hiçbir şey değişmedi. canLogin() çağıran her yer otomatik olarak bu kuralı da uygulamış olacak.
Bu iki yaklaşımın pozitif ve negatif yanlarını özetlemek gerekirse:
| Anemic Domain Model | Rich Domain Model | |
|---|---|---|
| Entity ne yapar | Sadece veri taşır | Veri + iş kuralları |
| Kural nerede | Service'te | Entity'de |
| Kural atlanabilir mi | Unutursak evet | Hayır |
| Service şişer mi | Zamanla evet | Hayır |
Özet
Anemic Domain Model'de entity sadece veri taşır, kurallar service'te yer alır. Rich Domain Model'de ise entity kendi kurallarını bilir, service sadece süreçle ilgilenir.
Önemli olan farkını bilerek tercih etmek diye düşünüyorum. Ben genellikle farkında olmadan Anemic yazmışım. Ama artık neyi neden yapacağımı ve alternatifinin ne olduğunu biliyorum. Dilerim okuyanlara da faydalı olmuştur.
Emirhan Kaya - Backend Software Developer
LinkedIn · GitHub · tremirhankaya@gmail.com
Top comments (0)