Entity and Value Object are fundamental concepts in Domain-Driven Design (DDD) that help model business domains effectively. Entities have identity and lifecycle, while Value Objects are immutable and describe characteristics.
Entity Modeling
1. Base Entity Class
import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;
// Base class for all entities
public abstract class Entity<ID> implements Serializable {
protected final ID id;
protected Entity(ID id) {
this.id = Objects.requireNonNull(id, "Entity ID cannot be null");
}
public ID getId() {
return id;
}
// Identity is defined by ID
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entity<?> entity = (Entity<?>) o;
return Objects.equals(id, entity.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return getClass().getSimpleName() + "{id=" + id + "}";
}
}
// Entity with UUID as identity
public abstract class BaseEntity extends Entity<UUID> {
protected BaseEntity(UUID id) {
super(id);
}
protected BaseEntity() {
this(UUID.randomUUID());
}
public static UUID generateId() {
return UUID.randomUUID();
}
}
// Entity with Long as identity (for database auto-increment)
public abstract class LongEntity extends Entity<Long> {
protected LongEntity(Long id) {
super(id);
}
protected LongEntity() {
this(null); // ID will be set by persistence layer
}
// For entities where ID is generated by database
public boolean isNew() {
return id == null;
}
}
2. Customer Entity Example
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
// Aggregate Root Entity
public class Customer extends BaseEntity {
private String email;
private String name;
private CustomerStatus status;
private final Address address;
private final Set<Order> orders;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Constructor for new customer
public Customer(String email, String name, Address address) {
super();
this.email = validateEmail(email);
this.name = validateName(name);
this.address = Objects.requireNonNull(address, "Address cannot be null");
this.status = CustomerStatus.ACTIVE;
this.orders = new HashSet<>();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Constructor for existing customer (from persistence)
public Customer(UUID id, String email, String name, Address address,
CustomerStatus status, Set<Order> orders,
LocalDateTime createdAt, LocalDateTime updatedAt) {
super(id);
this.email = validateEmail(email);
this.name = validateName(name);
this.address = Objects.requireNonNull(address, "Address cannot be null");
this.status = status;
this.orders = new HashSet<>(orders);
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// Business operations
public void updateProfile(String name, Address newAddress) {
this.name = validateName(name);
this.address.updateFrom(newAddress);
this.updatedAt = LocalDateTime.now();
}
public void deactivate() {
if (this.status == CustomerStatus.ACTIVE) {
this.status = CustomerStatus.INACTIVE;
this.updatedAt = LocalDateTime.now();
}
}
public void activate() {
if (this.status == CustomerStatus.INACTIVE) {
this.status = CustomerStatus.ACTIVE;
this.updatedAt = LocalDateTime.now();
}
}
public Order placeOrder(Set<OrderItem> items) {
if (status != CustomerStatus.ACTIVE) {
throw new IllegalStateException("Cannot place order for inactive customer");
}
Order order = new Order(this, items);
orders.add(order);
updatedAt = LocalDateTime.now();
return order;
}
public void addOrder(Order order) {
if (!order.getCustomerId().equals(this.id)) {
throw new IllegalArgumentException("Order does not belong to this customer");
}
orders.add(order);
}
// Validation methods
private String validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalArgumentException("Invalid email format");
}
return email.trim().toLowerCase();
}
private String validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
String trimmed = name.trim();
if (trimmed.length() < 2 || trimmed.length() > 100) {
throw new IllegalArgumentException("Name must be between 2 and 100 characters");
}
return trimmed;
}
// Getters
public String getEmail() { return email; }
public String getName() { return name; }
public CustomerStatus getStatus() { return status; }
public Address getAddress() { return address; }
public Set<Order> getOrders() { return Collections.unmodifiableSet(orders); }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
// Business invariants
public boolean canPlaceOrder() {
return status == CustomerStatus.ACTIVE &&
!address.isIncomplete();
}
public double getTotalSpent() {
return orders.stream()
.filter(order -> order.getStatus() == OrderStatus.COMPLETED)
.mapToDouble(Order::getTotalAmount)
.sum();
}
}
enum CustomerStatus {
ACTIVE, INACTIVE, SUSPENDED
}
3. Order Entity (Aggregate Root)
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Order extends BaseEntity {
private final UUID customerId;
private OrderStatus status;
private final Set<OrderItem> items;
private Money totalAmount;
private ShippingAddress shippingAddress;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime completedAt;
public Order(Customer customer, Set<OrderItem> items) {
super();
this.customerId = customer.getId();
this.status = OrderStatus.PENDING;
this.items = new HashSet<>(validateItems(items));
this.totalAmount = calculateTotalAmount();
this.shippingAddress = customer.getAddress().toShippingAddress();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Constructor for existing order
public Order(UUID id, UUID customerId, OrderStatus status, Set<OrderItem> items,
Money totalAmount, ShippingAddress shippingAddress,
LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime completedAt) {
super(id);
this.customerId = customerId;
this.status = status;
this.items = new HashSet<>(validateItems(items));
this.totalAmount = totalAmount;
this.shippingAddress = shippingAddress;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.completedAt = completedAt;
}
// Business operations
public void addItem(OrderItem item) {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Cannot modify order in " + status + " status");
}
items.add(item);
recalculateTotal();
updatedAt = LocalDateTime.now();
}
public void removeItem(Product product) {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Cannot modify order in " + status + " status");
}
items.removeIf(item -> item.getProductId().equals(product.getId()));
recalculateTotal();
updatedAt = LocalDateTime.now();
}
public void updateShippingAddress(ShippingAddress newAddress) {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Cannot update shipping address in " + status + " status");
}
this.shippingAddress = newAddress;
updatedAt = LocalDateTime.now();
}
public void confirm() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Order cannot be confirmed from " + status + " status");
}
if (items.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
status = OrderStatus.CONFIRMED;
updatedAt = LocalDateTime.now();
}
public void complete() {
if (status != OrderStatus.CONFIRMED && status != OrderStatus.SHIPPED) {
throw new IllegalStateException("Order cannot be completed from " + status + " status");
}
status = OrderStatus.COMPLETED;
completedAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
public void cancel() {
if (status == OrderStatus.COMPLETED || status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Cannot cancel order in " + status + " status");
}
status = OrderStatus.CANCELLED;
updatedAt = LocalDateTime.now();
}
// Private methods
private Set<OrderItem> validateItems(Set<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item");
}
return items;
}
private void recalculateTotal() {
this.totalAmount = calculateTotalAmount();
}
private Money calculateTotalAmount() {
return items.stream()
.map(OrderItem::getLineTotal)
.reduce(Money.ZERO, Money::add);
}
// Getters
public UUID getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public Set<OrderItem> getItems() { return Collections.unmodifiableSet(items); }
public Money getTotalAmount() { return totalAmount; }
public ShippingAddress getShippingAddress() { return shippingAddress; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
// Business invariants
public boolean canBeModified() {
return status == OrderStatus.PENDING;
}
public boolean isCompleted() {
return status == OrderStatus.COMPLETED;
}
}
enum OrderStatus {
PENDING, CONFIRMED, PROCESSING, SHIPPED, COMPLETED, CANCELLED
}
Value Object Modeling
4. Base Value Object Class
import java.util.Objects;
// Base class for all Value Objects
public abstract class ValueObject implements Comparable<ValueObject> {
// Subclasses must implement these methods
protected abstract Object[] getEqualityComponents();
// Value Objects are equal if all their components are equal
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValueObject that = (ValueObject) o;
Object[] theseComponents = this.getEqualityComponents();
Object[] thoseComponents = that.getEqualityComponents();
if (theseComponents.length != thoseComponents.length) {
return false;
}
for (int i = 0; i < theseComponents.length; i++) {
if (!Objects.equals(theseComponents[i], thoseComponents[i])) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
Object[] components = getEqualityComponents();
return Objects.hash(components);
}
@Override
public int compareTo(ValueObject other) {
Object[] theseComponents = this.getEqualityComponents();
Object[] thoseComponents = other.getEqualityComponents();
if (theseComponents.length != thoseComponents.length) {
return Integer.compare(theseComponents.length, thoseComponents.length);
}
for (int i = 0; i < theseComponents.length; i++) {
Comparable thisComp = (Comparable) theseComponents[i];
Comparable thatComp = (Comparable) thoseComponents[i];
int comparison = thisComp.compareTo(thatComp);
if (comparison != 0) {
return comparison;
}
}
return 0;
}
@Override
public String toString() {
Object[] components = getEqualityComponents();
StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append("{");
for (int i = 0; i < components.length; i++) {
if (i > 0) sb.append(", ");
sb.append(components[i]);
}
return sb.append("}").toString();
}
}
5. Money Value Object
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
// Immutable Money Value Object
public final class Money extends ValueObject {
public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("USD"));
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = validateAmount(amount).setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
this.currency = Objects.requireNonNull(currency, "Currency cannot be null");
}
public Money(double amount, Currency currency) {
this(BigDecimal.valueOf(amount), currency);
}
public Money(String amount, Currency currency) {
this(new BigDecimal(amount), currency);
}
// Factory methods
public static Money of(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
public static Money of(double amount, Currency currency) {
return new Money(amount, currency);
}
public static Money usd(BigDecimal amount) {
return new Money(amount, Currency.getInstance("USD"));
}
public static Money usd(double amount) {
return new Money(amount, Currency.getInstance("USD"));
}
public static Money eur(BigDecimal amount) {
return new Money(amount, Currency.getInstance("EUR"));
}
// Business operations
public Money add(Money other) {
validateSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
public Money subtract(Money other) {
validateSameCurrency(other);
return new Money(amount.subtract(other.amount), currency);
}
public Money multiply(BigDecimal multiplier) {
return new Money(amount.multiply(multiplier), currency);
}
public Money multiply(double multiplier) {
return multiply(BigDecimal.valueOf(multiplier));
}
public Money divide(BigDecimal divisor) {
if (divisor.compareTo(BigDecimal.ZERO) == 0) {
throw new ArithmeticException("Division by zero");
}
return new Money(amount.divide(divisor, currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN), currency);
}
public Money divide(double divisor) {
return divide(BigDecimal.valueOf(divisor));
}
// Comparison operations
public boolean isGreaterThan(Money other) {
validateSameCurrency(other);
return amount.compareTo(other.amount) > 0;
}
public boolean isGreaterThanOrEqual(Money other) {
validateSameCurrency(other);
return amount.compareTo(other.amount) >= 0;
}
public boolean isLessThan(Money other) {
validateSameCurrency(other);
return amount.compareTo(other.amount) < 0;
}
public boolean isLessThanOrEqual(Money other) {
validateSameCurrency(other);
return amount.compareTo(other.amount) <= 0;
}
public boolean isPositive() {
return amount.compareTo(BigDecimal.ZERO) > 0;
}
public boolean isNegative() {
return amount.compareTo(BigDecimal.ZERO) < 0;
}
public boolean isZero() {
return amount.compareTo(BigDecimal.ZERO) == 0;
}
// Validation
private BigDecimal validateAmount(BigDecimal amount) {
if (amount == null) {
throw new IllegalArgumentException("Amount cannot be null");
}
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Money amount cannot be negative: " + amount);
}
return amount;
}
private void validateSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException(
String.format("Currency mismatch: %s vs %s", this.currency, other.currency));
}
}
// ValueObject implementation
@Override
protected Object[] getEqualityComponents() {
return new Object[] { amount, currency };
}
// Getters
public BigDecimal getAmount() { return amount; }
public Currency getCurrency() { return currency; }
// Formatting
public String format() {
return String.format("%s %.2f", currency.getSymbol(), amount);
}
public String formatWithCurrencyCode() {
return String.format("%s %.2f", currency.getCurrencyCode(), amount);
}
}
6. Address Value Objects
// Base Address Value Object
public class Address extends ValueObject {
protected final String street;
protected final String city;
protected final String state;
protected final String zipCode;
protected final String country;
protected Address(String street, String city, String state, String zipCode, String country) {
this.street = validateStreet(street);
this.city = validateCity(city);
this.state = validateState(state);
this.zipCode = validateZipCode(zipCode);
this.country = validateCountry(country);
}
// Factory method
public static Address of(String street, String city, String state, String zipCode, String country) {
return new Address(street, city, state, zipCode, country);
}
// Business operations
public ShippingAddress toShippingAddress() {
return new ShippingAddress(street, city, state, zipCode, country);
}
public boolean isIncomplete() {
return street == null || street.trim().isEmpty() ||
city == null || city.trim().isEmpty() ||
zipCode == null || zipCode.trim().isEmpty();
}
public void updateFrom(Address newAddress) {
// Since Value Objects are immutable, return a new instance
// This method is for Entity convenience
}
// Validation methods
protected String validateStreet(String street) {
if (street == null || street.trim().isEmpty()) {
throw new IllegalArgumentException("Street cannot be null or empty");
}
return street.trim();
}
protected String validateCity(String city) {
if (city == null || city.trim().isEmpty()) {
throw new IllegalArgumentException("City cannot be null or empty");
}
return city.trim();
}
protected String validateState(String state) {
if (state == null || state.trim().isEmpty()) {
throw new IllegalArgumentException("State cannot be null or empty");
}
return state.trim().toUpperCase();
}
protected String validateZipCode(String zipCode) {
if (zipCode == null || zipCode.trim().isEmpty()) {
throw new IllegalArgumentException("Zip code cannot be null or empty");
}
return zipCode.trim();
}
protected String validateCountry(String country) {
if (country == null || country.trim().isEmpty()) {
throw new IllegalArgumentException("Country cannot be null or empty");
}
return country.trim().toUpperCase();
}
// ValueObject implementation
@Override
protected Object[] getEqualityComponents() {
return new Object[] { street, city, state, zipCode, country };
}
// Getters
public String getStreet() { return street; }
public String getCity() { return city; }
public String getState() { return state; }
public String getZipCode() { return zipCode; }
public String getCountry() { return country; }
public String getFormattedAddress() {
return String.format("%s, %s, %s %s, %s", street, city, state, zipCode, country);
}
}
// Specialized Address for shipping
public final class ShippingAddress extends Address {
private final String recipientName;
private final String phoneNumber;
private final String instructions;
public ShippingAddress(String street, String city, String state,
String zipCode, String country) {
this(street, city, state, zipCode, country, null, null, null);
}
public ShippingAddress(String street, String city, String state,
String zipCode, String country,
String recipientName, String phoneNumber, String instructions) {
super(street, city, state, zipCode, country);
this.recipientName = recipientName;
this.phoneNumber = phoneNumber;
this.instructions = instructions;
}
// Factory methods
public static ShippingAddress forRecipient(String recipientName, String phoneNumber,
String street, String city, String state,
String zipCode, String country) {
return new ShippingAddress(street, city, state, zipCode, country,
recipientName, phoneNumber, null);
}
public ShippingAddress withInstructions(String instructions) {
return new ShippingAddress(street, city, state, zipCode, country,
recipientName, phoneNumber, instructions);
}
// ValueObject implementation
@Override
protected Object[] getEqualityComponents() {
return new Object[] { street, city, state, zipCode, country,
recipientName, phoneNumber, instructions };
}
// Getters
public String getRecipientName() { return recipientName; }
public String getPhoneNumber() { return phoneNumber; }
public String getInstructions() { return instructions; }
@Override
public String getFormattedAddress() {
StringBuilder sb = new StringBuilder();
if (recipientName != null) {
sb.append(recipientName).append("\n");
}
sb.append(super.getFormattedAddress());
if (instructions != null && !instructions.trim().isEmpty()) {
sb.append("\nInstructions: ").append(instructions);
}
return sb.toString();
}
}
7. Email Value Object
import java.util.regex.Pattern;
// Immutable Email Value Object
public final class Email extends ValueObject {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
private final String value;
public Email(String value) {
this.value = validateEmail(value);
}
// Factory method
public static Email of(String value) {
return new Email(value);
}
// Business operations
public String getLocalPart() {
return value.substring(0, value.indexOf('@'));
}
public String getDomain() {
return value.substring(value.indexOf('@') + 1);
}
public boolean isCorporate() {
String domain = getDomain().toLowerCase();
return domain.endsWith(".com") ||
domain.endsWith(".org") ||
domain.endsWith(".net");
}
// Validation
private String validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
String trimmed = email.trim().toLowerCase();
if (!EMAIL_PATTERN.matcher(trimmed).matches()) {
throw new IllegalArgumentException("Invalid email format: " + email);
}
if (trimmed.length() > 254) {
throw new IllegalArgumentException("Email is too long");
}
return trimmed;
}
// ValueObject implementation
@Override
protected Object[] getEqualityComponents() {
return new Object[] { value };
}
// Getters
public String getValue() { return value; }
@Override
public String toString() {
return value;
}
}
Supporting Classes
8. OrderItem and Product
// Entity for Order Item (part of Order aggregate)
public class OrderItem extends Entity<UUID> {
private final UUID productId;
private final String productName;
private final Money price;
private final int quantity;
public OrderItem(UUID productId, String productName, Money price, int quantity) {
super(generateId());
this.productId = Objects.requireNonNull(productId, "Product ID cannot be null");
this.productName = validateProductName(productName);
this.price = validatePrice(price);
this.quantity = validateQuantity(quantity);
}
// Business operations
public Money getLineTotal() {
return price.multiply(quantity);
}
public OrderItem withQuantity(int newQuantity) {
return new OrderItem(productId, productName, price, newQuantity);
}
// Validation
private String validateProductName(String productName) {
if (productName == null || productName.trim().isEmpty()) {
throw new IllegalArgumentException("Product name cannot be null or empty");
}
return productName.trim();
}
private Money validatePrice(Money price) {
if (price == null) {
throw new IllegalArgumentException("Price cannot be null");
}
if (price.isNegative()) {
throw new IllegalArgumentException("Price cannot be negative");
}
return price;
}
private int validateQuantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (quantity > 1000) {
throw new IllegalArgumentException("Quantity cannot exceed 1000");
}
return quantity;
}
// Getters
public UUID getProductId() { return productId; }
public String getProductName() { return productName; }
public Money getPrice() { return price; }
public int getQuantity() { return quantity; }
}
// Product Entity (Aggregate Root)
public class Product extends BaseEntity {
private String name;
private String description;
private Money price;
private int stockQuantity;
private ProductCategory category;
private boolean active;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Product(String name, String description, Money price,
int stockQuantity, ProductCategory category) {
super();
this.name = validateName(name);
this.description = validateDescription(description);
this.price = validatePrice(price);
this.stockQuantity = validateStockQuantity(stockQuantity);
this.category = Objects.requireNonNull(category, "Category cannot be null");
this.active = true;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Business operations
public void updatePrice(Money newPrice) {
this.price = validatePrice(newPrice);
this.updatedAt = LocalDateTime.now();
}
public void updateStock(int newStock) {
this.stockQuantity = validateStockQuantity(newStock);
this.updatedAt = LocalDateTime.now();
}
public void reduceStock(int quantity) {
if (quantity > stockQuantity) {
throw new IllegalArgumentException("Insufficient stock");
}
this.stockQuantity -= quantity;
this.updatedAt = LocalDateTime.now();
}
public void activate() {
this.active = true;
this.updatedAt = LocalDateTime.now();
}
public void deactivate() {
this.active = false;
this.updatedAt = LocalDateTime.now();
}
// Validation methods
private String validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Product name cannot be null or empty");
}
String trimmed = name.trim();
if (trimmed.length() < 2 || trimmed.length() > 100) {
throw new IllegalArgumentException("Product name must be between 2 and 100 characters");
}
return trimmed;
}
private String validateDescription(String description) {
if (description == null) {
return "";
}
return description.trim();
}
private Money validatePrice(Money price) {
if (price == null) {
throw new IllegalArgumentException("Price cannot be null");
}
if (price.isNegative()) {
throw new IllegalArgumentException("Price cannot be negative");
}
return price;
}
private int validateStockQuantity(int stockQuantity) {
if (stockQuantity < 0) {
throw new IllegalArgumentException("Stock quantity cannot be negative");
}
return stockQuantity;
}
// Getters
public String getName() { return name; }
public String getDescription() { return description; }
public Money getPrice() { return price; }
public int getStockQuantity() { return stockQuantity; }
public ProductCategory getCategory() { return category; }
public boolean isActive() { return active; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
// Business invariants
public boolean isAvailable() {
return active && stockQuantity > 0;
}
public boolean canFulfillOrder(int quantity) {
return isAvailable() && stockQuantity >= quantity;
}
}
enum ProductCategory {
ELECTRONICS, CLOTHING, BOOKS, HOME_AND_GARDEN, SPORTS, BEAUTY
}
Demo and Usage
9. Comprehensive Demo
public class EntityValueObjectDemo {
public static void main(String[] args) {
System.out.println("=== Entity and Value Object Modeling Demo ===\n");
demoValueObjects();
demoEntities();
demoBusinessOperations();
}
private static void demoValueObjects() {
System.out.println("1. VALUE OBJECTS DEMO");
System.out.println("=====================");
// Money Value Object
Money price1 = Money.usd(99.99);
Money price2 = Money.usd(99.99);
Money price3 = Money.usd(149.99);
System.out.println("Money equality: " + price1.equals(price2)); // true
System.out.println("Money inequality: " + price1.equals(price3)); // false
System.out.println("Money operations: " + price1.add(Money.usd(50)).format());
// Email Value Object
Email email1 = Email.of("[email protected]");
Email email2 = Email.of("[email protected]"); // normalized to lowercase
System.out.println("Email equality: " + email1.equals(email2)); // true
System.out.println("Email local part: " + email1.getLocalPart());
// Address Value Object
Address address1 = Address.of("123 Main St", "Springfield", "IL", "62701", "US");
Address address2 = Address.of("123 Main St", "Springfield", "IL", "62701", "US");
System.out.println("Address equality: " + address1.equals(address2)); // true
System.out.println("Formatted address: " + address1.getFormattedAddress());
}
private static void demoEntities() {
System.out.println("\n2. ENTITIES DEMO");
System.out.println("================");
// Create customer with Value Objects
Address address = Address.of("456 Oak Ave", "Chicago", "IL", "60601", "US");
Customer customer = new Customer("[email protected]", "Alice Johnson", address);
System.out.println("Customer created: " + customer.getName());
System.out.println("Customer ID: " + customer.getId());
System.out.println("Customer email: " + customer.getEmail());
System.out.println("Customer address: " + customer.getAddress().getFormattedAddress());
// Create products
Product laptop = new Product(
"MacBook Pro",
"High-performance laptop",
Money.usd(1999.99),
10,
ProductCategory.ELECTRONICS
);
Product mouse = new Product(
"Wireless Mouse",
"Ergonomic wireless mouse",
Money.usd(49.99),
25,
ProductCategory.ELECTRONICS
);
System.out.println("\nProducts created:");
System.out.println("- " + laptop.getName() + ": " + laptop.getPrice().format());
System.out.println("- " + mouse.getName() + ": " + mouse.getPrice().format());
}
private static void demoBusinessOperations() {
System.out.println("\n3. BUSINESS OPERATIONS DEMO");
System.out.println("===========================");
// Setup
Address address = Address.of("789 Pine Rd", "New York", "NY", "10001", "US");
Customer customer = new Customer("[email protected]", "Bob Smith", address);
Product laptop = new Product("Laptop", "Gaming laptop", Money.usd(1299.99), 5, ProductCategory.ELECTRONICS);
Product headphones = new Product("Headphones", "Noise-cancelling", Money.usd(199.99), 15, ProductCategory.ELECTRONICS);
// Place an order
Set<OrderItem> orderItems = Set.of(
new OrderItem(laptop.getId(), laptop.getName(), laptop.getPrice(), 1),
new OrderItem(headphones.getId(), headphones.getName(), headphones.getPrice(), 2)
);
Order order = customer.placeOrder(orderItems);
System.out.println("Order placed: " + order.getId());
System.out.println("Order total: " + order.getTotalAmount().format());
System.out.println("Order status: " + order.getStatus());
// Confirm order
order.confirm();
System.out.println("Order confirmed. Status: " + order.getStatus());
// Complete order
order.complete();
System.out.println("Order completed. Status: " + order.getStatus());
// Customer statistics
System.out.println("\nCustomer Statistics:");
System.out.println("Total spent: " + customer.getTotalSpent());
System.out.println("Can place new order: " + customer.canPlaceOrder());
}
}
Key Differences and Best Practices
Entities vs Value Objects:
| Aspect | Entity | Value Object |
|---|---|---|
| Identity | Defined by ID | Defined by attributes |
| Mutability | Mutable | Immutable |
| Equality | Based on ID | Based on all attributes |
| Lifespan | Has lifecycle | Can be replaced |
| References | Reference by ID | Can be copied freely |
Best Practices:
- Keep Value Objects immutable
- Use Entities for business objects with identity
- Prefer Value Objects for descriptive aspects
- Validate invariants in constructors
- Use factory methods for complex object creation
- Implement proper equals() and hashCode()
- Use meaningful business operations instead of setters
This modeling approach leads to more maintainable, testable, and domain-focused code that accurately represents business concepts and rules.