Java Omnibus
OOAD & Architecture / OOP foundations / Pillars of OOP in Java
OOP Foundations Java Order Management
OOP Foundations

The pillars of OOP matter because they shape how a Java system carries behavior, not just data.

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.

Why This Topic Matters

Why it matters in real Java systems

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.

Core Explanation

The design idea behind this topic

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?”

Concept Breakdown

Key ideas to hold onto

Concept

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.

Concept

Encapsulation

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.

Concept

Inheritance

Reuse can be helpful, but inheritance should express an actual subtype relationship. It is weaker than composition for many evolving business models.

Concept

Polymorphism

Different collaborators can honor the same contract in different ways. That matters when discounts, payment methods, or notification channels vary over time.

Java Example

Java example in the Order Management domain

Java example: the pillars working together in one order workflow
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 }
Code Walkthrough

Step by step

  1. Abstraction appears in the way the order talks to PaymentProcessor rather than to a concrete gateway implementation.
  2. Encapsulation appears because Order owns its own status transition instead of letting outside callers mutate it freely.
  3. Polymorphism appears because different PaymentProcessor implementations can handle the same request through one contract.
  4. Inheritance is not forced here, which is itself an OO lesson: many business problems are better served by composition and interfaces than by deep class trees.
Real-World Usage

Where this shows up in practice

  • Domain models that need to protect state transitions such as draft-to-paid or reserved-to-shipped
  • Application services that should coordinate through interfaces rather than framework-specific concrete classes
  • Payment, pricing, and notification workflows where behavior varies by policy or channel
Trade-Offs

When not to over-apply it

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.

Common Mistakes

Frequent mistakes to watch for

  • Explaining OOP only in syntax terms and never connecting it to responsibility or boundaries
  • Using inheritance as the default reuse mechanism
  • Calling an object encapsulated even though outside code can still break its invariants
  • Confusing polymorphism with any use of an interface, even when there is no meaningful behavioral variation
Related Pages

Related concepts


What To Read Next