Definition
- Entity – has a unique identity and mutable lifecycle; equality is by ID.
- Value Object – no identity, immutable, compared by value; Java record is the ideal mapping.
- Aggregate – cluster of entities and value objects treated as one consistency unit, accessed through its root.
- Repository – abstracts persistence for one aggregate root.
- Domain Service – stateless domain logic spanning entities; Application Service orchestrates use cases.
- Strategic – Bounded Context, Ubiquitous Language, Context Mapping, Domain Events organize across services.
At National Bank of India, an Entity is Rajesh's account: it has a unique number and its balance changes. A Value Object is an amount of money: one 50,000-rupee note is interchangeable with any other. An Aggregate is a passbook treated as one unit: changes go through the front cover (root). A Repository is the filing clerk who fetches and stores passbooks. A Domain Service is the interest calculator. An Application Service is the bank officer who runs the whole transfer procedure.
Understanding the Concepts
What is an Entity?
An Entity has a unique identity and a lifecycle. Two accounts with the same balance are still different accounts because they have different IDs.
package com.bank.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.util.Objects;
@Entity
public class Account {
@Id
private String accountId; // identity
private String holderName;
private BigDecimal balance;
@Version
private Long version;
public void debit(BigDecimal amount) {
if (balance.compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient funds");
}
balance = balance.subtract(amount);
}
public void credit(BigDecimal amount) {
balance = balance.add(amount);
}
// Equality by identity, not by field values.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Account a)) return false;
return Objects.equals(accountId, a.accountId);
}
@Override
public int hashCode() {
return Objects.hash(accountId);
}
}
Account SAV2001 (balance 500000) != Account SAV2002 (balance 500000)
Same balance, different identity -> different entities.
What is a Value Object?
A Value Object has no identity, is immutable, and is compared by value. Java record is the ideal mapping.
package com.bank.model;
import java.math.BigDecimal;
// Value Object: immutable, no identity, value-based equality.
// Java record gives all of this for free.
public record Money(BigDecimal amount, String currency) {
public Money {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount must be non-negative");
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException("Currency is required");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot subtract different currencies");
}
return new Money(this.amount.subtract(other.amount), this.currency);
}
}
new Money(new BigDecimal("50000"), "INR")
.equals(new Money(new BigDecimal("50000"), "INR")) -> true
Value Objects prevent bugs:
WRONG: account.debit(new BigDecimal("50000")) // what currency?
CORRECT: account.debit(new Money(new BigDecimal("50000"), "INR"))
What is an Aggregate?
An Aggregate is a cluster of entities and value objects treated as one consistency unit, accessed only through its root.
package com.bank.model;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class LoanAccount { // Aggregate Root
private final String loanId;
private BigDecimal principal;
private final List<EmiPayment> payments = new ArrayList<>(); // child entities
public LoanAccount(String loanId, BigDecimal principal) {
this.loanId = loanId;
this.principal = principal;
}
// All changes go through the root.
public void recordEmiPayment(Money amount) {
principal = principal.subtract(amount.amount());
payments.add(new EmiPayment(loanId, amount, payments.size() + 1));
}
public List<EmiPayment> getPayments() {
return Collections.unmodifiableList(payments);
}
// Never access EmiPayment directly. Always through LoanAccount root.
}
Aggregate = LoanAccount (root) + EmiPayment (child entities)
Invariant: principal decreases correctly with each EMI.
Access: only through root methods.
What is a Repository?
A Repository abstracts persistence for one aggregate root. One repository per aggregate root.
package com.bank.repository;
import com.bank.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
// One repository per aggregate root.
// There is NO EmiPaymentRepository. Access EMI through LoanAccountRepository.
public interface AccountRepository extends JpaRepository<Account, String> {
// findById loads the entire Account aggregate.
// save persists the entire Account aggregate.
}
Rule: one repository per aggregate root.
AccountRepository -> loads/saves Account aggregate.
LoanAccountRepository -> loads/saves LoanAccount aggregate (includes EMI payments).
NEVER: EmiPaymentRepository (not an aggregate root).
What is a Domain Service vs Application Service?
A Domain Service holds stateless domain logic spanning entities. An Application Service orchestrates the use case.
package com.bank.domain;
import com.bank.model.Money;
import java.math.BigDecimal;
import org.springframework.stereotype.Service;
// Domain Service: stateless domain logic that spans entities.
@Service
public class InterestCalculator {
public Money calculateInterest(BigDecimal principal, BigDecimal rate, int months) {
BigDecimal interest = principal.multiply(rate)
.multiply(BigDecimal.valueOf(months))
.divide(BigDecimal.valueOf(1200), 2, java.math.RoundingMode.HALF_UP);
return new Money(interest, "INR");
}
}
package com.bank.application;
import com.bank.domain.InterestCalculator;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
// Application Service: orchestrates the use case. No business rules here.
@Service
public class TransferApplicationService {
private final AccountRepository accountRepo;
private final InterestCalculator interestCalc;
public TransferApplicationService(AccountRepository accountRepo,
InterestCalculator interestCalc) {
this.accountRepo = accountRepo;
this.interestCalc = interestCalc;
}
@Transactional
public void transfer(String fromId, String toId, BigDecimal amount) {
var from = accountRepo.findById(fromId).orElseThrow();
var to = accountRepo.findById(toId).orElseThrow();
from.debit(amount); // business rule inside entity
to.credit(amount); // business rule inside entity
accountRepo.save(from);
accountRepo.save(to);
}
}
Domain Service: contains business rules (interest calculation).
Application Service: orchestrates (load, call, save). NO business rules.
Entities: enforce their own invariants (balance >= 0).
What are the strategic DDD concepts?
Bounded Context, Ubiquitous Language, Context Mapping, and Domain Events organize across services.
Strategic concepts:
Bounded Context: boundary where one model is consistent.
Deposits: Account = savings account
Lending: Account = loan account
Ubiquitous Language: team uses same words in code, docs, conversation.
Lending team says "sanction", "EMI", "tenure".
Context Mapping: how contexts relate.
Shared Kernel: small shared model
Customer-Supplier: one depends on another
Anti-Corruption Layer: translation at border
Domain Events: decouple contexts.
Deposits publishes FundsDebited.
Lending reacts without sharing Deposits model.
How do tactical and strategic concepts relate?
Tactical blocks keep one service's model clean. Strategic concepts stop many services from collapsing into a shared model.
| Level | Concept | Purpose |
|---|---|---|
| Tactical | Entity, Value Object, Aggregate | Model inside one service |
| Tactical | Repository, Domain Service, App Service | Persist and orchestrate |
| Strategic | Bounded Context, Ubiquitous Language | Organize across services |
| Strategic | Context Mapping, Domain Events | Communicate between contexts |
What should a senior engineer know?
Application Services contain no business rules; the rules live in entities and domain services. Value Objects eliminate a whole class of bugs by making invalid states unrepresentable. The strategic concepts are what prevent ten services from collapsing back into one tangled shared model.
Visual Overview
DDD Building Blocks Layered:
┌─────────────────────────────────────────────┐
│ APPLICATION SERVICE (orchestrates use case) │
│ @Service @Transactional │
│ transfer(fromId, toId, amount) │
└──────────────────┬──────────────────────────┘
│ calls
┌──────────────────┴──────────────────────────┐
│ DOMAIN SERVICE (cross-entity business rules) │
│ InterestCalculator.calculateInterest() │
└──────────────────┬──────────────────────────┘
│ calls
┌──────────────────┴──────────────────────────┐
│ AGGREGATE (consistency boundary) │
│ Account (root) -> enforces balance >= 0 │
│ ├── Money (value object, immutable) │
│ └── Transaction (child entity) │
└──────────────────┬──────────────────────────┘
│ persisted by
┌──────────────────┴──────────────────────────┐
│ REPOSITORY (one per aggregate root) │
│ AccountRepository extends JpaRepository │
└─────────────────────────────────────────────┘
Key Takeaway
Domain-Driven Design gives tactical building blocks for modeling inside a single service and strategic concepts for organizing across services. On the tactical side, an Entity has identity (Account), a Value Object is immutable and compared by value (Money as a Java record), an Aggregate is a consistency unit accessed through its root, a Repository persists one aggregate root, a Domain Service holds stateless cross-entity logic, and an Application Service orchestrates use cases without containing business rules.
At National Bank of India, the Account entity enforces the never-negative invariant through its debit method. Money as a Value Object prevents currency-mixing bugs. The Application Service loads accounts, calls their methods, saves them, and publishes events, but never contains business rules itself.
Strategically, Bounded Context ensures each service has one consistent model, Ubiquitous Language keeps the team's words precise, Context Mapping defines how contexts relate, and Domain Events decouple communication. Together, the tactical blocks keep one service clean and the strategic concepts stop many services from collapsing back into a tangled shared model.
Remember one sentence: Entity has identity, Value Object has value, Aggregate has consistency, Repository has persistence, and the strategic concepts keep services from collapsing into a shared model.
---