Inheritance is strongest when the subtype is stable
If a PremiumCustomer truly behaves as a specialized Customer with preserved expectations, inheritance can work.
Inheritance is one of the first reuse techniques developers learn, and one of the first they later unlearn in overused form. In OOAD, the question is not whether inheritance is good or bad. The question is whether a design expresses a true ‘is-a’ relationship or whether it is really a case of assembling behavior from smaller collaborators.
Commerce systems change by variation: discounts vary, payment authorization varies, shipping policies vary, and customer messaging varies. Those variations often evolve independently. Deep inheritance trees turn that natural variation into rigid coupled structure. Composition lets you combine behaviors without pretending every variation is a subtype.
Inheritance couples a subtype to the implementation and assumptions of its parent. That can be appropriate when the subtype truly preserves the parent’s promises and the hierarchy matches domain reality.
Composition instead says: build the object from collaborators or contained behavior units. In Java, this often leads to cleaner use of interfaces, policies, and delegation. It also tends to align better with dependency injection, testing, and module boundaries.
Most mature OOAD guidance favors composition by default, then uses inheritance deliberately where the domain genuinely supports it.
If a PremiumCustomer truly behaves as a specialized Customer with preserved expectations, inheritance can work.
If checkout depends on pricing, fraud checks, and notification policies, those are usually collaborators, not subclasses.
Composition helps when different responsibilities change for different reasons.
package org.javaomnibus.ecommerce.foundations;
import java.math.BigDecimal;
import java.util.List;
public final class Cart {
private final List lines;
private final DiscountPolicy discountPolicy;
public Cart(List lines, DiscountPolicy discountPolicy) {
this.lines = List.copyOf(lines);
this.discountPolicy = discountPolicy;
}
public Money total() {
Money subtotal = lines.stream()
.map(OrderLine::lineTotal)
.reduce(Money.zero("USD"), Money::add);
return discountPolicy.apply(subtotal);
}
}
interface DiscountPolicy {
Money apply(Money subtotal);
}
final class NoDiscountPolicy implements DiscountPolicy {
@Override
public Money apply(Money subtotal) {
return subtotal;
}
}
final class LoyaltyDiscountPolicy implements DiscountPolicy {
@Override
public Money apply(Money subtotal) {
return subtotal.subtract(new Money(new BigDecimal("10.00"), "USD"));
}
}
record OrderLine(String sku, int quantity, Money unitPrice) {
Money lineTotal() {
return unitPrice.multiply(quantity);
}
}
record Money(BigDecimal value, String currency) {
static Money zero(String currency) {
return new Money(BigDecimal.ZERO, currency);
}
Money add(Money other) {
return new Money(value.add(other.value), currency);
}
Money subtract(Money other) {
return new Money(value.subtract(other.value), currency);
}
Money multiply(int quantity) {
return new Money(value.multiply(BigDecimal.valueOf(quantity)), currency);
}
}
Composition can produce more collaborators and slightly more wiring. That is usually a worthwhile trade when the domain varies in more than one dimension, but it can feel heavier for small, fixed, stable models.