Object-Oriented Programming (OOP)

Object-Oriented Programming in Java: The Four Pillars and Where Each One Breaks

A code review comment saying “this violates encapsulation” or a defect ticket noting “the abstraction is leaking” only means something if you can place those words in a real framework, not just recognize them as jargon. Object-oriented programming in Java organizes code around four pillars — encapsulation, abstraction, inheritance, and polymorphism — and understanding them changes how you read a data model, write acceptance criteria, or triage a defect that “shouldn’t be possible” given the validation rules everyone assumed were in place. This guide covers all four pillars, with real depth on the two — encapsulation and abstraction — that rarely get the attention inheritance and polymorphism do, plus where each one actually breaks in production systems.

What Is Object-Oriented Programming in Java?

Object-oriented programming models software as interacting objects, each bundling its own data and behavior. Java organizes this around four pillars, and Oracle’s official Java OOP concepts documentation treats all four as equally foundational, though in practice, two get far more attention than the other two.

Pillar One-Line Definition QA/BA-Relevant Example
Encapsulation Bundling data with the logic that protects it A Patient class that rejects an invalid date of birth at the point of assignment
Abstraction Hiding implementation behind a simple contract A PaymentGateway interface that hides whether Stripe or PayPal is behind it
Inheritance A subclass extending a parent’s fields and methods Covered in depth in Java inheritance
Polymorphism One method call, different behavior by object type Covered in depth in polymorphism and extensibility

Encapsulation
Protects data with
controlled access

Abstraction
Hides complexity
behind a contract

Inheritance
Reuses a parent’s
fields and methods

Polymorphism
One call, behavior
varies by object

All four work together: encapsulation and abstraction control WHAT’S exposed; inheritance and polymorphism control HOW behavior varies.

Encapsulation: Why “Getters and Setters” Isn’t the Whole Story

Encapsulation means bundling an object’s data with the logic that protects it, restricting direct access to internal state. Most explanations stop at “make fields private and add getters and setters” — but a setter that accepts anything is not encapsulation, it’s just a wrapped public field with extra syntax.

// Not real encapsulation — a private field with an
// unrestricted setter is functionally still public
class Patient {
    private int age;
    public void setAge(int age) { this.age = age; }
}

// Real encapsulation — the class protects its own invariant
class Patient {
    private int age;
    public void setAge(int age) {
        if (age < 0 || age > 130) {
            throw new IllegalArgumentException("Invalid age: " + age);
        }
        this.age = age;
    }
}

The second version is what encapsulation actually buys you: a guarantee that no code, anywhere in the system, can put a Patient object into an invalid state. That guarantee is only as strong as the validation inside the setter — a private field with a pass-through setter provides zero real protection, despite looking identical to real encapsulation at a glance.

Abstraction: Hiding Complexity Behind a Contract

Abstraction means exposing only what a caller needs, hiding how it’s actually implemented. An interface is Java’s primary abstraction tool: code written against PaymentGateway never needs to know whether Stripe, PayPal, or a bank’s direct API sits behind it.

interface PaymentGateway {
    PaymentResult charge(double amount) throws PaymentException;
}
// Calling code only ever sees this contract —
// never Stripe's or PayPal's specific SDK types or error codes

Abstraction breaks down the moment implementation details leak through the contract — a PaymentException that actually carries a raw Stripe error code, forcing every caller to know about Stripe internals anyway. This is called a “leaky abstraction,” and it defeats the entire purpose: the contract promised independence from the implementation, and the leak quietly takes it back.

Inheritance and Polymorphism, Briefly

These two pillars get full, dedicated treatment elsewhere on this site because each has enough real-world failure modes to warrant it. Java inheritance covers the extends keyword, the fragile base class problem, and when composition is the safer choice. Polymorphism and extensibility covers method overriding, dynamic dispatch, and Liskov Substitution violations. Both are worth reading in full if a defect touches either pillar specifically.

Where OOP Literacy Actually Matters in QA and BA Work

Reading Code Review Comments

“This breaks encapsulation” and “the abstraction is leaking” are specific, technical critiques, not vague style complaints. Recognizing which pillar a review comment refers to helps a BA or QA lead gauge whether a flagged issue is a minor style preference or a real risk to data integrity.

Writing Acceptance Criteria for Entity Classes

Acceptance criteria that specify “invalid data must be rejected” are really asking for encapsulation — validation enforced at the point data enters an object, not scattered across every place that happens to touch it later.

Defect Triage: Encapsulation Leak or Business Logic Bug?

A defect where invalid data made it into the system despite “having validation” is frequently an encapsulation gap — validation existing somewhere in the codebase, but not enforced at the object level, leaving other entry points free to bypass it entirely.

Scenario: Healthcare IT — A Batch Import Bypasses Patient Validation

An EHR system’s UI form correctly rejects an invalid date of birth. A nightly batch import job, built later by a different team, writes directly to the same Patient object’s public fields, skipping the UI’s validation logic entirely. A HIPAA data quality audit finds hundreds of patient records with impossible birth dates.

TFF-3944Critical

Summary: Batch-imported patient records contain invalid dates of birth

Steps to Reproduce: Run nightly batch import with a source record containing an out-of-range birth date.

Expected: Record rejected or flagged, matching UI validation behavior.

Actual: Record imported successfully with the invalid date intact.

Environment: Production batch pipeline

The root cause wasn’t missing validation logic — it existed, correctly, in the UI form’s controller. It just wasn’t enforced by the Patient class itself, so any code path that didn’t go through that specific controller had no protection at all. Moving the validation into the Patient class’s constructor and setters, as shown in the encapsulation example above, closes every entry point at once instead of requiring every future integration to remember to re-implement the same check.

Scenario: Financial IT — A Leaky Payment Abstraction Breaks on a Second Provider

A checkout system’s PaymentGateway interface was implemented only against Stripe for its first two years in production. Error handling code throughout the application checks for Stripe-specific error code strings directly, even though it’s written against the interface. Adding PayPal as a second provider breaks error handling across a dozen call sites, because PayPal’s error codes don’t match the strings that logic expects.

This is a leaky abstraction: the interface promised implementation independence, but the exception-handling contract was never actually abstracted — it silently assumed Stripe underneath the whole time. The fix defined a provider-agnostic set of payment error categories in the interface itself, with each implementation responsible for translating its own provider-specific errors into that shared vocabulary.

This is worth flagging in any interface design review: an abstraction is only as good as its weakest leaking detail, and error handling is the most common place implementation details sneak through unnoticed.

Common Mistakes and Edge Cases

Confusing “private fields” with real encapsulation. As shown above, a private field with an unrestricted setter provides no actual protection. The point of encapsulation is enforced invariants, not just syntax.

Anemic domain models. Martin Fowler’s widely-referenced critique of the “anemic domain model” describes exactly this pattern at scale: data classes that are just bags of getters and setters, with all real business logic living elsewhere in service classes. It looks object-oriented, but the objects themselves protect nothing.

Leaky abstractions in error handling. As the payment scenario shows, an interface can look perfectly abstracted in its happy path while leaking implementation details entirely through its exceptions or error codes — check both, not just the successful case.

Treating all four pillars as equally likely culprits. In practice, encapsulation gaps and leaky abstractions cause more silent data-integrity defects than inheritance or polymorphism issues do, simply because they’re less visible in code review — a missing validation check doesn’t look wrong the way a broken override does.

Anemic Domain Model vs. Rich Domain Model

Model Where Logic Lives Risk
Anemic Separate service classes; data objects are pure getters/setters Any new code path can bypass validation, as in the healthcare scenario above
Rich Inside the domain objects themselves Requires more upfront design discipline; harder to bolt on later

Quick Reference Checklist by Role

QA / Test Automation Engineer

  • Test every entry point into an entity class, not just the primary UI form
  • Flag setters that accept any value with no validation as a testing gap
  • Check error-handling paths for leaked implementation-specific details, not just happy paths
Business Analyst

  • Write acceptance criteria that specify validation belongs at the data object level, not one specific form
  • Ask whether “invalid data got through” defects involve a second, unvalidated entry point
  • Flag anemic domain models as a design risk during architecture review, not just a style note
IT Support / Ops Analyst

  • Trace “how did invalid data get in” incidents to every write path, not just the obvious one
  • Watch for provider-specific error codes leaking through supposedly abstracted integrations
  • Flag new integrations added to an existing abstraction as a re-test trigger for error handling

The next time invalid data shows up despite “having validation,” or a new integration breaks error handling that was supposedly abstracted away, check which pillar actually failed. Encapsulation and abstraction don’t announce themselves the way a broken inheritance chain does — they fail quietly, by simply not being where everyone assumed they were.


Further reading: Oracle’s official Java OOP concepts documentation covers the formal definitions referenced throughout this guide. For the anemic domain model pattern, see Martin Fowler’s widely-cited analysis.

Download the OOP Code Review Checklist (PDF)

A one-page checklist for spotting encapsulation gaps, leaky abstractions, and anemic domain models during code review.

Get the Free Checklist →

Scroll to Top