Project: Modeling a Bank Account
Project: Modeling a Bank Account
This final lesson brings together everything you have learned in this tutorial: classes and objects, fields and state, constructors, encapsulation, access modifiers, getters and setters, and validation. Instead of isolated examples, you will build a single, realistic BankAccount class from the ground up — step by step — and understand exactly why every design decision is made.
Why a bank account?
A bank account is a classic OOP case study because it has natural rules that mirror real-world constraints:
- The balance must never be exposed directly — the bank decides when and how it changes.
- Deposits must be positive.
- Withdrawals must not exceed the available balance.
- Every account needs an owner and a starting state set at creation time.
These rules map perfectly onto encapsulation and validation — the two skills you refined in lessons 4 and 5.
Step 1 — Define the class and its private fields
Start with the data. A bank account needs an account number, an owner name, and a balance. All three are private because no outside code should touch them directly.
final for accountNumber and ownerName? Once an account is opened, the account number and the owner name do not change. Marking those fields final makes that contract explicit and lets the compiler enforce it. balance is not final because it is meant to change with every transaction.
Step 2 — Write the constructor
The constructor is the only place that sets the initial state. It validates the inputs before accepting them, so a BankAccount object is always born in a valid state.
Notice the this. prefix — it distinguishes the field from the parameter when they share the same name, exactly as you learned in lesson 3.
Step 3 — Add deposit with validation
A deposit increases the balance, but only if the amount is strictly positive.
balance and then throw an exception, the object is left in a corrupt state. With validation at the top of the method, either everything succeeds or nothing changes.
Step 4 — Add withdraw with validation
A withdrawal decreases the balance, but two rules apply: the amount must be positive, and there must be enough funds.
IllegalArgumentException signals that the caller passed bad data (a negative amount). IllegalStateException signals that the object is in the wrong state for the operation (not enough money). Choosing the correct exception type makes error messages easier to understand and debug.
Step 5 — Add read-only getters
External code needs to read the balance and account info, but never write it directly. Provide getters with no matching setters — that is intentional.
There is no setBalance(). The only way to change the balance is through deposit() or withdraw(), which both apply the business rules. This is encapsulation doing exactly what it is designed to do.
Step 6 — Override toString
A useful toString() lets you print the account cleanly during debugging or logging.
Putting it all together
Here is the complete class followed by a small driver that exercises every method:
What you built and why it matters
The BankAccount class demonstrates every concept from this tutorial working together:
- Class and object —
BankAccountis the blueprint;accountis the instance. - Fields and state —
balancerepresents the account's evolving state. - Constructor — guarantees a valid starting state with
thisassignments. - Encapsulation —
privatefields protected behind public methods. - Validation — every mutating method checks its input before acting.
- Read-only access — getters without setters enforce the rule that only transactions can change the balance.
- toString — clean output for debugging and logging.
This pattern — private state, a validating constructor, behaviour-driven public methods, and read-only getters — is the foundation of professional Java code. Congratulations on completing the OOP Basics tutorial!