When should a class expose getters and setters at all, and what are the alternatives for good information hiding?
answer
- no reflexive get/set for every field
- minimal public surface = each member is a forever promise
- prefer immutability / records, omit setters
- Tell, Don't Ask — expose operations not state
- DTOs/serialization are the legit accessor cases
basics
~20 sDon't add a getter and setter for every field by reflex. Expose accessors only when callers genuinely need that data, prefer no setters (immutable objects), and favor methods that express real behavior (like withdraw()) over raw setX/getX that just leak the representation.
solid answer
~50 sReflexive getter/setter pairs are an anti-pattern: they re-expose the internal representation through methods, coupling callers to it and defeating the point of making fields private. The senior stance is to design the public interface around what callers actually need. Prefer immutability: provide read accessors only where required and omit setters, constructing the object fully and validly up front. Where behavior is involved, expose intention-revealing operations (transfer, activate, applyDiscount) instead of letting callers get state, mutate it, and set it back — keeping invariants inside the class. Tell, Don't Ask captures this: tell the object to do something rather than asking for its data to act on it externally. Getters are fine for genuine read needs (DTOs, serialization, display), but they should expose stable concepts, not just mirror fields, and should return immutable values or copies. The measure of good hiding is a small, stable, behavior-oriented surface, not a complete set of accessors.
code
java · 24 lines// Anti-pattern: reflexive accessors re-expose the representation
public class Account {
private long balanceCents;
public long getBalanceCents() { return balanceCents; }
public void setBalanceCents(long v) { this.balanceCents = v; } // any value, no rules
}
// Better: hide state, expose intention-revealing behavior (Tell, Don't Ask)
public class BetterAccount {
private long balanceCents;
public BetterAccount(long openingCents) {
if (openingCents < 0) throw new IllegalArgumentException();
this.balanceCents = openingCents;
}
public long balanceCents() { return balanceCents; } // read only where needed
public void withdraw(long cents) { // rule lives inside the object
if (cents <= 0) throw new IllegalArgumentException("positive only");
if (cents > balanceCents) throw new IllegalStateException("insufficient funds");
balanceCents -= cents;
}
}go deeper
Can write getters/setters correctly; starting to learn that not every field needs both.
Knows to omit unnecessary setters, prefers immutability where possible, and avoids exposing mutable internals.
Designs minimal behavior-oriented interfaces, applies Tell-Don't-Ask, prefers immutable types/records, and recognizes the legitimate DTO/serialization exceptions.
Sets codebase-wide conventions distinguishing domain models (rich, hidden, immutable) from boundary DTOs, weighs framework constraints (JPA/JavaBeans) against design ideals, and governs API stability and coupling across modules.
## The default-getter/setter habit and why it's wrong Many developers, and most IDE 'generate getters and setters' tools, produce a full read/write accessor pair for every field: ```java public class Order { private OrderStatus status; public OrderStatus getStatus() { return status; } public void setStatus(OrderStatus status) { this.status = status; } } ``` This *looks* like encapsulation (the field is private) but achieves almost none of its benefits. `setStatus(...)` lets any caller move the order to any status with no rules — e.g. `SHIPPED` → `DRAFT` — so the class no longer owns its own lifecycle. The representation ("an order has a settable status field") is now part of the public contract, so it can't change. This is the **anaemic / reflexive accessor** anti-pattern. ## Principle 1: expose only what's needed Information hiding is about a **minimal public surface**. Each public member is a promise you must keep forever. So: - Add a **getter only when a caller truly needs to read that value** (display, serialization, a legitimate query). - Add a **setter only when the value legitimately changes after construction and there's no better operation to model that change.** Fewer accessors = fewer dependencies on your internals = more freedom to evolve. ## Principle 2: prefer immutability over setters The cleanest way to avoid bad setters is to not have them. Build the object fully and validly in its constructor (or via a builder/factory), make fields `private final`, and provide read accessors where needed: ```java public final class Money { private final long amountCents; private final String currency; public Money(long amountCents, String currency) { if (amountCents < 0) throw new IllegalArgumentException(); this.amountCents = amountCents; this.currency = Objects.requireNonNull(currency); } public long amountCents() { return amountCents; } public Money plus(Money other) { /* returns a NEW Money */ } } ``` Immutable objects can't enter an invalid state after construction, are thread-safe, and are safe to share and cache — and there's nothing mutable to leak. **Records** make this trivial for plain data: `record Money(long amountCents, String currency) {}` with validation in a compact constructor. ## Principle 3: expose behavior, not state (Tell, Don't Ask) The strongest hiding replaces accessor pairs with **intention-revealing operations** that keep the rules inside the object. Compare: ```java // Ask: caller reaches into state and enforces the rule externally if (account.getBalance() >= amount) { account.setBalance(account.getBalance() - amount); } // Tell: the object owns the rule account.withdraw(amount); // throws if insufficient funds ``` The "Tell, Don't Ask" form means the balance rule lives in exactly one place, can never be forgotten or duplicated, and the field is never directly exposed. This is the heart of object-oriented design: objects with behavior, not data bags with external procedures. ## When getters/setters ARE appropriate - **DTOs / API boundary objects** that exist purely to carry data across a wire (JSON, forms) often legitimately need accessors so frameworks can read/write them — though even here records or constructor binding are increasingly preferred. - **Read-only queries** for display, reporting, or serialization, returning immutable values or defensive copies. - **Frameworks** (JPA entities, JavaBeans-based tools) may require the bean shape; that's a pragmatic concession, not a design ideal. Even then, a getter should expose a **stable concept**, not necessarily a raw field — e.g. `getFullName()` deriving from parts — so the internal layout can change. ## Decision checklist 1. Does any caller actually need this value? If not, no getter. 2. Can the object be fully and validly constructed up front and never change? If so, no setters — make it immutable / a record. 3. Is there a meaningful operation the caller is really trying to perform? Expose that operation (Tell, Don't Ask) instead of get-mutate-set. 4. If you must expose state, return immutable types or defensive copies, and expose concepts, not raw fields. ## Cost / nuance This is design judgment, not a hard rule. Over-modeling trivial data carriers with elaborate behavior is its own waste; some layers (DTOs, view models) really are just data. The senior skill is matching the approach to the role of the class: rich behavior + tight hiding for domain objects; plain accessors/records for boundary data.
- What is 'Tell, Don't Ask' and how does it relate to information hiding?It's the guideline to tell an object to perform an operation rather than asking for its data and acting on it externally. It keeps decisions and invariants inside the object that owns the data, which is the strongest form of hiding — callers depend on behavior, not on the internal representation.
- Are getters and setters always bad?No. They're appropriate for DTOs, serialization/framework boundaries, and genuine read queries. The anti-pattern is reflexively pairing them for every field on a domain object, which re-exposes the representation and enables invalid state. Match the approach to the class's role.
saying these in an interview costs you the question
- Auto-generating a get/set pair for every field and calling it encapsulation.
- Letting callers get state, mutate it, and set it back — duplicating invariants outside the class.
- Adding setters for fields that should be immutable, enabling illegal state transitions.
- Exposing raw internal fields/types through getters instead of stable concepts.