7 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Encapsulation
1. What Is It?
is the practice of bundling an object's data (fields/attributes) and the methods that operate on that data into a single unit — the class — while controlling which parts of that unit are accessible to the outside world. It is one of the four pillars of OOP alongside , , and .
Without , any external code can freely read and modify an object's internal . This creates invisible dependencies: a BankAccount's balance field can be set to a negative number by any caller, not just the withdraw method that enforces business rules. Encapsulation enforces that changes flow through a controlled , making invariants enforceable and the class independently evolvable.
Encapsulation vs. Data Hiding: These terms are related but not synonymous. Encapsulation is the structural mechanism — bundling data and behavior together. Data hiding is a goal it enables — restricting unauthorized access to internal state. A class can be encapsulated (organized as a unit) without being tightly hidden (e.g., public fields), but strong data hiding is typically implemented through encapsulation.
A BankAccount class exposes its balance field as public, allowing any part of the codebase to directly set it to any value, including negative numbers. A developer argues that since all the relevant data and methods are still defined inside the BankAccount class, encapsulation is intact. What is the most accurate assessment of this situation?
2. How It Works
The core mechanics are:
- Declare fields with a restrictive access modifier (
privatein Java;_or__prefix convention in Python). - Expose controlled read/write access through methods (getters/setters in Java;
@propertyin Python). - Embed validation, computation, or side effects inside those accessor methods.
Mermaid Class Diagram
Python
from dataclasses import dataclass class BankAccount: def __init__(self, owner: str, initial_balance: float) -> None: self._owner = owner # Convention: "internal use" — single underscore self._balance = initial_balance def deposit(self, amount: float) -> None: if amount <= 0: raise ValueError("Deposit amount must be positive.") self._balance += amount def withdraw(self, amount: float) -> bool: if amount <= 0 or amount > self._balance: return False self._balance -= amount return True @property def balance(self) -> float: """Read-only computed access — no setter exposed.""" return self._balance @property def owner(self) -> str: return self._owner # Usage account = BankAccount("Alice", 100.0) account.deposit(50.0) print(account.balance) # 150.0 — accessed via @property, not direct field # account._balance = -999 # Possible, but violates the convention
Java
public class BankAccount { private final String owner; // Immutable after construction private double balance; public BankAccount(String owner, double initialBalance) { this.owner = owner; this.balance = initialBalance; } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive."); } this.balance += amount; } public boolean withdraw(double amount) { if (amount <= 0 || amount > this.balance) { return false; } this.balance -= amount; return true; } // Getter — no setter, so balance is read-only from outside public double getBalance() { return balance; } public String getOwner() { return owner; } } // Usage BankAccount account = new BankAccount("Alice", 100.0); account.deposit(50.0); System.out.println(account.getBalance()); // 150.0 // account.balance = -999; // Compile error — private field
A backend developer exposes a BankAccount class where _balance is a private field. External code needs to read the current balance but must never set it directly. Which design correctly enforces this in Python?
3. Variants & Comparisons
Python Access Control
Python does not enforce access at the language level. It relies on convention.
| Convention | Syntax | Enforcement | Purpose |
|---|---|---|---|
| Public | attr | None — fully accessible | Normal attribute |
| Internal convention | _attr | None — social contract only | Signal: "don't touch unless you know what you're doing" |
| Name mangling | __attr | Interpreter renames to _ClassName__attr | Prevent accidental override in subclasses |
| Dunder (special) | __attr__ | No mangling | Python reserved protocol methods (__init__, __str__) |
Name mangling with
__attrdoes not create true privacy.obj._BankAccount__balanceis still valid Python. Its primary use case is preventing subclass attribute collisions, not enforcing access control. For intent, a single underscore is the Pythonic convention.
Java Access Modifiers (most to least restrictive)
| Modifier | Keyword | Accessible From |
|---|---|---|
| Private | private | Declaring class only |
| Package-private | (none) | Same package |
| Protected | protected | Same package + any subclass |
| Public | public | Everywhere |
Python @property vs. Java getter/setter
| Approach | Syntax for Caller | Convention | Best For |
|---|---|---|---|
Python @property | obj.balance (attribute-style) | Decorators on methods | Pythonic APIs — caller doesn't know it's a method |
Java getBalance() | obj.getBalance() (explicit call) | JavaBeans convention | Framework compatibility (Spring, JPA, Jackson) |
A junior developer on your team defines a Python class PaymentProcessor with an attribute __transaction_id. They believe this double-underscore prefix makes the attribute truly private and inaccessible from outside the class. Which of the following best describes what actually happens?
4. When to Use It (and When NOT To)
Use when:
- A field has invariants that must be maintained (e.g., balance must be non-negative).
- Internal representation may change without breaking callers (e.g., storing balance in cents vs. dollars internally).
- You want to add validation, logging, or notifications on change.
- A class's internal data structure is an implementation detail (e.g.,
ArrayList's backing array).
Do NOT over-encapsulate when:
- The class is a pure data container (e.g., a value object or DTO) where all fields are inherently public.
- Adding getters/setters for every field with no logic is just boilerplate — it provides zero protection and makes the code harder to read.
- Python dataclasses or Java records are the right tool: if the intent is "a named bundle of data," use those instead of hand-rolling accessors.
Anti-patterns:
- Anemic getters/setters: A class with
private int xandpublic int getX()/public void setX(int x)with no validation is not encapsulated — it just has more verbose syntax for public fields. The field is effectively public. - Exposing mutable internals: Returning a reference to a mutable internal collection from a getter breaks . Return a defensive copy or an unmodifiable view instead.
Decision triggers:
- If you see "this field must always be positive" → make it private, validate in setter.
- If you see "caller shouldn't depend on how this is stored" → hide the field, expose a calculated property.
- If you see a class where every setter sets a field with no logic → reconsider whether this should be a record/dataclass with public fields.
A backend developer creates a UserProfile class with a private String email field, along with getEmail() and setEmail(String email) methods — but setEmail simply assigns the value with no validation or logic. Which of the following best describes this design?
5. Real-World Usage
1. Java's java.util.ArrayList
The backing array (transient Object[] elementData) is package-private (not public) in modern Java. Callers interact only through add(), get(), remove(), etc. This lets the JDK team resize the array, change its type, or swap the implementation entirely without breaking any code that uses ArrayList. If elementData were public, any refactor would be a breaking API change.
2. Python's @property in Django ORM models
Django model fields use property-like descriptors to intercept attribute access. When you write user.email, Django's descriptor system may lazily load, validate, or translate the value. From the caller's perspective it looks like a plain attribute — that's enabling the ORM magic without changing the public API.
3. Java's LocalDate (immutable )
java.time.LocalDate stores day, month, and year as private fields and provides no setters. All "modification" returns a new instance. This makes the class thread-safe by design and ensures the date always represents a valid calendar date — the constructor validates it. Encapsulation of the fields is what makes immutability enforceable.
The java.time.LocalDate class stores its day, month, and year as private fields and provides no setters — all 'modification' methods return a new instance instead. What is the primary benefit of this design?
6. Interview Cheat Sheet
Key sentences to say:
- " bundles and behavior into a class and controls access to that through a defined — it's not just about making fields private, it's about ensuring state changes go through methods that can enforce invariants."
- "Data hiding is a goal; is the mechanism. You can have encapsulation without data hiding, but in practice they go together."
- "Anemic getters and setters with no validation aren't encapsulation — they're public fields with extra steps. Real encapsulation means the accessor does meaningful work."
- "In Python, privacy is enforced by convention, not the language — a single underscore signals intent. In Java,
privateis compiler-enforced." - "Encapsulation directly enables the Open/Closed Principle: by hiding the internal representation, I can change it without breaking callers."
Common follow-up questions:
| Question | Concise Answer |
|---|---|
| "What's the difference between encapsulation and abstraction?" | Abstraction hides what the implementation does (complexity); encapsulation hides how the data is stored (access). Both use interfaces/methods as the boundary. |
| "Why not just use public fields?" | You lose the ability to add validation, change the internal representation, or trigger side effects without breaking all callers. |
"Is Python's __attr actually private?" | No. It triggers name mangling (_ClassName__attr) which prevents accidental subclass collisions — not true access restriction. |
| "When would you expose a mutable field?" | Almost never from a class that owns it. Return an unmodifiable view or defensive copy to prevent external mutation of internal state. |
| "How does encapsulation relate to thread safety?" | Encapsulation is a prerequisite. You can only synchronize on internal state changes if that state is private — otherwise callers can mutate fields directly, bypassing your locks. |
Connections to other concepts:
- SRP: A well-encapsulated class tends to have a single responsibility — it owns one piece of state and all methods that operate on it.
- Open/Closed Principle: Encapsulation enables OCP — hiding internals means you can change implementation without modifying the public that other classes depend on.
- : Encapsulation behind an interface lets you swap implementations; DIP says you should depend on that interface, not the concrete class.
- Immutability: The strictest form of encapsulation — private fields with no setters, all state set at construction.
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.