Use interfaces for capability boundaries
If the system needs multiple implementations or external substitution, interfaces usually express the intent better.
Java gives you more than one way to model common contracts. Interfaces define capability and abstraction boundaries. Abstract classes share partial implementation and state. Both are useful, but they should not be treated as interchangeable defaults.
In long-lived systems, the choice between interface and abstract class affects flexibility, testing, framework integration, and future change. A payment authorization contract may benefit from an interface because multiple implementations are expected. A reusable base for domain events may benefit from an abstract class if shared state and behavior are truly stable.
Interfaces are especially strong when OOAD aims to separate policy from implementation. They work well with dependency inversion, testing, and composition. In modern Java, default methods also make interfaces more expressive than they once were.
Abstract classes become useful when there is meaningful shared implementation that should live in one place. They are not just “interfaces with code.” They imply a closer implementation relationship and can make the inheritance trade-offs more visible.
A practical heuristic is this: if the main need is substitutability, start with an interface. If the main need is stable shared implementation, consider an abstract class. Then check whether composition would be even cleaner.
If the system needs multiple implementations or external substitution, interfaces usually express the intent better.
If subclasses genuinely share stable implementation and lifecycle logic, an abstract class can reduce duplication.
OOAD improves when you let the real responsibility and variation shape the abstraction choice.
package org.javaomnibus.ecommerce.foundations;
import java.time.Instant;
interface OrderProcessor {
void process(Order order);
}
abstract class AuditedProcessor implements OrderProcessor {
@Override
public final void process(Order order) {
System.out.println("AUDIT " + Instant.now() + " -> " + order.id());
doProcess(order);
}
protected abstract void doProcess(Order order);
}
final class ShipmentPreparationProcessor extends AuditedProcessor {
@Override
protected void doProcess(Order order) {
System.out.println("Preparing shipment for " + order.id());
}
}
record Order(String id) {}
Abstract classes make subclasses more tightly coupled to shared implementation. Interfaces can be cleaner, but sometimes leave too much duplication if stable shared behavior truly exists.