What you're building and why it matters
Creating checking and savings account classes in Ruby teaches you how real banking software models money movement. You'll write code that tracks balances, enforces rules (like overdraft limits), and records transactions—the same logic that runs behind the scenes when you deposit a check or transfer funds online.
This project sits between theory and practice. You're not connecting to a real bank, but you're building the object-oriented structure that a bank's system would use. Each account type has different behavior: a checking account might allow overdrafts up to a limit, while a savings account might charge a fee for too many withdrawals. Your classes will enforce those rules.
The work also forces you to think about state—what information an account needs to remember, and what methods change that state. That's the core of object-oriented programming.
Key Takeaways
- A checking account class needs instance variables for balance, account number, and overdraft limit, plus methods to deposit, withdraw, and check balance.
- A savings account class should track balance and withdrawal count, and charge a fee or block withdrawals if the limit is exceeded.
- Both classes benefit from a parent class (or module) that handles shared behavior like transaction logging.
- Write tests as you go—test that a withdrawal fails if it would exceed the overdraft limit, and that a savings account fee is applied correctly.
- Use instance variables (prefixed with @) to store state, and attr_reader or attr_accessor to expose only what other code needs to see.
Setting up the basic checking account class
Start with a Checking class that holds a balance, an account number, and an overdraft limit. Use initialize to set these up when you create a new account:
class Checking def initialize(account_number, starting_balance = 0, overdraft_limit = 500) @account_number = account_number @balance = starting_balance @overdraft_limit = overdraft_limit end def balance @balance end def deposit(amount) @balance += amount end def withdraw(amount) if @balance - amount >= -@overdraft_limit @balance -= amount else puts "Withdrawal denied. Insufficient funds." end end end
The withdraw method is the key difference from a simple balance tracker. It checks whether the withdrawal would push the account below the negative of the overdraft limit. If it would, the withdrawal is rejected. Otherwise, the balance is reduced. This is how a real checking account prevents you from going too far into the red.
Test this by creating an account, depositing money, and trying to withdraw more than the balance plus overdraft limit. You should see the denial message and the balance should not change.
Building the savings account class with withdrawal limits
A Savings class works differently. It tracks how many withdrawals have been made in a period (usually a month), and charges a fee or blocks withdrawals if the limit is exceeded. Here's a basic version:
class Savings def initialize(account_number, starting_balance = 0, withdrawal_limit = 6) @account_number = account_number @balance = starting_balance @withdrawal_limit = withdrawal_limit @withdrawals_this_month = 0 @monthly_fee = 2.50 end def balance @balance end def deposit(amount) @balance += amount end def withdraw(amount) if @withdrawals_this_month >= @withdrawal_limit puts "Withdrawal limit reached. Fee of $#{@monthly_fee} applied." @balance -= @monthly_fee elsif @balance >= amount @balance -= amount @withdrawals_this_month += 1 else puts "Insufficient funds." end end def reset_monthly_count @withdrawals_this_month = 0 end end
Notice that @withdrawals_this_month increments each time a withdrawal succeeds. You'll need a method like reset_monthly_count to reset this counter at the start of each month—in a real system, this would be automated by a scheduled job.
The fee is applied immediately if the limit is reached, and the withdrawal itself is blocked. This matches how many banks handle savings accounts: you get a certain number of free withdrawals, and then you pay for extras.
Reducing duplication with a parent class
Both classes share some behavior: they hold a balance, an account number, and they support deposit and balance-checking. Extract this into a parent class called Account:
class Account def initialize(account_number, starting_balance = 0) @account_number = account_number @balance = starting_balance end def balance @balance end def deposit(amount) @balance += amount end def account_number @account_number end end class Checking < Account def initialize(account_number, starting_balance = 0, overdraft_limit = 500) super(account_number, starting_balance) @overdraft_limit = overdraft_limit end def withdraw(amount) if @balance - amount >= -@overdraft_limit @balance -= amount else puts "Withdrawal denied." end end end class Savings < Account def initialize(account_number, starting_balance = 0, withdrawal_limit = 6) super(account_number, starting_balance) @withdrawal_limit = withdrawal_limit @withdrawals_this_month = 0 end def withdraw(amount) if @withdrawals_this_month >= @withdrawal_limit @balance -= 2.50 elsif @balance >= amount @balance -= amount @withdrawals_this_month += 1 end end def reset_monthly_count @withdrawals_this_month = 0 end end
The super keyword calls the parent class's initialize method, so you don't repeat the account number and balance setup. Now if you need to add a method that both account types use—like a way to print a statement—you add it once to Account and both subclasses inherit it.
Writing tests to verify behavior
Test each class with a simple test file or in IRB. Here are the scenarios you should check:
checking = Checking.new("CHK001", 1000, 500) puts checking.balance # Should print 1000 checking.deposit(200) puts checking.balance # Should print 1200 checking.withdraw(1500) # Should succeed (1200 - 1500 = -300, within -500 limit) puts checking.balance # Should print -300 checking.withdraw(300) # Should fail (would be -600, beyond -500 limit) puts checking.balance # Should still be -300 savings = Savings.new("SAV001", 5000, 6) 6.times { savings.withdraw(100) } # Six withdrawals, all succeed puts savings.balance # Should print 4400 savings.withdraw(100) # Seventh withdrawal, fee applied instead puts savings.balance # Should print 4397.50 (fee deducted)
These tests confirm that the overdraft limit works, that the withdrawal counter increments, and that the fee is applied at the right time. As you add features—like transaction history or interest calculation—write tests for those too.
Adding transaction history and logging
Real accounts keep a record of every transaction. Add a @transactions array to the parent class and log each deposit and withdrawal:
class Account def initialize(account_number, starting_balance = 0) @account_number = account_number @balance = starting_balance @transactions = [] @transactions << { type: "opening", amount: starting_balance, date: Time.now } end def deposit(amount) @balance += amount @transactions << { type: "deposit", amount: amount, date: Time.now } end def transactions @transactions end end
Update the withdraw methods in both subclasses to log transactions only when the withdrawal succeeds. This gives you a complete history of what happened to the account, which is essential for debugging and for users who want to review their activity.
Frequently Asked Questions
Should I use attr_accessor or attr_reader for the balance?
Use attr_reader :balance to let other code read the balance but not change it directly. This forces all balance changes through your deposit and withdraw methods, where you can enforce rules. If you use attr_accessor, someone could write account.balance = 1000000 and bypass all your logic.
How do I handle deposits and withdrawals that fail?
Return a boolean (true or false) from your withdraw method instead of just printing a message. This lets the calling code decide what to do: if account.withdraw(100) then puts "Success" else puts "Failed" end. This is cleaner than relying on puts statements.
What if I want to charge interest on savings accounts?
Add an apply_interest method that calculates interest based on the current balance and an interest rate, then adds it to the balance. Call this method once a month or once a year, depending on your rules. Store the interest rate as an instance variable so different savings accounts can have different rates.
Can I transfer money between accounts?
Yes. Write a method that takes two account objects and an amount, then calls withdraw on one and deposit on the other. Make sure the withdraw succeeds before you deposit: def transfer(from_account, to_account, amount) if from_account.withdraw(amount) to_account.deposit(amount) end end
How do I prevent someone from creating an account with a negative starting balance?
Add a check in initialize: raise "Starting balance cannot be negative" if starting_balance < 0. This stops the account from being created at all if the input is invalid, which is better than silently accepting bad data.