What you're building and why it matters
Creating checking and savings account classes in Ruby teaches you how real banking systems separate account types with different rules. A checking account class handles frequent transactions and debit cards. A savings account class enforces withdrawal limits and tracks interest. Building both shows you how inheritance, methods, and instance variables work together to model real-world behavior.
This is a practical exercise in object-oriented design. You're not connecting to a real bank—you're learning the structure that banks use internally to keep checking and savings accounts separate, even though they're both accounts.
Key Takeaways
- Create a parent Account class with shared behavior like deposits, balance tracking, and transaction history.
- Build a CheckingAccount subclass that allows unlimited transactions and charges overdraft fees.
- Build a SavingsAccount subclass that limits withdrawals per month and calculates interest.
- Use instance variables to track balance, transaction history, and account-specific rules.
- Override methods in subclasses when checking and savings accounts need different behavior for the same action.
Start with a parent Account class
The parent class holds everything both account types share: a balance, a way to deposit money, a way to track transactions. This avoids writing the same code twice.
Your Account class needs an initializer that sets the starting balance to zero and creates an empty array for transaction history. Add a deposit method that adds money to the balance and records the transaction. Add a method that returns the current balance. These three pieces form the foundation both subclasses will build on.
Write a method to record each transaction as a hash with the type (deposit or withdrawal), amount, and timestamp. Store these in the transaction history array. This becomes useful later when you need to show account statements or enforce limits.
Build the CheckingAccount subclass
CheckingAccount inherits from Account, so it gets deposits and balance tracking for free. Add a withdraw method that removes money from the balance, records the transaction, and checks whether the balance went negative. If it did, charge an overdraft fee (typically $35 in real banks, but use a smaller number like $5 for testing).
The key difference: checking accounts allow unlimited withdrawals. You don't need to track how many times someone withdrew this month. You just need to handle the overdraft fee when the balance drops below zero.
Add a method that returns the account type as a string—"Checking"—so you can tell accounts apart when you're testing. This becomes important when you have multiple accounts in a list.
Build the SavingsAccount subclass
SavingsAccount also inherits from Account, but it enforces a withdrawal limit. Most real savings accounts allow six withdrawals per month before charging a fee. Add an initializer that calls the parent initializer and sets a withdrawal counter to zero and a withdrawal limit to six.
Override the withdraw method. Before removing money, check whether the withdrawal counter is below the limit. If it is, allow the withdrawal, increment the counter, and record the transaction. If the counter has hit the limit, reject the withdrawal and return an error message instead of removing money.
Add an interest method that calculates interest based on the current balance and an annual rate (try 0.02 for 2 percent). Multiply the balance by the rate, divide by 12 to get the monthly amount, and add it to the balance. Record this as a special transaction type called "interest".
Add a reset_withdrawal_counter method that sets the counter back to zero. Call this once a month (or in your tests, call it manually to simulate a month passing).
Test both classes with real scenarios
Create a checking account, deposit $1,000, withdraw $500, then withdraw $600. The balance should be negative $100, and the overdraft fee should bring it to negative $105. Print the transaction history to verify each step was recorded.
Create a savings account, deposit $5,000, and withdraw $100 six times. The seventh withdrawal should be rejected. Call the interest method and verify the balance increased. Call reset_withdrawal_counter and verify you can withdraw again.
Create both accounts in the same program and loop through them, printing the account type, balance, and transaction count. This tests that each class behaves independently and that inheritance is working.
Common mistakes to avoid
Don't forget to call the parent initializer in subclass initializers using super. If you don't, the balance and transaction history won't exist, and your code will crash when you try to use them.
Don't modify the parent class method when you need different behavior in a subclass. Override it instead. If you change the parent's withdraw method to handle savings account limits, the checking account will inherit those limits too, which is wrong.
Don't forget to record transactions. It's tempting to just change the balance number, but real banks track every move. When you test later, you'll need that history to debug what went wrong.
Don't use a fixed overdraft fee in the CheckingAccount class if you think you might want to change it later. Store it as an instance variable so you can set it when you create the account, or as a class variable if all checking accounts should share the same fee.
Extending your classes later
Once both classes work, add a transfer method to the parent Account class that withdraws from one account and deposits into another. This teaches you how methods can work with multiple objects.
Add a statement method that prints all transactions in a readable format, with dates, amounts, and running balances. This is how real banks show you what happened.
Create an Account subclass for a money market account that combines checking and savings rules—unlimited transactions but interest earned. This tests whether you understand inheritance well enough to build a third type.
Frequently Asked Questions
Should I use a class variable or instance variable for the overdraft fee?
Use an instance variable if different checking accounts might have different fees. Use a class variable if all checking accounts share the same fee. For learning, instance variables are safer because they're easier to test—you can create one account with a $5 fee and another with a $35 fee and verify they behave differently.
How do I prevent someone from setting the balance directly?
Use attr_reader for balance instead of attr_accessor. This creates a method that reads the balance but prevents direct assignment. Someone can call account.balance but not account.balance = 500. They have to use deposit or withdraw, which enforces your rules.
What should I store in the transaction history?
Store a hash for each transaction with keys like :type (deposit, withdrawal, interest, fee), :amount, and :timestamp. Use Time.now to capture when the transaction happened. This gives you enough information to print statements, calculate totals, and debug problems later.
Do I need to validate that amounts are positive?
Yes. Add a check in deposit and withdraw that returns an error message if the amount is zero or negative. This prevents someone from depositing -$100 to steal money or withdrawing -$50 to accidentally add funds.
How do I test that the withdrawal limit works?
Create a savings account, call withdraw six times in a loop, then try a seventh time and check that it returns an error message instead of changing the balance. Print the balance before and after to confirm it didn't move. This is a straightforward but complete test of the limit logic.