Stable call site, varying behavior
The caller should not need a growing chain of if-else logic just to handle ordinary business variation.
Polymorphism is easy to explain syntactically and harder to apply well in design. It is not just ‘calling a method on an interface.’ In OOAD, polymorphism becomes valuable when one business flow should stay coherent while multiple implementations supply specialized behavior behind the same contract.
Checkout, payment, notification, and fraud review flows often share the same high-level structure while differing in specific execution details. Without polymorphism, those differences become conditionals spread across services. With it, variation moves into focused collaborators that honor one stable interface.
Polymorphism is best understood as a way of localizing variation. One part of the system should know that there are different behaviors, but the rest of the flow should not need to keep branching on implementation detail.
In Java, that variation is often expressed through interfaces and dependency injection. In some domains, a sealed interface or enum-specific behavior can be clearer because the allowed variants are finite and known.
The OOAD challenge is identifying the right place for the variation. If every tiny difference becomes a new interface, the design fragments. If all variation stays in one service, the design calcifies.
The caller should not need a growing chain of if-else logic just to handle ordinary business variation.
A useful polymorphic design chooses the right variation point, not merely an interface for everything.
Interfaces, sealed hierarchies, enums with behavior, and record-friendly strategies can all express polymorphism.
package org.javaomnibus.ecommerce.foundations;
public final class OrderConfirmationService {
private final NotificationChannel notificationChannel;
public OrderConfirmationService(NotificationChannel notificationChannel) {
this.notificationChannel = notificationChannel;
}
public void confirm(Order order) {
notificationChannel.send(
new NotificationMessage(order.customerEmail(), "Your order " + order.id() + " has been confirmed.")
);
}
}
interface NotificationChannel {
void send(NotificationMessage message);
}
final class EmailNotificationChannel implements NotificationChannel {
@Override
public void send(NotificationMessage message) {
System.out.println("EMAIL -> " + message.destination() + ": " + message.body());
}
}
final class SmsNotificationChannel implements NotificationChannel {
@Override
public void send(NotificationMessage message) {
System.out.println("SMS -> " + message.destination() + ": " + message.body());
}
}
record NotificationMessage(String destination, String body) {}
record Order(String id, String customerEmail) {}
Polymorphism can hide important differences if the abstraction is too broad. Sometimes a branching structure is clearer than forcing very different behaviors through one interface.