Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 7

    Object Oriented Programming

    Object-Oriented Programming (OOP) is not an academic exercise — it is how banking systems model the real world.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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 balance field 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 PaymentProcessor interface.
    • 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. Call account.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:

    text
    ┌─────────────────── 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:

    Encapsulation — account balance boundary
    Caller
    PaymentService
    deposit()
    Public method
    validate()
    Private check
    balance
    Private field
    External code cannot touch balance directly — all access through validated methods.
    Inheritance — account specialization
    Account
    Abstract base
    CheckingAccount
    Overdraft allowed
    SavingsAccount
    Interest accrual
    Child classes inherit common state and behavior; override withdraw() with different rules.
    Polymorphism — runtime dispatch
    Account ref
    Compile-time type
    CheckingAccount
    Runtime type
    withdraw()
    Overridden method
    JVM dispatch
    Virtual method table
    Account a = new CheckingAccount(); a.withdraw() calls checking's version at runtime.
    Abstraction — payment processor contract
    PaymentProcessor
    Interface
    WireTransfer
    SWIFT impl
    ACH
    Domestic impl
    PaymentService
    Depends on interface
    Service code depends on PaymentProcessor — swap implementations without changing callers.

    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 through deposit() with validation.
    • Abstraction (line 6): PaymentProcessor interface — callers depend on contract, not SWIFT vs ACH implementation details.
    • Inheritance (line 49, 66): CheckingAccount extends Account — inherits deposit(), overrides withdraw() with overdraft logic.
    • Polymorphism (line 107-113): List<Account> holds different runtime types — same withdraw() call, different behavior per account type.
    java
    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 outside
    private BigDecimal balance; // no direct access
    protected Account(String accountId, BigDecimal openingBalance) {
    this.accountId = accountId;
    this.balance = openingBalance.setScale(2, RoundingMode.HALF_UP);
    }
    // Public API — controlled access with validation
    public 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); }
    @Override
    public 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 pattern
    deposit(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); }
    @Override
    public 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 differ
    List<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 OK
    accounts.get(1).withdraw(new BigDecimal("6000.00")); // savings: rejected
    // Polymorphic payment processing — swap processor without changing this code
    PaymentProcessor 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 PaymentProcessor interface (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.
    java
    // domain/Account.java — encapsulation + inheritance
    public 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 specialization
    public class CheckingAccount extends Account {
    @Override
    public Result<Money> withdraw(Money amount) {
    // overdraft rules enforced here — not in controller
    }
    }
    // application/PaymentService.java — depends on abstraction
    @Service
    public class PaymentService {
    private final PaymentProcessor processor; // interface, not concrete class
    private final AccountRepository repository;
    public PaymentService(PaymentProcessor processor, AccountRepository repo) {
    this.processor = processor; // Spring injects Wire or ACH impl
    this.repository = repo;
    }
    public PaymentResult transfer(AccountId from, AccountId to, Money amount) {
    Account source = repository.findById(from);
    source.withdraw(amount); // polymorphic — checking vs savings
    processor.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 PaymentProcessor implementation — zero changes to existing transfer flow.
    • Interface segregation: Small focused interfaces — ReadableAccount, WritableAccount — services depend only on what they need.
    • Test doubles: Mock PaymentProcessor in 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 @Data on 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 Engine is 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 @Data on 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.
    bash
    # Find encapsulation violations
    grep -rn "setBalance\|\.balance\s*=" src/ --include="*.java" \
    | grep -v "Account.java"
    # Find instanceof anti-pattern
    grep -rn "instanceof.*Account" src/ --include="*.java"
    # Verify polymorphic dispatch — add temporary log in each withdraw() override

    Best practices

    • Encapsulate all domain state — private fields, public behavior methods only.
    • Program to interfaces — PaymentProcessor processor, not WireTransferProcessor processor.
    • Use abstract class when shared state/logic exists (Account); interface for pure contracts (PaymentProcessor).
    • Favor composition over inheritance — Account has-a FraudChecker vs FraudCheckingAccount extends Account.
    • Use record for 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, SavingsAccount gives 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

    5
    1. 1What are the four pillars of OOP?
      Beginner

      Model 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.

    2. 2Explain encapsulation with a banking example.
      Beginner

      Model 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?

    3. 3What is inheritance and when should you use it?
      Beginner

      Model 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?

    4. 4Explain polymorphism in Java.
      Beginner

      Model 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?

    5. 5What is the difference between abstract class and interface?
      Beginner

      Model 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

    5
    1. 6What is an anemic domain model and why is it bad?
      Intermediate

      Model 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?

    2. 7Explain the Liskov Substitution Principle with Account example.
      Intermediate

      Model 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.

    3. 8Why use interface over abstract class for PaymentProcessor?
      Intermediate

      Model 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?

    4. 9What is the Template Method pattern in OOP?
      Intermediate

      Model 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?

    5. 10How do sealed classes (Java 17+) improve OOP?
      Intermediate

      Model 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

    5
    1. 11Design an OOP model for a multi-currency banking platform.
      Advanced

      Model 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?

    2. 12Your team uses @Data Lombok on all entities — staff review flags it. Why?
      Advanced

      Model 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?

    3. 13Refactor: if (account instanceof CheckingAccount) { overdraft... } else if (account instanceof SavingsAccount) { ... }
      Advanced

      Model 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?

    4. 14How does OOP support microservice boundaries in banking?
      Advanced

      Model 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?

    5. 15Compare OOP in Java vs functional style for banking ledger.
      Advanced

      Model 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 FixedDepositAccount that extends Account — withdraw always rejected before maturity.
    • Add it to the accounts list and call withdraw() — same polymorphic loop, new behavior.
    • Create a PaymentProcessor interface with one implementation — call from main.
    • Bonus: convert Account to sealed abstract class Account permits CheckingAccount, SavingsAccount, FixedDepositAccount.

    JavaObject Oriented Programming

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    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.
    Ready to mark this lesson complete?Track your journey across the entire course.