01Where does low-level design sit, between an idea and the code?
After high-level design (services and databases) and before the code. It names the classes, what each one owns, and how they call each other.
02In a requirement, what do the nouns and the verbs usually become?
Nouns become candidate classes. Verbs become methods, on the class that owns the data they change.
In college you met four words: encapsulation, abstraction, inheritance and polymorphism. Most people can define them. Fewer can say which bug each one stops.
In this lesson, all four appear in one small module: the checkout of a food delivery app that takes a wallet, UPI and a card. The code grows by one pillar at a time.
Classes and objects
A describes a kind of thing. An is one thing built from it.
┌────────────────────────────────────────────┐ │ │ │ ┌──────────────────┐ │ │ │ Wallet (class) │ │ │ ├──────────────────┤ │ │ │ - balance │ │ │ │ + pay(amount) │ │ │ └────────┬─────────┘ │ │ new │ new │ │ ┌─────────┴────────┐ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ asha: Wallet │ │ ravi: Wallet │ │ │ │ balance 300 │ │ balance 1200 │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ - private + public │ │ │ └────────────────────────────────────────────┘
new Wallet(300)builds an object. Asha's wallet and Ravi's wallet are two objects of one class.- Paying from Asha's wallet changes only her balance.
The four pillars are rules for how classes guard their data and share their work.
1. Encapsulation: guard the data
Say balance is a public field. Then any class in the app can write wallet.balance = -500, and nothing stops it. Every caller must remember the rule "never below zero", and one of them will forget.
- Public balanceanyone writes it
- -any class can set balance to -500
- -the rule is copied into every caller
- -a bug can come from anywhere
- Private balanceonly pay() writes it
- +outside code cannot touch balance
- +the rule lives in one method
- +a bug can come from one place
means keep the data private, and change it only through methods that check the rules.
class Wallet {
private int balance;
Wallet(int balance) { this.balance = balance; }
boolean pay(int amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
int getBalance() { return balance; }
}paychecks the rule once: no zero or negative amounts, and no more than the balance. A wallet with 300 refuses 450 and accepts 200, which leaves 100.getBalancelets others read the balance. Nothing lets them write it.- In Java,
wallet.balance = -500does not compile: "balance has private access in Wallet". The TypeScript compiler stops it too. - Python has no private fields. The leading underscore in
_balancesays "do not touch", and linters and reviewers enforce it.
2. Abstraction: show what, hide how
The checkout needs to take money. It does not need to know about PINs, bank calls or wallet balances.
gives the checkout a short list of what a payment method does, and hides how. Here the list has one item: pay(amount).
abstract class PaymentMethod {
abstract boolean pay(int amount);
}
class Checkout {
boolean pay(PaymentMethod method, int amount) {
return method.pay(amount);
}
}PaymentMethodis . It says that every payment method canpay, but not how.- You cannot create a bare
PaymentMethod. Java and TypeScript refuse to compile it. Python raises aTypeErrorwhen you try. Checkoutknows onlypay. When a bank changes its API,Checkoutdoes not change.
3. Inheritance: share the common part
Every payment method prints the same receipt: "Paid 450 by UPI asha@upi". Write that once, in the parent, and let each child add only what is different.
means a child class reuses its parent's code and fills in its own part.
abstract class PaymentMethod {
private final String label;
PaymentMethod(String label) { this.label = label; }
abstract boolean pay(int amount);
String receipt(int amount) {
return "Paid " + amount + " by " + label;
}
}
class Upi extends PaymentMethod {
Upi(String upiId) { super("UPI " + upiId); }
@Override
boolean pay(int amount) {
System.out.println("UPI: ask for PIN, send " + amount);
return true;
}
}
class Card extends PaymentMethod {
Card(String last4) { super("card " + last4); }
@Override
boolean pay(int amount) {
System.out.println("Card: ask the bank to charge " + amount);
return true;
}
}super(...)passes the label up to the parent's constructor. The parent stores it and builds the receipt.- Each child writes only
pay.receiptcomes from the parent, unchanged. Walletjoins the family the same way:extends PaymentMethod, andsuper("wallet")as the first line of its constructor. Itspaystays as it was.
Before you extend a class, say the sentence "a Card is a PaymentMethod". If the sentence sounds wrong, do not inherit. DRY, KISS, YAGNI and composition shows the other tool, composition, for "has a".
Here is the whole module:
┌────────────────────────────────────────────────────┐ │ │ │ ┌───────────────────┐ │ │ │ Checkout │ │ │ ├───────────────────┤ │ │ │ + pay(methods) │ │ │ └─────────┬─────────┘ │ │ │ uses │ │ ▼ │ │ ┌───────────────────┐ │ │ │ PaymentMethod │ │ │ │ (abstract) │ │ │ ├───────────────────┤ │ │ │ - label │ │ │ ├───────────────────┤ │ │ │ + pay(amount) │ │ │ │ + receipt(amount) │ │ │ └───────────────────┘ │ │ △ │ │ ┌──────────────┼──────────────┐ │ │ │ │ │ │ │ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │ │ │ Wallet │ │ Upi │ │ Card │ │ │ ├───────────┤ ├───────────┤ ├───────────┤ │ │ │ - balance │ │ + pay() │ │ + pay() │ │ │ ├───────────┤ └───────────┘ └───────────┘ │ │ │ + pay() │ │ │ └───────────┘ │ │ │ │ ──▶ uses ──▷ extends - private + public │ │ │ └────────────────────────────────────────────────────┘
Checkoutpoints only atPaymentMethod. It has no line toWallet,UpiorCard.- The hollow head (
△) means "extends". Each child writes its ownpay. labelis private to the parent. The children reach it only throughreceipt.
4. Polymorphism: one call, many forms
Asha has 300 in her wallet and orders food for 450. The checkout should try her wallet first, then UPI, then her card.
class Checkout {
String pay(List<PaymentMethod> methods, int amount) {
for (PaymentMethod method : methods) {
if (method.pay(amount)) return method.receipt(amount);
}
return "Payment failed";
}
}
// Asha has 300 in her wallet and orders food for 450.
List<PaymentMethod> methods =
List.of(new Wallet(300), new Upi("asha@upi"), new Card("4242"));
System.out.println(new Checkout().pay(methods, 450));The run prints UPI: ask for PIN, send 450, then Paid 450 by UPI asha@upi. The wallet said no, UPI said yes, and the card was never asked.
The loop has one line that matters: method.pay(amount). The same call runs three different methods, picked at run time by the object's real class. That is : one call, many forms.
How it runs
- Checkout to Wallet:pay(450)
- Wallet to Checkout:false
- Checkout to Upi:pay(450)
- Upi to Checkout:true
- Checkout to Upi:receipt(450)
- Upi to Checkout:Paid 450 by UPI
The change request
"Add net banking." Without polymorphism, the checkout would hold a chain of checks on the type:
- A check on the typeinside Checkout
- -if wallet, check the balance
- -else if UPI, ask for a PIN
- -else if card, call the bank
- -net banking edits this chain
- One callmethod.pay(amount)
- +each class knows its own steps
- +Checkout stays the same
- +net banking is one new class
With polymorphism, net banking is one new class, NetBanking extends PaymentMethod, with its own pay. Checkout does not change. A run with a wallet and net banking prints Paid 450 by net banking HDFC.
The four pillars side by side
| Pillar | What it is | The test | The bug it stops |
|---|---|---|---|
| Encapsulation | private data, guarded methods | can outside code break a rule? | a balance of -500 |
| Abstraction | what, not how | does the caller know only pay? | checkout breaks on a bank change |
| Inheritance | a child reuses its parent | is a Card a PaymentMethod? | copied receipt code that drifts |
| Polymorphism | one call, many forms | does a new type edit the caller? | an if-else chain on the type |
The pillars work as a team. Abstraction gives the list of methods, inheritance lets each class fill it in, and polymorphism lets one caller use them all.
Abstract class or interface?
Interviewers often ask why PaymentMethod is an abstract class and not an interface.
- An abstract class can hold fields and shared code.
PaymentMethodholdslabelandreceipt, so it fits. - An interface is a list of methods that unrelated classes can promise. A class can implement many interfaces, but extend only one class.
If PaymentMethod had only pay, an interface would be the lighter choice. Python has no separate keyword: an abc.ABC class plays both roles.
- 01
Encapsulation: keep data private and change it only through methods that check the rules.
- 02
Abstraction: callers see a short list of what an object does, such as
pay(amount), and nothing of how. - 03
Inheritance: a child reuses its parent's code. Use it only when "is a" sounds right.
- 04
Polymorphism: one call runs different code for each class, so a new payment method is one new class.
Read more
- Polymorphism and abstract classes in the Java tutorial
- abc, abstract base classes in the Python docs
- Classes in the TypeScript handbook:
private,abstractandextends - InputStream, the JDK's abstract class from the example
Next: DRY, KISS, YAGNI and composition, four rules that keep a design small.