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.