DEV Community

Anubhav Gupta
Anubhav Gupta

Posted on

Getters and Setters Are Not Encapsulation — Here’s What Encapsulation Actually Means

When we first learn Object-Oriented Programming, encapsulation is often explained like this:

Make fields private and access them using getters and setters.

Technically, this introduces data hiding, but it does not automatically give us good encapsulation.

There is an important difference.

class BankAccount {

    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this looks perfectly encapsulated.

The field is private.

Nobody can directly write:

account.balance = 1000;
Enter fullscreen mode Exit fullscreen mode

Instead, they have to call:

account.setBalance(1000);
Enter fullscreen mode Exit fullscreen mode

But ask yourself one question:

What exactly did we protect?

Not much.

A caller can still do this:

account.setBalance(-50000);
Enter fullscreen mode Exit fullscreen mode

or:

account.setBalance(999999999);
Enter fullscreen mode Exit fullscreen mode

The field is private, but the object's internal state is still completely controlled by the outside world.

That is not strong encapsulation.


What Encapsulation Actually Means

Encapsulation is not just about restricting how a variable is accessed.

It is about restricting how an object's state can change.

A well-encapsulated object should:

  • protect its internal state
  • enforce business rules
  • maintain valid state
  • expose meaningful operations
  • hide unnecessary implementation details

The object itself should decide which state transitions are allowed.

This leads to an important concept:

Invariants

An invariant is a condition that should always remain true for an object.

For example, suppose our banking system does not allow a balance to become negative.

Then:

balance >= 0
Enter fullscreen mode Exit fullscreen mode

is an invariant.

If we expose a generic setter:

setBalance(double balance)
Enter fullscreen mode Exit fullscreen mode

we are allowing outside code to break that invariant.

Instead of exposing the state directly, we should expose behavior.


A Better Design

Consider this version:

class BankAccount {

    private double balance;

    public BankAccount(double initialBalance) {

        if (initialBalance < 0) {
            throw new IllegalArgumentException(
                "Initial balance cannot be negative"
            );
        }

        this.balance = initialBalance;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {

        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Deposit amount must be positive"
            );
        }

        balance += amount;
    }

    public void withdraw(double amount) {

        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Withdrawal amount must be positive"
            );
        }

        if (amount > balance) {
            throw new IllegalStateException(
                "Insufficient balance"
            );
        }

        balance -= amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice something important.

There is no setBalance() method.

Instead, the object exposes operations that make sense in its domain:

deposit()
withdraw()
getBalance()
Enter fullscreen mode Exit fullscreen mode

Now someone cannot arbitrarily change:

1000 -> 500000
Enter fullscreen mode Exit fullscreen mode

by calling:

setBalance(500000);
Enter fullscreen mode Exit fullscreen mode

They have to perform a legitimate operation.

account.deposit(500);
Enter fullscreen mode Exit fullscreen mode

This is much closer to real encapsulation.


Data Hiding vs Encapsulation

These two concepts are related, but they are not identical.

Data Hiding

Data hiding restricts direct access to internal data.

For example:

private double balance;
Enter fullscreen mode Exit fullscreen mode

Outside code cannot directly access the variable.

That's useful.

But encapsulation goes further.

Encapsulation

Encapsulation combines state and behavior while controlling the way the state can change.

Instead of asking:

"Can external code access this variable?"

we should ask:

"Can external code put this object into an invalid state?"

That question gives us a much better understanding of encapsulation.


Another Example: User Age

Consider:

class User {

    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
Enter fullscreen mode Exit fullscreen mode

We technically have a private variable.

But this is possible:

user.setAge(-25);
Enter fullscreen mode Exit fullscreen mode

Now our object contains an invalid state.

We could add validation:

public void setAge(int age) {

    if (age < 0) {
        throw new IllegalArgumentException(
            "Age cannot be negative"
        );
    }

    this.age = age;
}
Enter fullscreen mode Exit fullscreen mode

This is already better.

But sometimes even the existence of a setter should be questioned.

Imagine age is calculated from date of birth.

In that case, there should probably be no:

setAge()
Enter fullscreen mode Exit fullscreen mode

at all.

Instead:

class User {

    private LocalDate dateOfBirth;

    public User(LocalDate dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }

    public int getAge() {
        return Period.between(
            dateOfBirth,
            LocalDate.now()
        ).getYears();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now age cannot accidentally become inconsistent with dateOfBirth.

The object's design prevents the invalid state from existing.

That is powerful encapsulation.


Tell, Don't Ask

There is another useful object-oriented design principle related to this discussion:

Tell, Don't Ask.

Instead of extracting an object's data, making decisions outside the object, and then setting the state again, tell the object what you want it to do.

Consider:

if (account.getBalance() >= amount) {
    account.setBalance(
        account.getBalance() - amount
    );
}
Enter fullscreen mode Exit fullscreen mode

Here, the caller knows:

  • how the balance is stored
  • how withdrawal works
  • what validation is required
  • how the state should change

That logic belongs to the BankAccount.

A better approach is:

account.withdraw(amount);
Enter fullscreen mode Exit fullscreen mode

Now the object owns the behavior.

The caller does not need to understand the object's internal rules.


Why Generic Setters Can Be Dangerous

Imagine a class with ten fields:

class Order {

    private String status;
    private double total;
    private boolean paid;
    private LocalDateTime shippedAt;
}
Enter fullscreen mode Exit fullscreen mode

Now imagine automatically generating setters for everything:

setStatus()
setTotal()
setPaid()
setShippedAt()
Enter fullscreen mode Exit fullscreen mode

External code could create combinations like:

status = SHIPPED
paid = false
shippedAt = null
Enter fullscreen mode Exit fullscreen mode

The individual values may be legal.

But together they represent an invalid Order.

This is why encapsulation is not just about validating individual variables.

Sometimes we need to protect the relationship between multiple variables.

Instead, our API might expose:

order.markAsPaid();

order.ship();
Enter fullscreen mode Exit fullscreen mode

Inside ship():

public void ship() {

    if (!paid) {
        throw new IllegalStateException(
            "An unpaid order cannot be shipped"
        );
    }

    this.status = "SHIPPED";
    this.shippedAt = LocalDateTime.now();
}
Enter fullscreen mode Exit fullscreen mode

Now the class protects its own rules.

The Order object becomes responsible for maintaining a valid order state.


Encapsulation Reduces Coupling

Good encapsulation has another major benefit:

implementation details can change without breaking callers.

Suppose today we store:

private double balance;
Enter fullscreen mode Exit fullscreen mode

Later, because financial calculations should avoid floating-point precision issues, we replace it with:

private BigDecimal balance;
Enter fullscreen mode Exit fullscreen mode

If the rest of our application directly depends on how balance works, the change could spread everywhere.

But if callers simply use:

account.deposit(amount);
account.withdraw(amount);
Enter fullscreen mode Exit fullscreen mode

the internal implementation can evolve independently.

That is one of the biggest advantages of encapsulation:

Objects expose stable behavior while hiding implementation decisions.


Should We Never Use Setters?

No.

Setters themselves are not bad.

The problem is automatically generating setters for every field without thinking about the object's rules.

A setter can be perfectly valid:

public void setDisplayName(String displayName) {

    if (displayName == null || displayName.isBlank()) {
        throw new IllegalArgumentException();
    }

    this.displayName = displayName;
}
Enter fullscreen mode Exit fullscreen mode

If changing a display name is a legitimate operation in the domain, this design is fine.

The important question is not:

"Should this field have a setter?"

The better question is:

"Should external code be allowed to change this value directly?"

Sometimes the answer is yes.

Sometimes the answer is no.


Encapsulation Is About Designing an API

One useful way to think about a class is:

Every public method becomes part of the API of that object.

For example:

setBalance()
Enter fullscreen mode Exit fullscreen mode

essentially says:

You are allowed to replace the balance with any value.

While:

deposit()
withdraw()
Enter fullscreen mode Exit fullscreen mode

say:

You can request legitimate banking operations, but the account controls how its state changes.

That difference may look small in code.

Architecturally, it is huge.


A Simple Rule I Use

Before adding a setter, ask:

"Am I exposing state, or am I exposing behavior?"

Instead of:

order.setStatus("SHIPPED");
Enter fullscreen mode Exit fullscreen mode

consider:

order.ship();
Enter fullscreen mode Exit fullscreen mode

Instead of:

account.setBalance(balance - 500);
Enter fullscreen mode Exit fullscreen mode

consider:

account.withdraw(500);
Enter fullscreen mode Exit fullscreen mode

Instead of:

user.setVerified(true);
Enter fullscreen mode Exit fullscreen mode

consider:

user.verify();
Enter fullscreen mode Exit fullscreen mode

The second approach usually produces objects that are easier to understand, harder to misuse, and safer to modify.


Final Thought

private fields and getters/setters are language mechanisms.

Encapsulation is a design principle.

You can have:

private fields
+ getters
+ setters
Enter fullscreen mode Exit fullscreen mode

and still have a poorly encapsulated class.

Good encapsulation means that an object:

  • owns its state
  • protects its invariants
  • exposes meaningful behavior
  • prevents invalid transitions
  • hides implementation details from callers

So next time your IDE offers:

Generate Getters and Setters

don't automatically click:

Select All.

First ask:

What should this object actually allow the outside world to do?

That is where encapsulation really begins.

Top comments (0)