Entity and Value Object Modeling in Java

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:

AspectEntityValue Object
IdentityDefined by IDDefined by attributes
MutabilityMutableImmutable
EqualityBased on IDBased on all attributes
LifespanHas lifecycleCan be replaced
ReferencesReference by IDCan be copied freely

Best Practices:

  1. Keep Value Objects immutable
  2. Use Entities for business objects with identity
  3. Prefer Value Objects for descriptive aspects
  4. Validate invariants in constructors
  5. Use factory methods for complex object creation
  6. Implement proper equals() and hashCode()
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper