Banking System Project¶
Mental Model
This project models a real banking domain where every account type shares a common interface (deposit, withdraw, calculate_interest) but implements the details differently. The key design insight: inheritance defines the shared contract, and polymorphism lets the bank process any account type without knowing which specific type it is.
Overview¶
Build a complete banking system with different account types using inheritance and polymorphism.
Objectives¶
- Design a class hierarchy for different account types
- Implement transaction handling
- Apply polymorphism for interest calculation
- Manage customer accounts
Requirements¶
1. Base Account Class¶
Create an abstract BankAccount class with:
- Attributes: account_number, account_holder, balance, transactions (list)
- Methods:
deposit(amount)- add money to accountwithdraw(amount)- remove money (check sufficient balance)get_balance()- return current balanceget_transaction_history()- return list of all transactionsapply_fees()- apply monthly fees (if any)- Abstract method:
calculate_interest()- varies by account type - Abstract method:
get_account_type()- return account type name
2. Account Types¶
Implement these account types:
SavingsAccount
- Minimum balance: $100
- Monthly fee: $0 (no fee)
- Interest rate: 2% annual (compounded monthly)
- Withdrawal limit: 6 per month
- Penalty: $5 fee if minimum balance not maintained
Compounding Details
Monthly compounding means dividing the annual rate by 12 and applying it each
month to the current balance. Edge cases to handle: what if interest is applied
mid-month? What if the balance changes during the month? A simple approach is to
compound based on the balance at the time apply_interest() is called.
CheckingAccount
- Minimum balance: $25
- Monthly fee: $5 (waived if balance > $500)
- Interest rate: 0.1% annual
- No withdrawal limit
- Overdraft protection: Can go negative up to -$100 (with $35 fee)
BusinessAccount
- Minimum balance: $1000
- Monthly fee: $15
- Interest rate: 1.5% annual
- No withdrawal limit
- Transaction fee: $0.50 per transaction after 50 transactions/month
StudentAccount (inherits from SavingsAccount)
- Minimum balance: $0
- Monthly fee: $0
- Interest rate: 1.5% annual
- Withdrawal limit: 10 per month
- Age restriction: Account holder must be student
3. Customer Class¶
Create a Customer class with:
- Attributes: customer_id, name, email, accounts (list)
- Methods:
add_account(account)- open new accountclose_account(account_number)- close accountget_total_balance()- sum of all account balancesget_all_accounts()- return list of all accountstransfer(from_account, to_account, amount)- move money between accounts
4. Bank Class¶
Create a Bank class that:
- Manages all customers and accounts
- Assigns account numbers
- Processes monthly maintenance (fees and interest)
- Generates reports
- Methods:
create_customer(name, email)- create new customerfind_customer(customer_id)- find customer by IDopen_account(customer_id, account_type)- open account for customerprocess_monthly_maintenance()- apply fees and interest to all accountsget_total_assets()- sum of all balancesgenerate_report()- display bank statistics
Avoid a God Object
The Bank class risks becoming a "god object" that handles customer management,
account creation, monthly maintenance, and reporting all in one class. This is the
most common design mistake in projects like this. Apply the Single Responsibility
Principle: extract reporting into BankReporter, monthly maintenance into
MaintenanceProcessor, and keep Bank focused on coordinating these collaborators.
A good test: if a method doesn't need most of the class's attributes, it probably
belongs in a separate class.
5. Transaction Class¶
Create a Transaction class to track:
- Transaction ID
- Date/time
- Transaction type (deposit, withdrawal, transfer, fee, interest)
- Amount
- Balance after transaction
Data Integrity
A production banking system requires atomic transactions: if a transfer debits one account but fails to credit the other, the debit must be rolled back. Consider implementing a simple rollback mechanism (e.g., wrap the pair of operations in a try/except block and undo the debit on failure).
Sample Usage¶
```python
Create bank¶
bank = Bank("First National Bank")
Create customer¶
customer = bank.create_customer("John Doe", "john@email.com")
Open accounts¶
savings = bank.open_account(customer.customer_id, "savings") checking = bank.open_account(customer.customer_id, "checking")
Perform transactions¶
savings.deposit(1000) checking.deposit(500) checking.withdraw(50)
Transfer money¶
customer.transfer(savings, checking, 200)
Check balances¶
print(f"Savings: ${savings.get_balance()}") print(f"Checking: ${checking.get_balance()}")
Monthly maintenance¶
bank.process_monthly_maintenance() ```
Bonus Features¶
- Add credit card accounts
- Implement loan accounts
- Add transaction categories
- Create account statements
- Add fraud detection
- Implement direct deposit
- Add bill payment system
- Implement simple transaction rollback for failed transfers
Testing Requirements¶
Your implementation should:
- Create multiple customers
- Open different account types
- Perform various transactions
- Test withdrawal limits
- Test minimum balance violations
- Test overdraft protection
- Calculate and apply interest
- Generate transaction history
- Test monthly maintenance
- Generate bank reports
Files to Create¶
accounts.py- All account classescustomer.py- Customer classbank.py- Bank managementtransaction.py- Transaction trackingmain.py- Demo programREADME.md- This file
Good luck building your bank!
Exercises¶
Exercise 1. List all the inheritance and composition relationships in this project. For each, state whether it is "is-a" or "has-a" and justify why that relationship type is appropriate.
Solution to Exercise 1
Inheritance (is-a):
SavingsAccountis-aBankAccount— it is a specialized account type with a specific interest rate and withdrawal limit.CheckingAccountis-aBankAccount— same interface, different rules (overdraft, monthly fee).BusinessAccountis-aBankAccount— same interface, different fee structure.StudentAccountis-aSavingsAccount— a more restricted savings account with relaxed minimums.
Composition (has-a):
BankAccounthas-a list ofTransactionobjects — transactions are created by the account and have no meaning outside it.Customerhas-a list ofBankAccountobjects — a customer owns accounts.Bankhas-a collection ofCustomerobjects — the bank manages customers.
Inheritance is appropriate here because account types genuinely specialize a common interface (deposit, withdraw, calculate_interest). Composition is appropriate because customers and transactions are independently meaningful entities that are owned or referenced by their containers.
Exercise 2.
The Bank.process_monthly_maintenance() method applies fees and interest to every account. Explain why this method risks making Bank a "god object." Sketch a refactored design that extracts this logic into a separate class.
Solution to Exercise 2
process_monthly_maintenance() combines two concerns: fee processing and interest calculation. As the system grows (adding statements, compliance checks, notifications), more logic accumulates in Bank, violating the Single Responsibility Principle.
Refactored design:
class MaintenanceProcessor:
def process(self, accounts):
for account in accounts:
account.apply_fees()
interest = account.calculate_interest()
if interest > 0:
account.deposit(interest)
class Bank:
def __init__(self, name):
self.name = name
self._customers = {}
self._maintenance = MaintenanceProcessor()
def process_monthly_maintenance(self):
all_accounts = []
for customer in self._customers.values():
all_accounts.extend(customer.get_all_accounts())
self._maintenance.process(all_accounts)
Now Bank delegates to MaintenanceProcessor. Adding a BankReporter for generate_report() follows the same pattern. Each class has one reason to change.
Exercise 3.
Implement a simple rollback mechanism for Customer.transfer(). If the deposit into the target account fails after the withdrawal from the source account succeeds, the withdrawal must be reversed. Write pseudocode or Python code showing the try/except structure.
Solution to Exercise 3
class Customer:
def transfer(self, from_account, to_account, amount):
# Step 1: Withdraw from source
from_account.withdraw(amount)
# Step 2: Deposit to target — may fail
try:
to_account.deposit(amount)
except Exception:
# Rollback: reverse the withdrawal
from_account.deposit(amount)
raise ValueError(
f"Transfer failed: could not deposit into "
f"{to_account.account_number}. Withdrawal reversed."
)
This ensures that the two accounts stay consistent even when the second operation fails. Production systems use database transactions for atomicity, but this try/except pattern captures the same principle at the application level.
Exercise 4.
StudentAccount inherits from SavingsAccount. Explain one scenario where this inheritance could cause a problem (hint: Liskov Substitution Principle). Then propose an alternative design that avoids the issue.
Solution to Exercise 4
Problem: SavingsAccount enforces a minimum balance of $100. StudentAccount overrides this to $0. If code expects a SavingsAccount and relies on the $100 minimum (e.g., a function that assumes withdraw will reject amounts that leave the balance below $100), passing a StudentAccount may violate that assumption — the student account allows balances that a savings account would reject.
This is an LSP violation: StudentAccount weakens a postcondition (minimum balance guarantee) of its parent class.
Alternative design: Make StudentAccount inherit directly from BankAccount instead of SavingsAccount. Both SavingsAccount and StudentAccount are siblings with different rules, not parent-child. Shared logic (like withdrawal limit checking) can be extracted into a mixin or utility method rather than forcing an inheritance relationship.