Encapsulation: getters and setters

If any code can write acc.balance = -5000, no rule in your bank class can protect the money. Encapsulation hides an object's data behind methods that check every change. You'll see private fields, getters and setters with validation, read-only and immutable classes, defensive copies, and then practise with 10 problems.

Module 7 · OOP Pillars⏱ 22 minIntermediate10 practice problems
1 The story

The wallet that went negative

Riya's team built a canteen wallet for the college fest. Students top up, the canteen deducts. On day two, the report shows a student with a balance of Rs −4,800. Someone's test code had written wallet.balance = -4800; directly, and nothing stopped it.

public balance: anyone can write Wallet balance = -4800 test codereport jobUI screen private balance: one guarded door Wallet balance 200 deposit() withdraw() every change passes a check first myphatshala.in
Riya

Sir, our withdraw method checks for enough balance. How did the balance still go negative?

Arjun Sir

Because balance was public. Your check lives in withdraw(), but any code could skip the method and write the field directly. A rule that can be skipped is not a rule.

Riya

So I should make it private?

Arjun Sir

Yes. Make the data private and let the outside world touch it only through methods that check every change. That idea is called encapsulation, the first of the four pillars of OOP.

2 Definition

What is encapsulation?

Formal definition (write this in exams)

Encapsulation is the OOP principle of bundling an object's data (fields) and the methods that operate on that data into a single unit (a class), and restricting direct access to the data. In Java this is done by declaring fields private and exposing controlled access through public methods such as getters (accessors) and setters (mutators), which can validate values and keep the object in a valid state. Hiding the data this way is also called data hiding.

In simple words

An ATM, not an open cash box. You can't reach in and take notes; you press buttons, and the machine checks your PIN and balance before anything changes.

Words you'll meet in this lesson

Data hidingMaking fields private so other classes can't read or write them directly.
GetterA method that returns a field's value: getBalance(), isActive().
SetterA method that changes a field, usually after checking the new value: setAge(int).
InvariantA rule that must always be true for the object, like "balance is never negative".
Read-only propertyA field with a getter but no setter, often also final.
Defensive copyA copy of a mutable object (array, list) taken in or handed out, so outsiders can't change the original.

Encapsulation at a glance

Fieldsprivate: visible only inside the class itself (Lesson 6.5)
Getter naminggetX(); for a boolean, isX() is also standard (isActive())
Setter namingvoid setX(Type x); it may reject bad values by throwing IllegalArgumentException
Read-onlyGetter only, no setter. Mark the field final if it never changes after the constructor.
Immutable classfinal class, private final fields, no setters, defensive copies of mutable parts. Example: String.
Checked when?private access is checked by the compiler; validation inside setters runs at run time

Exam tip: "data hiding" (private fields) is the means; encapsulation (data + methods in one unit, with controlled access) is the principle. A class with private fields and a public setter that accepts anything is hidden but not well encapsulated: the setter must protect the rule.

3 Types and ways to use it

Three kinds of access you can design

Read-write

Getter and setter. The setter checks the value first: setPrice(-10) is rejected.

get + validated set

Read-only

Getter only. Account numbers, roll numbers and IDs are set once in the constructor.

get, final field

Immutable

Nothing can change after creation. "Changing" it returns a new object, like String.

final everything

All three in one class

class Product {
    private final String sku;      // read-only
    private double price;          // read-write, validated

    Product(String sku, double price) {
        this.sku = sku;
        setPrice(price);           // reuse the same check
    }

    public String getSku() { return sku; }
    public double getPrice() { return price; }

    public void setPrice(double price) {
        if (price <= 0) {
            throw new IllegalArgumentException("Price must be positive: " + price);
        }
        this.price = price;
    }

    public double getPriceWithGst() {   // computed, no field behind it
        return price * 1.18;
    }
}

public class Main {
    public static void main(String[] args) {
        Product p = new Product("TEA-250", 120);
        p.setPrice(150);
        System.out.println(p.getSku() + " costs Rs " + p.getPrice());
        System.out.println("With 18% GST: Rs " + p.getPriceWithGst());
        try {
            p.setPrice(-10);
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
        System.out.println("Price is still Rs " + p.getPrice());
    }
}
TEA-250 costs Rs 150.0 With 18% GST: Rs 177.0 Rejected: Price must be positive: -10.0 Price is still Rs 150.0
Public fieldPrivate field + methods
Bad valuesAnyone can store -5000The setter or method rejects them
Change the inside laterEvery caller breaksCallers keep calling the same methods
Read-only possible?Only with final, and only for one value foreverYes: give a getter, no setter
Logging, auditing a changeImpossible; writes are invisibleAdd one line inside the method
Computed valuesMust be stored and kept in syncgetPriceWithGst() computes on demand

Classic mistake: a getter that returns a private array or list hands out a reference to the original. The caller can then change your private data without any setter. Return a copy (Arrays.copyOf, List.copyOf) instead. You'll see this in the animation and the worked examples.

4 Step by step

Encapsulating a bank account

  1. Make every field private

    private final String accountNo;
    private double balance;
    private is checked by javac. Code in another class that writes acc.balance doesn't compile, so no bytecode that touches the field from outside can even exist.
  2. Validate in the constructor

    BankAccount(String accountNo, double opening) {
        if (opening < 0) throw new IllegalArgumentException("Opening balance cannot be negative");
        this.accountNo = accountNo;
        this.balance = opening;
    }
    The object is checked before anyone gets a reference to it. If the constructor throws, the caller never receives a half-valid object.
  3. Give getters for what others may read

    public String getAccountNo() { return accountNo; }
    public double getBalance()   { return balance; }
    For a primitive like double, the getter returns a copy of the value. The caller can change its copy freely; the field is untouched.
  4. Change data only through meaningful methods

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }
    Prefer deposit and withdraw over a raw setBalance. A bank never "sets" a balance; it records money in and money out. Each method is the one place where the rule is enforced.
  5. Protect mutable parts with copies

    public double[] getLastFive() {
        return Arrays.copyOf(lastFive, lastFive.length);
    }
    Arrays and lists are objects on the heap. Returning the field returns its address. Returning a copy gives the caller a separate object, so your private array stays private.
5 See it happen

Public field vs guarded methods: both views

Left: the line being run and what the console shows. Right: the account object on the heap and which "door" the change came through.

Protecting balance 6 steps
What you see acc.balance = -5000; // public acc.balance = -5000; // private acc.deposit(2000); acc.withdraw(50000); acc.deposit(-500); double b = acc.getBalance(); b = 0; Balance: Rs -5000.0 After deposit: Rs 12000.0 Withdraw 50000 allowed? false Balance still: Rs 12000.0 b → 0.0 acc.getBalance() → 12000.0 error: balance has private access in BankAccount Exception in thread "main" java.lang.IllegalArgumentException: Deposit must be positive Nothing stopped a negative balance. Now the compiler refuses the line. The only way in: a method with a check. Rule broken? The method says no. A bad argument is rejected loudly. The getter gave out a copy of the value. myphatshala.in
Behind the screen Stack (main) Heap acc → #1 b 0.0 #1 BankAccount accountNo"SB-1001" balance -5000.0 10000.0 12000.0 private fields: methods only public: written directly deposit withdraw get No check stands between the caller and the field. javac blocks it: no bytecode is ever produced. deposit: 2000 > 0 ✓, then balance += 2000. withdraw: 50000 > 12000 ✗, return false. deposit: -500 ≤ 0 ✗, throw before any change. get copies the double into b; the field stays. myphatshala.in
6 Real-world use

Encapsulation in real systems

Every serious Java codebase keeps fields private. What differs is how much logic the methods hold.

Watch it with an example: a wallet that protects its balance

The balance field is private. Outside code can't touch it directly; it must go through withdraw(), which checks the rules first.

private balance, public withdraw() 6 steps
Animated example Wallet w = new Wallet(500); w.balance = 1000000; w.withdraw(200); w.withdraw(200); w.withdraw(900); w.getBalance(); #1 Wallet private int balance 500 500 500 300 300 300 public withdraw(amt) if (amt > balance) throw … outside code ✗ compile error: balance has private access in Wallet 200 ≤ 500 ✓ balance 500 → 300 900 > 300 ✗ IllegalArgument… balance unchanged read-only access returns 300 Only the class's own methods can change balance. myphatshala.in
balance

1. Banking and wallets

Account classes expose deposit, withdraw and transfer, never setBalance. Limits, minimum balance and freezing rules all live inside those methods, so no screen or batch job can skip them.

UPI request→account.withdraw(amt)→Rule checked
getName()setName(..)isActive()

2. JavaBeans and frameworks

Frameworks that map objects to database rows, JSON or web forms commonly find properties through the getX/setX/isX naming convention. Following it lets those tools read and fill your objects.

JSON / DB row→setName(), setAge()→Java object
Moneyfinal, no setters

3. Immutable value objects

Money, dates, coordinates and configuration are often immutable. They can be shared between threads and cached safely, because nobody can change them after creation. String and LocalDate work this way.

amount.add(gst)→returns new Money→Original unchanged
originalcopy

4. Safe sharing of internal lists

An order returns its line items as an unmodifiable list or a copy, so a reporting module that sorts or filters the result can't corrupt the order itself.

order.getItems()→List.copyOf(items)→Order stays intact

Examples are simplified. Real systems also check rules in the database and service layers, but the object guarding its own state is the first and cheapest line of defence.

7 Run the code

Riya's wallet, encapsulated

class BankAccount {
    private final String accountNo;
    private double balance;

    BankAccount(String accountNo, double opening) {
        if (opening < 0) throw new IllegalArgumentException("Opening balance cannot be negative");
        this.accountNo = accountNo;
        this.balance = opening;
    }

    public String getAccountNo() { return accountNo; }
    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount("SB-1001", 10000);
        acc.deposit(2000);
        System.out.println("After deposit: Rs " + acc.getBalance());
        boolean ok = acc.withdraw(50000);
        System.out.println("Withdraw 50000 allowed? " + ok);
        System.out.println("Balance still: Rs " + acc.getBalance());
        System.out.println("Account: " + acc.getAccountNo());
    }
}
After deposit: Rs 12000.0 Withdraw 50000 allowed? false Balance still: Rs 12000.0 Account: SB-1001
  • private final String accountNo;Read-only: set once in the constructor, getter only, no setter.
  • private double balance;Changed only by deposit and withdraw, which check every request.
  • throw new IllegalArgumentException(...)For a caller's mistake (negative deposit). Exceptions are covered in Module 9.
  • return false;For a normal business "no" (not enough money). The caller decides what to show the user.
8 Line by line

A wallet that refuses a bad top-up

Watch the add frame appear on top of main, check the amount, and either change the object or leave it alone.

Worked examples: real problems, solved and traced in memory

Three real jobs where an object must guard its own data. Run each solution, then step through it: watch method frames push and pop, and see that the private fields of each object (#1, #2 …) change only inside its own methods.

Example 1Ed-tech · results portal

A setter that refuses impossible marks

Problem statement

A results portal lets teachers enter marks. A typo like 105 must never be saved. Write a Student class whose setMarks accepts only 0 to 100 and reports whether the value was saved.

Input
Student "Aman"; setMarks(105), then setMarks(88)
Expected output
false true 88

Approach

  1. Make marks private so the setter is the only way to change it.
  2. In setMarks, check the range first and return false without touching the field if it fails.
  3. Otherwise store the value and return true.
  4. Read the final value through getMarks().

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Fintech · UPI wallet

A wallet with a daily spending limit

Problem statement

A student wallet allows at most Rs 2,000 of payments per day. pay(amount) must refuse a payment if the balance is too low or if it would cross the daily limit. Pay Rs 1,500 and then Rs 800, starting with Rs 5,000.

Input
balance 5000, daily limit 2000; pay 1500, pay 800
Expected output
Pay 1500: true
Pay 800: false
Balance: 3500

Approach

  1. Keep balance and spentToday private: both are changed together by one method.
  2. pay checks both rules before changing anything.
  3. If allowed, update both fields; the object can never be half-updated.
  4. Expose only getters for reading.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3E-commerce · orders

A defensive copy keeps the order safe

Problem statement

An Order is created from an array of item quantities. Later, the cart screen reuses its array for the next customer and sets it to zeros. The placed order must keep its original quantities.

Input
cart = {2, 1}; order created; then cart[0] = 0
Expected output
Order items: 3

Approach

  1. In the constructor, store a copy of the array, not the caller's array.
  2. The caller's later change hits only its own array.
  3. Compute the item count from the order's private copy.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 7.1 in one screen

  • Encapsulation = data + methods in one class, with the data hidden behind those methods.
  • Fields private; access through getX() / isX() and setX() or business methods.
  • Setters and constructors validate; bad values are rejected (exception or false).
  • Read-only: getter, no setter, field often final.
  • Immutable: final class, private final fields, no setters, "changes" return new objects.
  • Never return or store a private array/list directly: make a defensive copy.
  • private is checked at compile time; validation runs at run time.

Interview and viva questions

What is the difference between encapsulation and data hiding?

Data hiding is making fields inaccessible from outside (with private). Encapsulation is the bigger idea: keeping data and the methods that work on it together in one class, and allowing changes only through those methods so the object stays valid. Data hiding is how Java achieves encapsulation.

If a class has a getter and a setter for every field, is it encapsulated?

Only partly. If the setters accept any value, the object can still be put into an invalid state; you've only added ceremony. Good encapsulation exposes meaningful operations (deposit, withdraw) and validates, and leaves out setters that shouldn't exist.

Why are getters and setters better than public fields?

They let the class validate changes, make some properties read-only, compute values on demand, log or audit changes, and change the internal representation later without breaking callers. A public field allows none of that.

How do you create an immutable class in Java?

Declare the class final (so subclasses can't add mutable behaviour), make all fields private final, set them in the constructor, provide no setters, take defensive copies of mutable inputs and return copies (or unmodifiable views) of mutable fields. "Modifying" methods return a new object.

What is a defensive copy and when do you need one?

A copy of a mutable object made when it enters or leaves your class. You need one when a field holds an array, a list, a Date or any other mutable object: otherwise the caller who passed it in, or who received it from a getter, still holds a reference and can change your private state.

Practice problems

Each problem asks you to design a small class that protects its own data. Decide first which fields are private, which get getters, which get setters (if any) and what each method must check. The last three are business problems.

P1Easy

Student with private fields

Create a Student class with private name and marks, a constructor, and getters. Print the student's details from main using only the getters.

Input
name = "Riya", marks = 86
Expected output
Riya scored 86
Show solution and explanation
class Student {
    private String name;
    private int marks;

    Student(String name, int marks) {
        this.name = name;
        this.marks = marks;
    }

    public String getName() { return name; }
    public int getMarks() { return marks; }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student("Riya", 86);
        System.out.println(s.getName() + " scored " + s.getMarks());
    }
}
Riya scored 86

How it works: the fields are private, so main cannot write s.marks. It reads the values through the getters. The constructor is the only place the values are set, which makes both of them effectively read-only.

P2Easy

Setter that rejects bad marks

Add setMarks(int) to a Student class. Marks must be between 0 and 100; otherwise print a message and keep the old value. Try 92, then 140.

Input
setMarks(92), then setMarks(140)
Expected output
Marks: 92
Rejected: 140 is not between 0 and 100
Marks: 92
Show solution and explanation
class Student {
    private int marks;

    public void setMarks(int marks) {
        if (marks < 0 || marks > 100) {
            System.out.println("Rejected: " + marks + " is not between 0 and 100");
            return;
        }
        this.marks = marks;
    }

    public int getMarks() { return marks; }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        s.setMarks(92);
        System.out.println("Marks: " + s.getMarks());
        s.setMarks(140);
        System.out.println("Marks: " + s.getMarks());
    }
}
Marks: 92 Rejected: 140 is not between 0 and 100 Marks: 92

How it works: the setter is the only door to the field, so the range check runs for every change. return leaves the method before the assignment, so 140 never reaches the field. (In larger programs you'd usually throw an exception instead of printing; see Module 9.)

P3Easy

Read-only roll number

A Student has a roll number that must never change after admission and a phone number that may change. Design the class so the roll number has no setter and is final.

Input
roll = "CS-042", phone = "98200 11111" then "98200 22222"
Expected output
CS-042 -> 98200 22222
Show solution and explanation
class Student {
    private final String rollNo;
    private String phone;

    Student(String rollNo, String phone) {
        this.rollNo = rollNo;
        this.phone = phone;
    }

    public String getRollNo() { return rollNo; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student("CS-042", "98200 11111");
        s.setPhone("98200 22222");
        System.out.println(s.getRollNo() + " -> " + s.getPhone());
    }
}
CS-042 -> 98200 22222

How it works: rollNo has a getter only, so other classes can't change it, and final means even the class's own methods can't reassign it after the constructor. phone is read-write. Try adding rollNo = "X"; inside setPhone: javac refuses.

P4Easy

Computed property: Celsius and Fahrenheit

Store a temperature only in Celsius (private). Provide getCelsius(), getFahrenheit() (computed) and setCelsius() that rejects values below absolute zero (−273.15).

Input
setCelsius(37), then setCelsius(-300)
Expected output
37.0 C = 98.6 F
Below absolute zero, ignored: -300.0
Still 37.0 C
Show solution and explanation
class Temperature {
    private double celsius;

    public void setCelsius(double c) {
        if (c < -273.15) {
            System.out.println("Below absolute zero, ignored: " + c);
            return;
        }
        celsius = c;
    }

    public double getCelsius() { return celsius; }
    public double getFahrenheit() { return celsius * 9 / 5 + 32; }
}

public class Main {
    public static void main(String[] args) {
        Temperature t = new Temperature();
        t.setCelsius(37);
        System.out.println(t.getCelsius() + " C = " + t.getFahrenheit() + " F");
        t.setCelsius(-300);
        System.out.println("Still " + t.getCelsius() + " C");
    }
}
37.0 C = 98.6 F Below absolute zero, ignored: -300.0 Still 37.0 C

How it works: only one value is stored, so Celsius and Fahrenheit can never disagree. getFahrenheit() looks like a normal getter to callers, but computes its answer. That's a benefit of methods over public fields: the inside can change without callers noticing.

P5Medium

Account with a minimum balance

A savings account must always keep at least Rs 1,000. withdraw returns true if allowed, false otherwise. Start with Rs 5,000, then try to withdraw 3,500 and then 1,000.

Input
opening 5000, withdraw 3500, withdraw 1000
Expected output
Withdraw 3500: true, balance 1500.0
Withdraw 1000: false, balance 1500.0
Show solution and explanation
class SavingsAccount {
    private static final double MIN_BALANCE = 1000;
    private double balance;

    SavingsAccount(double opening) {
        balance = opening;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || balance - amount < MIN_BALANCE) {
            return false;
        }
        balance -= amount;
        return true;
    }

    public double getBalance() { return balance; }
}

public class Main {
    public static void main(String[] args) {
        SavingsAccount acc = new SavingsAccount(5000);
        System.out.println("Withdraw 3500: " + acc.withdraw(3500) + ", balance " + acc.getBalance());
        System.out.println("Withdraw 1000: " + acc.withdraw(1000) + ", balance " + acc.getBalance());
    }
}
Withdraw 3500: true, balance 1500.0 Withdraw 1000: false, balance 1500.0

How it works: after the first withdrawal, 1,500 is left. The second would leave 500, below the minimum, so it's refused and the balance stays 1,500. The rule is written once, inside the class; no caller can forget it.

P6Medium

Write-only PIN with a check method

An ATM card stores a 4-digit PIN. There must be no getter for the PIN. Provide changePin(old, new) and verify(pin). A new PIN must have exactly 4 digits.

Input
initial PIN "4321"; verify "1111"; changePin("4321", "98"); changePin("4321", "2580"); verify "2580"
Expected output
verify 1111: false
change to 98: false
change to 2580: true
verify 2580: true
Show solution and explanation
class AtmCard {
    private String pin;

    AtmCard(String pin) {
        this.pin = pin;
    }

    public boolean verify(String attempt) {
        return pin.equals(attempt);
    }

    public boolean changePin(String oldPin, String newPin) {
        if (!verify(oldPin)) return false;
        if (newPin.length() != 4 || !newPin.chars().allMatch(Character::isDigit)) return false;
        pin = newPin;
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        AtmCard card = new AtmCard("4321");
        System.out.println("verify 1111: " + card.verify("1111"));
        System.out.println("change to 98: " + card.changePin("4321", "98"));
        System.out.println("change to 2580: " + card.changePin("4321", "2580"));
        System.out.println("verify 2580: " + card.verify("2580"));
    }
}
verify 1111: false change to 98: false change to 2580: true verify 2580: true

How it works: encapsulation doesn't mean “a getter and setter for everything”. The PIN can be checked but never read. chars().allMatch(Character::isDigit) checks every character is a digit (streams are covered in Module 14; a simple loop works too).

P7Medium

Defensive copy of a marks array

A ReportCard receives an int[] of marks. Make sure that changing the caller's array later, or changing the array returned by the getter, does not change the report card.

Input
marks = {70, 85, 90}; caller sets marks[0] = 0; getter result [1] set to 0
Expected output
input:  [0, 85, 90]
got:    [70, 0, 90]
stored: [70, 85, 90]
Show solution and explanation
import java.util.Arrays;

class ReportCard {
    private final int[] marks;

    ReportCard(int[] marks) {
        this.marks = Arrays.copyOf(marks, marks.length);
    }

    public int[] getMarks() {
        return Arrays.copyOf(marks, marks.length);
    }
}

public class Main {
    public static void main(String[] args) {
        int[] input = {70, 85, 90};
        ReportCard rc = new ReportCard(input);
        input[0] = 0;
        int[] got = rc.getMarks();
        got[1] = 0;
        System.out.println("input:  " + Arrays.toString(input));
        System.out.println("got:    " + Arrays.toString(got));
        System.out.println("stored: " + Arrays.toString(rc.getMarks()));
    }
}
input: [0, 85, 90] got: [70, 0, 90] stored: [70, 85, 90]

How it works: copies are taken at both doors: on the way in (constructor) and on the way out (getter). Three separate arrays exist, so the two changes hit only the outside copies. final on the field stops reassigning it, but would not stop changing the elements; only the copies do that.

P8Business problem

GST invoice line

An invoice line has a product name, unit price and quantity. Price and quantity must be positive. Provide computed getters for the taxable amount, GST (18%) and the line total. Reject a quantity of 0.

Input
"Office chair", price 4500, qty 3; then setQuantity(0)
Expected output
Office chair
Taxable Rs 13500.00, GST Rs 2430.00, Total Rs 15930.00
Rejected: Quantity must be at least 1
Total still Rs 15930.00
Show solution and explanation
class InvoiceLine {
    private final String product;
    private final double unitPrice;
    private int quantity;

    InvoiceLine(String product, double unitPrice, int quantity) {
        if (unitPrice <= 0) throw new IllegalArgumentException("Bad price");
        this.product = product;
        this.unitPrice = unitPrice;
        setQuantity(quantity);
    }

    public void setQuantity(int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("Quantity must be at least 1");
        this.quantity = quantity;
    }

    public double getTaxable() { return unitPrice * quantity; }
    public double getGst() { return getTaxable() * 0.18; }
    public double getTotal() { return getTaxable() + getGst(); }
    public String getProduct() { return product; }
}

public class Main {
    public static void main(String[] args) {
        InvoiceLine line = new InvoiceLine("Office chair", 4500, 3);
        System.out.println(line.getProduct());
        System.out.printf("Taxable Rs %.2f, GST Rs %.2f, Total Rs %.2f%n",
                line.getTaxable(), line.getGst(), line.getTotal());
        try {
            line.setQuantity(0);
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
        System.out.printf("Total still Rs %.2f%n", line.getTotal());
    }
}
Office chair Taxable Rs 13500.00, GST Rs 2430.00, Total Rs 15930.00 Rejected: Quantity must be at least 1 Total still Rs 15930.00

How it works:

  1. Only price and quantity are stored. Taxable, GST and total are computed, so they can never go out of sync after a quantity change.
  2. The constructor calls setQuantity, so the same check guards both creation and later changes.
  3. 4500 × 3 = 13,500; GST 2,430; total 15,930.
P9Business problem

Inventory item that can't go below zero

A warehouse item tracks stock. receive(n) adds stock; dispatch(n) removes it only if enough is available and returns whether it succeeded. Also count how many dispatches were refused, readable via a getter.

Input
start 20; dispatch 15; dispatch 10; receive 30; dispatch 10
Expected output
dispatch 15: true, stock 5
dispatch 10: false, stock 5
received 30, stock 35
dispatch 10: true, stock 25
RICE-25KG refused dispatches: 1
Show solution and explanation
class StockItem {
    private final String sku;
    private int stock;
    private int refused;

    StockItem(String sku, int stock) {
        this.sku = sku;
        this.stock = stock;
    }

    public void receive(int qty) {
        if (qty > 0) stock += qty;
    }

    public boolean dispatch(int qty) {
        if (qty <= 0 || qty > stock) {
            refused++;
            return false;
        }
        stock -= qty;
        return true;
    }

    public int getStock() { return stock; }
    public int getRefused() { return refused; }
    public String getSku() { return sku; }
}

public class Main {
    public static void main(String[] args) {
        StockItem rice = new StockItem("RICE-25KG", 20);
        System.out.println("dispatch 15: " + rice.dispatch(15) + ", stock " + rice.getStock());
        System.out.println("dispatch 10: " + rice.dispatch(10) + ", stock " + rice.getStock());
        rice.receive(30);
        System.out.println("received 30, stock " + rice.getStock());
        System.out.println("dispatch 10: " + rice.dispatch(10) + ", stock " + rice.getStock());
        System.out.println(rice.getSku() + " refused dispatches: " + rice.getRefused());
    }
}
dispatch 15: true, stock 5 dispatch 10: false, stock 5 received 30, stock 35 dispatch 10: true, stock 25 RICE-25KG refused dispatches: 1

How it works:

  1. refused is internal bookkeeping: it has a getter but no setter, so only the class can count.
  2. Because stock changes only in receive and dispatch, the rule “stock is never negative” holds for every object, always.
  3. Stock goes 20 → 5 → (refused) → 35 → 25.
P10Business problem

Immutable money transfer request

A UPI transfer request has a sender, receiver and amount in rupees. It must be immutable: no setters, and a method withAmount that returns a new request with a different amount. Amount must be between Rs 1 and Rs 1,00,000.

Input
from "riya@upi" to "canteen@upi", 250; then withAmount(300)
Expected output
riya@upi -> canteen@upi : Rs 250
riya@upi -> canteen@upi : Rs 300
Same object? false
Show solution and explanation
final class TransferRequest {
    private final String from;
    private final String to;
    private final long amount;

    TransferRequest(String from, String to, long amount) {
        if (amount < 1 || amount > 100_000) {
            throw new IllegalArgumentException("Amount out of range: " + amount);
        }
        this.from = from;
        this.to = to;
        this.amount = amount;
    }

    public TransferRequest withAmount(long newAmount) {
        return new TransferRequest(from, to, newAmount);
    }

    public String describe() {
        return from + " -> " + to + " : Rs " + amount;
    }
}

public class Main {
    public static void main(String[] args) {
        TransferRequest r1 = new TransferRequest("riya@upi", "canteen@upi", 250);
        TransferRequest r2 = r1.withAmount(300);
        System.out.println(r1.describe());
        System.out.println(r2.describe());
        System.out.println("Same object? " + (r1 == r2));
    }
}
riya@upi -> canteen@upi : Rs 250 riya@upi -> canteen@upi : Rs 300 Same object? false

How it works:

  1. final class stops anyone from writing a subclass that adds setters. private final fields can't be changed after the constructor.
  2. withAmount builds a new object through the same constructor, so the range check runs again.
  3. r1 is untouched, which makes such requests safe to log, retry or share between threads.

Real-world scenario questions (asked in company interviews)

Most data-corruption bugs start with state that anyone could change. Decide your answer first, then open the model answer.

Banking · core accountsFresher

The overdraft nobody approved

A savings account class has public double balance and a withdraw() method that checks the minimum balance of Rs 1,000. An audit finds accounts at Rs −2,300. The withdraw code looks correct. Where would you look, and how would you fix it?

The interviewer is checking: do you see that a public field lets code bypass the rule?

Show a strong answer
  • Search for direct writes to balance (for example a reversal job doing acc.balance -= fee). Those skip withdraw() entirely.
  • Make the field private. The compiler will then list every place that writes it, and each must be changed to call a method.
  • Add methods for the real operations (chargeFee, reverse) that apply the same rules, and add a test that a negative balance is impossible.

Avoid: adding the same check in every caller. The rule belongs in one place: the account class.

E-commerce · orders1–3 years

The report that changed real orders

An Order class keeps private List<Item> items and has getItems() that returns it. A new analytics feature removes cancelled items from the list it receives before calculating totals. Customers start getting invoices with missing items. Explain the bug and the fix.

The interviewer is checking: do you understand reference leaks through getters?

Show a strong answer
  • private protected the variable, not the list object. The getter returned a reference to the same list, so the analytics code removed items from the real order.
  • Return List.copyOf(items) (Java 10+, unmodifiable) or Collections.unmodifiableList(items). Callers that try to modify it get an exception instead of silently corrupting data.
  • Also copy the list passed into the constructor, and offer methods like addItem that validate.

Avoid: "the analytics team should be careful". The class must protect its own state.

SaaS · code review1–3 years

"The IDE generated all the setters"

In a subscription billing service, a teammate generated getters and setters for every field of Subscription, including setStatus, setPaidUntil and setPlanPrice. Support staff tools now call setPaidUntil directly to "fix" accounts. What would you change?

The interviewer is checking: can you design meaningful methods instead of blind setters?

Show a strong answer
  • Remove setters that let callers skip business rules. Replace them with operations: renew(Payment p), cancel(String reason), changePlan(Plan newPlan).
  • Each operation updates several fields together and validates (a renewal needs a successful payment; the paid-until date moves forward only).
  • Support fixes should go through an audited method such as grantExtension(days, approvedBy).

Avoid: treating "private fields + public setters for all" as encapsulation. It's a public field with extra steps.

Fintech · pricing engine3+ years

A shared rate object changed mid-calculation

A loan pricing service caches one RateCard object (with setters) and shares it across all request threads. An admin screen updates rates by calling setters on that object. Occasionally a customer's EMI is calculated with half old and half new rates. How would you redesign it?

The interviewer is checking: do you know why immutability makes shared data safe?

Show a strong answer
  • Make RateCard immutable: final class, private final fields, no setters, defensive copies of any maps or arrays.
  • To change rates, build a new RateCard and swap the shared reference in one step (for example a volatile field or an AtomicReference, Module 12).
  • Each calculation reads the reference once at the start, so it sees one consistent RateCard from start to finish.

Avoid: adding locks around every getter. Immutability removes the problem instead of managing it.