Object Oriented Programming
Object-Oriented Programming (OOP) is not an academic exercise — it is how banking systems model the real world.
Introduction
Object-Oriented Programming (OOP) is not an academic exercise — it is how banking systems model the real world. A checking account, a wire transfer, a credit card payment — each is an object with state (balance, account number) and behavior (deposit, withdraw, transfer). OOP exists because financial software must mirror domain concepts that business stakeholders, auditors, and regulators already understand.
Before OOP, banking systems were written as monolithic procedural code — thousands of functions operating on shared global state. A single bug in updateBalance() could corrupt every account in memory. OOP introduced boundaries: data and the operations on that data live together, hidden behind controlled interfaces.
Java was designed OOP-first. Every line of production banking code — from JPMorgan's trading platforms to Stripe's payment APIs — uses the four pillars: encapsulation (hide internal state), inheritance (reuse and specialize), polymorphism (one interface, many implementations), and abstraction (expose what, hide how). This lesson teaches OOP through a banking domain you can take directly into Spring Boot microservices and staff-level architecture reviews.
Business problem
Procedural and poorly encapsulated code fails in regulated banking:
- Balance corruption: Public
double balancefield modified anywhere in codebase — no audit trail, no validation, race conditions under concurrent transfers. - Duplicate logic: Checking and savings accounts each reimplement
withdraw()with slightly different overdraft rules — bug fixed in one, not the other. - Vendor lock-in: Payment processing tightly coupled to one gateway's API — switching providers requires rewriting entire service, not swapping one class.
- Untestable monoliths: 800-line
processTransaction()function — cannot unit test overdraft logic without spinning up database and message queue.
Why this topic exists
OOP exists to manage complexity in domains where state and rules intertwine:
- Model the domain: Business analysts say "transfer between accounts" — OOP maps that directly to
Account.transferTo(Account, Money). Code reads like the business. - Control change: Overdraft policy changes? Modify
CheckingAccount.withdraw()— not 47 call sites scattered across the codebase. - Enforce invariants: Account balance can never go negative on savings accounts — rule enforced inside the class, not hoped for at every call site.
- Enable polymorphic extension: Add cryptocurrency settlement without changing existing wire transfer code — new class implements
PaymentProcessorinterface. - Audit and compliance: Encapsulated operations log every state change — regulators ask "who modified this balance?" — answer is in the method, not random field assignment.
Core concepts
The four pillars of OOP — banking definitions:
- Encapsulation: Hide internal state (
private BigDecimal balance), expose controlled access via methods (deposit(Money)). Validation, logging, and locking happen inside the class boundary. - Inheritance:
SavingsAccount extends Account— child inherits common fields/methods, adds or overrides behavior (interest accrual). IS-A relationship: savings IS-A account. - Polymorphism:
Account account = new CheckingAccount();— reference type is parent, runtime type is child. Callaccount.withdraw()— JVM dispatches to checking's override. One variable, many behaviors. - Abstraction:
interface PaymentProcessor { PaymentResult process(Payment); }— callers depend on contract, not implementation. WireTransferProcessor and ACHProcessor swap without caller changes.
Internal architecture
Banking domain class hierarchy — how OOP pillars compose in a production payment system:
┌─────────────────── Abstraction Layer ───────────────────────────┐│ interface PaymentProcessor { PaymentResult process(Payment); } ││ interface AccountRepository { Account findById(String id); } │└────────────────────────────┬──────────────────────────────────────┘│ implements┌────────────────────┼────────────────────┐▼ ▼ ▼WireTransferProcessor ACHProcessor CardProcessor│ │ │└────────────────────┼────────────────────┘│ uses (polymorphism)┌────────────────────────────▼──────────────────────────────────────┐│ abstract class Account ││ - private String accountId ← encapsulation (hidden) ││ - private BigDecimal balance ││ + deposit(Money) / withdraw(Money) ← public API ││ # validateAmount(Money) ← protected helper │└──────────────┬──────────────────────────────┬─────────────────────┘│ extends (inheritance) │ extends▼ ▼class CheckingAccount class SavingsAccount+ withdraw() — overdraft OK + withdraw() — no overdraft+ linkDebitCard() + accrueInterest()
Four OOP pillar diagrams — encapsulation, inheritance, polymorphism, abstraction in banking context:
Code walkthrough
Complete banking OOP example — all four pillars with expected output and line-by-line explanation:
- Encapsulation (line 14-15):
private balance— no external code can set balance directly; all changes throughdeposit()with validation. - Abstraction (line 6):
PaymentProcessorinterface — callers depend on contract, not SWIFT vs ACH implementation details. - Inheritance (line 49, 66):
CheckingAccount extends Account— inheritsdeposit(), overrideswithdraw()with overdraft logic. - Polymorphism (line 107-113):
List<Account>holds different runtime types — samewithdraw()call, different behavior per account type.
import java.math.BigDecimal;import java.math.RoundingMode;import java.util.ArrayList;import java.util.List;// ── ABSTRACTION: interface defines contract, not implementation ──interface PaymentProcessor {String getProcessorName();boolean processPayment(String fromAccount, String toAccount, BigDecimal amount);}// ── ENCAPSULATION + INHERITANCE: abstract base class ──abstract class Account {private final String accountId; // private — hidden from outsideprivate BigDecimal balance; // no direct accessprotected Account(String accountId, BigDecimal openingBalance) {this.accountId = accountId;this.balance = openingBalance.setScale(2, RoundingMode.HALF_UP);}// Public API — controlled access with validationpublic void deposit(BigDecimal amount) {validatePositive(amount);balance = balance.add(amount);System.out.println("[" + accountId + "] Deposited " + amount + " → balance: " + balance);}// Abstract — forces subclasses to define withdrawal rules (polymorphism)public abstract boolean withdraw(BigDecimal amount);public BigDecimal getBalance() { return balance; }public String getAccountId() { return accountId; }protected void validatePositive(BigDecimal amount) {if (amount.compareTo(BigDecimal.ZERO) <= 0)throw new IllegalArgumentException("Amount must be positive");}}// ── INHERITANCE + POLYMORPHISM: specialized account types ──class CheckingAccount extends Account {private static final BigDecimal OVERDRAFT_LIMIT = new BigDecimal("500.00");CheckingAccount(String id, BigDecimal opening) { super(id, opening); }@Overridepublic boolean withdraw(BigDecimal amount) {validatePositive(amount);BigDecimal newBalance = getBalance().subtract(amount);if (newBalance.compareTo(OVERDRAFT_LIMIT.negate()) < 0) {System.out.println("[" + getAccountId() + "] Overdraft limit exceeded");return false;}// Encapsulated state change via protected access patterndeposit(amount.negate()); // reuse deposit logic (negate = withdraw)System.out.println("[" + getAccountId() + "] Withdrew " + amount);return true;}}class SavingsAccount extends Account {SavingsAccount(String id, BigDecimal opening) { super(id, opening); }@Overridepublic boolean withdraw(BigDecimal amount) {validatePositive(amount);if (getBalance().compareTo(amount) < 0) {System.out.println("[" + getAccountId() + "] Insufficient funds (no overdraft)");return false;}deposit(amount.negate());System.out.println("[" + getAccountId() + "] Withdrew " + amount);return true;}}// ── ABSTRACTION: concrete payment processor implementations ──class WireTransferProcessor implements PaymentProcessor {public String getProcessorName() { return "SWIFT-Wire"; }public boolean processPayment(String from, String to, BigDecimal amount) {System.out.println(" SWIFT wire: " + from + " → " + to + " amount: " + amount);return true;}}class ACHProcessor implements PaymentProcessor {public String getProcessorName() { return "ACH-Domestic"; }public boolean processPayment(String from, String to, BigDecimal amount) {System.out.println(" ACH transfer: " + from + " → " + to + " amount: " + amount);return true;}}// ── POLYMORPHISM in action: service works with any Account / PaymentProcessor ──class BankingDemo {public static void main(String[] args) {// Polymorphic collection — compile-time type Account, runtime types differList<Account> accounts = new ArrayList<>();accounts.add(new CheckingAccount("CHK-001", new BigDecimal("1000.00")));accounts.add(new SavingsAccount("SAV-001", new BigDecimal("5000.00")));accounts.get(0).deposit(new BigDecimal("200.00"));accounts.get(0).withdraw(new BigDecimal("1600.00")); // checking: overdraft OKaccounts.get(1).withdraw(new BigDecimal("6000.00")); // savings: rejected// Polymorphic payment processing — swap processor without changing this codePaymentProcessor processor = new WireTransferProcessor();processor.processPayment("CHK-001", "EXT-999", new BigDecimal("500.00"));}}/** Expected output:* [CHK-001] Deposited 200.00 → balance: 1200.00* [CHK-001] Deposited -1600.00 → balance: -400.00* [CHK-001] Withdrew 1600.00* [SAV-001] Insufficient funds (no overdraft)* SWIFT wire: CHK-001 → EXT-999 amount: 500.00*/
Production example
Production Spring Boot banking service — OOP pillars in a real microservice structure:
- Constructor injection — depends on
PaymentProcessorinterface (abstraction), not concrete SWIFT client. - Domain methods — business rules in
Account.withdraw(), not in REST controller — encapsulation preserves invariants. - @Profile / @ConditionalOnProperty — swap payment processor implementation per environment without code change.
// domain/Account.java — encapsulation + inheritancepublic abstract class Account {private final AccountId id;private Money balance;protected Account(AccountId id, Money openingBalance) { ... }public abstract Result<Money> withdraw(Money amount);public Result<Money> deposit(Money amount) { ... }}// domain/CheckingAccount.java — polymorphic specializationpublic class CheckingAccount extends Account {@Overridepublic Result<Money> withdraw(Money amount) {// overdraft rules enforced here — not in controller}}// application/PaymentService.java — depends on abstraction@Servicepublic class PaymentService {private final PaymentProcessor processor; // interface, not concrete classprivate final AccountRepository repository;public PaymentService(PaymentProcessor processor, AccountRepository repo) {this.processor = processor; // Spring injects Wire or ACH implthis.repository = repo;}public PaymentResult transfer(AccountId from, AccountId to, Money amount) {Account source = repository.findById(from);source.withdraw(amount); // polymorphic — checking vs savingsprocessor.process(from, to, amount);return PaymentResult.success();}}// OOP wins: swap ACHProcessor → WireTransferProcessor via @Profile or config// No change to PaymentService — dependency inversion (SOLID-D)
Enterprise case study
Capital One — domain-driven OOP in microservices migration: When Capital One decomposed its monolithic banking platform into microservices, the teams that succeeded mapped OOP domain models directly to bounded contexts. Accounts, Transactions, and PaymentInstruments became encapsulated aggregate roots — each enforcing its own invariants. Teams that copied procedural code into microservices ("REST endpoint calls SQL directly") accumulated distributed monolith debt — same balance corruption bugs, now with network latency.
- Before: Procedural transaction script — 1,200 lines, shared mutable state, untestable without full stack.
- Decision: Rich domain model — Account encapsulates balance; Transaction is immutable value object; PaymentProcessor interface for gateway swap.
- After: Unit tests run in milliseconds; overdraft policy change = one class; ACH→wire migration = new PaymentProcessor impl.
- Lesson: OOP is not ceremony — it is how you survive microservice decomposition without losing domain integrity.
Performance considerations
OOP performance considerations at banking scale:
- Virtual method dispatch: Polymorphic calls use vtable — ~1-2ns overhead per call, negligible vs I/O. JIT devirtualizes monomorphic call sites (one runtime type) to direct calls.
- Object allocation: Rich domain models create more objects than procedural code — acceptable for business logic; avoid creating objects in tight loops (use primitives).
- Inheritance depth: Deep hierarchies (5+ levels) hurt readability and JIT optimization — prefer composition over inheritance beyond 2 levels.
- Interface vs abstract class: Interface dispatch same cost as virtual methods; no performance difference — choose on design merits.
Security considerations
Encapsulation is a security boundary in banking:
- Private fields: Balance, PIN hash, account status — never public. Prevents unauthorized modification from any code with object reference.
- Validation inside methods:
withdraw()checks authorization, amount limits, and fraud rules before state change — cannot bypass via field access. - Immutable value objects:
record Money(BigDecimal amount, Currency currency)— cannot be modified after creation; thread-safe by default. - Least privilege: Protected methods for subclass access only — external packages cannot call
validateAmount().
Scalability considerations
OOP enables team and system scale in banking:
- Bounded contexts: Each microservice team owns its domain classes — Account team, Payments team, Fraud team — parallel development without merge conflicts on shared functions.
- Polymorphic extensibility: New payment rail (FedNow, RTP) = new
PaymentProcessorimplementation — zero changes to existing transfer flow. - Interface segregation: Small focused interfaces —
ReadableAccount,WritableAccount— services depend only on what they need. - Test doubles: Mock
PaymentProcessorin unit tests — test transfer logic without real SWIFT gateway — OOP abstraction enables this.
Production challenges
OOP anti-patterns that cause banking production incidents:
- Anemic domain model: Account class has only getters/setters; all logic in
AccountService— encapsulation defeated; invariants not enforced. - God class inheritance:
SuperAccount extends Account extends Entity extends BaseModel— fragile base class; parent change breaks all children. - instanceof chains:
if (account instanceof CheckingAccount) ... else if (account instanceof SavingsAccount)— violates polymorphism; add new type = modify every chain. - Public mutable fields: Lombok
@Dataon domain entity — generates public setters on balance; bypasses all validation.
Common mistakes
- Using inheritance for code reuse only — prefer composition when IS-A relationship doesn't hold (
Car extends Engineis wrong). - Exposing internal collections:
public List<Transaction> getHistory()— caller can modify; return unmodifiable copy. - Abstract class with only one implementation — use interface instead; abstract class when shared state/logic exists.
- Calling
super.withdraw()then adding logic — fragile; use template method pattern explicitly or composition. - Lombok
@Dataon domain entities — generates public setters that break encapsulation.
Debugging guide
Debug OOP-related issues in banking services:
- Wrong withdraw behavior: Log runtime class —
account.getClass().getName()— verify polymorphic reference points to expected subtype. - Balance changed unexpectedly: Search for direct field access or public setter — grep for
.setBalance(outside Account class. - PaymentProcessor not called: Check Spring bean wiring — is interface injected or wrong concrete class bound?
- AbstractMethodError: Subclass missing @Override — added abstract method to parent, child not recompiled.
# Find encapsulation violationsgrep -rn "setBalance\|\.balance\s*=" src/ --include="*.java" \| grep -v "Account.java"# Find instanceof anti-patterngrep -rn "instanceof.*Account" src/ --include="*.java"# Verify polymorphic dispatch — add temporary log in each withdraw() override
Best practices
- Encapsulate all domain state —
privatefields,publicbehavior methods only. - Program to interfaces —
PaymentProcessor processor, notWireTransferProcessor processor. - Use
abstract classwhen shared state/logic exists (Account);interfacefor pure contracts (PaymentProcessor). - Favor composition over inheritance —
Account has-a FraudCheckervsFraudCheckingAccount extends Account. - Use
recordfor immutable value objects — Money, AccountId, TransactionId. - Apply @Override always — compiler catches signature drift when parent methods change.
- Keep inheritance hierarchies shallow (≤2 levels) — CheckingAccount extends Account, not deeper.
Anti-patterns
- Anemic domain model: Entity = data bag, Service = all logic — defeats OOP; put behavior with data.
- Base class with 30 methods: Fat abstract class — split into interfaces (Interface Segregation Principle).
- instanceof type checking: Replace with polymorphism — each account type knows its own withdraw rules.
- Public fields for "simplicity": Breaks encapsulation immediately — no validation, no audit trail.
- Inheritance for HAS-A relationships:
SavingsAccount extends InterestCalculator— use composition.
Staff engineer notes
- Staff engineers evaluate OOP design by asking: "Where are the invariants enforced?" — if answer is "the service layer," domain model is anemic.
- In banking, encapsulation is a compliance feature — every balance change must pass through a method that logs, validates, and authorizes.
- Polymorphism is not just inheritance — Strategy pattern (PaymentProcessor interface) is often cleaner than account type hierarchies.
- Modern Java (records, sealed classes) refines OOP — sealed
Account permits CheckingAccount, SavingsAccountgives exhaustive pattern matching without fragile instanceof. - When reviewing PRs, reject any public setter on a domain entity with mutable state — that is an encapsulation breach.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What are the four pillars of OOP?
BeginnerModel answer
- Encapsulation (hide state, expose behavior), Inheritance (reuse via IS-A hierarchy), Polymorphism (one interface, many implementations
- runtime dispatch), Abstraction (expose essential features, hide complexity
- interfaces and abstract classes).
Follow-up probe
Give a banking example of each.
2Explain encapsulation with a banking example.
BeginnerModel answer
Account class has private BigDecimal balance.
External code cannot read or modify balance directly.
All changes go through deposit() and withdraw() which validate amount, check authorization, log audit trail, and enforce invariants (no negative balance on savings).
Follow-up probe
What breaks if balance is public?
3What is inheritance and when should you use it?
BeginnerModel answer
Child class extends parent, inheriting fields and methods.
Use when IS-A relationship is true: CheckingAccount IS-A Account.
Child overrides methods (withdraw with overdraft) or adds new ones (accrueInterest).
Don't use for HAS-A or code reuse alone.
Follow-up probe
Composition vs inheritance?
4Explain polymorphism in Java.
BeginnerModel answer
Same reference type, different runtime types.
withdraw(amount) calls CheckingAccount's withdraw at runtime via virtual method dispatch.
Enables writing code that works with any Account subtype without instanceof checks.
Follow-up probe
What is dynamic dispatch?
5What is the difference between abstract class and interface?
BeginnerModel answer
Abstract class: can have state (fields), constructors, concrete and abstract methods, single inheritance.
Interface: contract only (Java 8+ default/static methods), no state, multiple inheritance of type.
Use abstract class for shared state; interface for capability contracts.
Follow-up probe
Can an interface have fields?
Intermediate
6What is an anemic domain model and why is it bad?
IntermediateModel answer
- Entity classes with only getters/setters; all business logic in service layer. Bad because invariants aren't enforced at domain boundary
- any service can set invalid state. Violates encapsulation. Rich domain model puts behavior with data: account.withdraw() not accountService.withdraw(account).
Follow-up probe
When is anemic model acceptable?
7Explain the Liskov Substitution Principle with Account example.
IntermediateModel answer
Subtypes must be substitutable for base type without breaking behavior.
If code works with Account, passing SavingsAccount must not cause surprises.
withdraw() rejecting overdraft is fine (tighter precondition); silently changing balance calculation is not (breaks contract).
Follow-up probe
Give a LSP violation example.
8Why use interface over abstract class for PaymentProcessor?
IntermediateModel answer
- Payment processing has no shared state
- only behavior contract. Interface allows ACHProcessor to also implement AuditLogger interface (multiple inheritance of type). Easy to mock in tests. Swap implementations via DI without class hierarchy constraints.
Follow-up probe
When would abstract class be better?
9What is the Template Method pattern in OOP?
IntermediateModel answer
- Abstract class defines algorithm skeleton (processTransaction: validate → debit → send → log); subclasses override specific steps (validate for checking vs savings). Inheritance enables reuse of flow while customizing steps
- common in banking transaction pipelines.
Follow-up probe
Template method vs Strategy pattern?
10How do sealed classes (Java 17+) improve OOP?
IntermediateModel answer
- sealed class Account permits CheckingAccount, SavingsAccount
- compiler knows all subtypes. Enables exhaustive pattern matching: switch(account) { case CheckingAccount c -> ... case SavingsAccount s -> ... }
- no default needed, no instanceof chains.
Follow-up probe
sealed class vs final class?
Advanced
11Design an OOP model for a multi-currency banking platform.
AdvancedModel answer
Account (abstract, sealed) with Money value object (BigDecimal + Currency).
CheckingAccount, SavingsAccount, ForeignCurrencyAccount.
PaymentProcessor interface with WireTransfer, ACH, SWIFT implementations.
Transaction as immutable record.
ExchangeRateService injected via interface.
Repository pattern for persistence abstraction.
Follow-up probe
Where do you enforce currency conversion rules?
12Your team uses @Data Lombok on all entities — staff review flags it. Why?
AdvancedModel answer
- @Data generates public setters for all fields
- breaks encapsulation on domain entities. Balance can be set directly bypassing validation. Prefer @Value (immutable) for value objects, manual accessors with validation for entities, or package-private setters for JPA only.
Follow-up probe
How do JPA entities maintain encapsulation?
13Refactor: if (account instanceof CheckingAccount) { overdraft... } else if (account instanceof SavingsAccount) { ... }
AdvancedModel answer
- Move logic into each account class's withdraw() override
- polymorphism replaces instanceof. Caller becomes: account.withdraw(amount). Add sealed Account permits CheckingAccount, SavingsAccount for compile-time exhaustiveness. Open/Closed Principle: new account type = new class, zero changes to caller.
Follow-up probe
When is instanceof acceptable?
14How does OOP support microservice boundaries in banking?
AdvancedModel answer
- Each bounded context owns its domain model
- Account aggregate in Account Service, Payment in Payment Service. Encapsulation = service boundary: other services call public API (REST/events), not internal fields. Interfaces define cross-service contracts. Polymorphism enables swapping internal implementations without API change.
Follow-up probe
Distributed monolith anti-pattern?
15Compare OOP in Java vs functional style for banking ledger.
AdvancedModel answer
- OOP: rich Account entity with behavior, mutable state with encapsulated invariants
- natural for domain modeling, audit trails via method entry. Functional: immutable Transaction records, pure functions for balance calculation
- natural for event sourcing, replay. Modern Java combines both: records + sealed classes + methods on entities.
Follow-up probe
Event sourcing with OOP?
Hands-on exercise
Lab: Banking OOP in the playground — run, observe output, then extend:
- Run starter code — observe checking overdraft vs savings rejection (polymorphism).
- Add a
FixedDepositAccountthat extends Account — withdraw always rejected before maturity. - Add it to the accounts list and call
withdraw()— same polymorphic loop, new behavior. - Create a
PaymentProcessorinterface with one implementation — call from main. - Bonus: convert Account to
sealed abstract class Account permits CheckingAccount, SavingsAccount, FixedDepositAccount.
JavaObject Oriented Programming
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Inheritance vs composition: Inheritance wins for true IS-A with shared state; composition wins for HAS-A and flexible behavior mixing.
- Abstract class vs interface: Abstract class wins when shared fields/constructors exist; interface wins for pure contracts and multiple typing.
- Rich domain model vs anemic + service: Rich wins for invariant enforcement; anemic wins for simple CRUD with no business rules.
- Class hierarchy vs Strategy pattern: Hierarchy wins for stable IS-A relationships; Strategy wins for swappable algorithms (payment processors).
Summary
Object-Oriented Programming is the foundation of enterprise Java — especially in banking where domain integrity, audit trails, and extensibility are non-negotiable. You now understand why OOP exists, how all four pillars apply to accounts and payments, and how to avoid anemic models and instanceof chains. Next: classes and objects in depth, or interfaces and abstract classes.
Key takeaways
- OOP exists to model domain concepts (accounts, transfers) with controlled state change and extensible behavior.
- Encapsulation: private state, public behavior — balance changes only through validated methods.
- Inheritance: IS-A specialization — CheckingAccount extends Account with different withdraw rules.
- Polymorphism: Account ref, CheckingAccount runtime — one withdraw() call, JVM dispatches to correct override.
- Abstraction: PaymentProcessor interface — swap SWIFT/ACH without changing PaymentService.