Abstraction
Choose the business-facing view that matters and suppress lower-level detail. A Checkout flow should depend on an Order abstraction, not on every persistence or transport concern inside it.
Object-oriented programming is often introduced as four terms to memorize: abstraction, encapsulation, inheritance, and polymorphism. In practice, those ideas matter because they determine where behavior lives, how change spreads, and whether your domain model reflects real business meaning. In Java, the pillars are most useful when they are applied together inside a coherent model instead of treated as isolated syntax features.
In an Order Management system, the difference between a meaningful object model and a bag of DTO-like records shows up quickly. Orders need invariants. Payments need clear boundaries. Shipments need state transitions. Notifications need controlled extension points. The pillars of OOP help us decide which behavior belongs on the object, what should stay hidden, when reuse is healthy, and how one part of the system can depend on abstractions instead of concrete details.
The pillars are not equally important in every design decision. Encapsulation and abstraction carry most of the day-to-day weight in business systems because they determine whether the model exposes the right surface and protects the right rules.
Inheritance and polymorphism are still important, but experienced Java developers usually treat inheritance carefully. Polymorphism often survives in the final design through interfaces, sealed hierarchies, strategy objects, or domain-specific policies.
The practical OOAD question is never “did we use all four pillars?” It is “did we shape objects that make responsibility clearer, changes safer, and the domain easier to understand?”
Choose the business-facing view that matters and suppress lower-level detail. A Checkout flow should depend on an Order abstraction, not on every persistence or transport concern inside it.
Keep rules and state together so the model can defend itself. If anything can set an Order status to PAID directly, the model stops protecting the business.
Reuse can be helpful, but inheritance should express an actual subtype relationship. It is weaker than composition for many evolving business models.
Different collaborators can honor the same contract in different ways. That matters when discounts, payment methods, or notification channels vary over time.
package org.javaomnibus.ecommerce.foundations;
import java.math.BigDecimal;
import java.util.List;
public final class Order {
private final Customer customer;
private final List lines;
private OrderStatus status;
public Order(Customer customer, List lines) {
this.customer = customer;
this.lines = List.copyOf(lines);
this.status = OrderStatus.DRAFT;
}
public Money total() {
return lines.stream()
.map(OrderLine::lineTotal)
.reduce(Money.zero("USD"), Money::add);
}
public PaymentReceipt pay(PaymentProcessor paymentProcessor) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Only draft orders can be paid");
}
PaymentReceipt receipt = paymentProcessor.process(total(), customer.preferredPaymentMethod());
status = OrderStatus.PAID;
return receipt;
}
}
interface PaymentProcessor {
PaymentReceipt process(Money amount, PaymentMethod paymentMethod);
}
final class CardPaymentProcessor implements PaymentProcessor {
@Override
public PaymentReceipt process(Money amount, PaymentMethod paymentMethod) {
return new PaymentReceipt("card-auth-" + amount.value(), true);
}
}
record Customer(String id, PaymentMethod preferredPaymentMethod) {}
record OrderLine(String sku, int quantity, Money unitPrice) {
Money lineTotal() {
return unitPrice.multiply(quantity);
}
}
record PaymentMethod(String type) {}
record PaymentReceipt(String authorizationId, boolean approved) {}
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 multiply(int quantity) {
return new Money(value.multiply(BigDecimal.valueOf(quantity)), currency);
}
}
enum OrderStatus { DRAFT, PAID }
Treating the pillars as a checklist can lead to over-designed code. You do not need inheritance everywhere, and you do not need rich objects around every CRUD use case. The value of the pillars comes from their fit with the problem, not from their presence in every class.