Intermediate project · Medium
Bank account ledger
You build a small set of account classes that hold money as whole cents, refuse deposits and withdrawals that do not make sense, and record every accepted operation in a transaction history. A printStatement method prints that history as an aligned table with a closing balance. Your main method runs a scripted sequence of operations, including several that must be rejected, so the statement itself is the evidence that the rules work.
- Languages
- Java
- Size
- Medium: a few sessions
- Where to build it
- Runs in the Java playground, which is a real compiler but has no standard input, so the brief is written for a scripted main method rather than a menu. Runtime faults there are not catchable either, which is why every rule is built on an exception you throw yourself. Open the Java playground →
What you will practise
- encapsulation: private fields with no setter for the balance
- constructors, inheritance and overriding
- throwing and catching your own exception type
- money as integer cents instead of floating point
- keeping an append-only history alongside mutable state
- formatting a table without a formatting shortcut
Requirements
The project is done when every one of these is true.
- An Account class holds the balance in a private field and exposes it only through a getter; there is no public setter and no way to set a balance from outside except through deposit and withdraw.
- All money is a whole number of cents in a long. No field, parameter or return value anywhere in the project is a double or a float.
- deposit and withdraw reject an amount of zero or less by throwing your own checked or unchecked exception type, with a message that says what was wrong.
- withdraw rejects an amount the account cannot cover and leaves the balance and the history completely unchanged.
- Every accepted operation appends one immutable transaction record holding the kind, the amount and the balance afterwards, and the history can be read but not modified from outside.
- printStatement prints one aligned row per transaction plus a closing balance, with amounts shown in the usual two-decimal form built from the cent value.
- There are at least two subclasses sharing the base class, such as a savings account that adds interest and a current account with an overdraft limit, and each overrides toString.
- Adding interest is computed in whole cents with one stated rounding rule, written in a comment, and applied consistently.
- main runs a fixed scripted sequence with no input at all, including at least three operations that must be rejected, and the rejections are caught and reported rather than stopping the program.
Milestones
A sensible order to build it in, so something works at every step.
Make the balance impossible to corrupt
Write Account with a private long balance, a constructor that validates the opening amount, and a getter. Prove from main that nothing outside the class can change it.
Add deposit and withdraw with their rules
Implement both, throwing your own exception for a non-positive amount and for a withdrawal the balance cannot cover, and check that a rejected call leaves the balance where it was.
Add the transaction record and the history
Write a small immutable record for a transaction, append one on every accepted operation, and return the history in a way that cannot be modified from outside.
Print the statement
Write a cents-to-string helper, then print the header, one row per transaction and the closing balance, all through the same widths so the columns line up.
Add the two subclasses
Extend Account for savings with an addInterest method and for a current account that overrides the can-I-cover-this check to allow an overdraft down to a limit, overriding toString in both.
Script the scenario in main
Write a sequence of operations that exercises every rule, including the rejections, catch your exception around each one, and print the statements at the end.
Hints
Open one only when you are stuck. Each gives a little more away.
Show hint 1Hint 1
The runtime here has no standard input at all, so there is no Scanner to reach for. Put the whole scenario in main as a list of operations; a scripted scenario is also easier to re-run identically, which is what you want while testing rules.
Show hint 2Hint 2
Runtime faults such as dividing by zero or using a null reference are not catchable in this runtime and stop the program outright. Exceptions you throw yourself work normally, so build every rule on your own thrown exception rather than on catching a fault.
Show hint 3Hint 3
e.getMessage() is not available here. Print e.toString() instead, and put the useful detail in the message you pass to the constructor so it shows up there.
Show hint 4Hint 4
%f and %n do not work in format strings in this runtime, and you do not need them: store cents as a long and build the display string from the whole part and the last two digits, which is exact anyway.
Show hint 5Hint 5
Whether the overdraft is allowed is the only thing that differs between the two account types. Put that single decision in one protected method and override it, rather than duplicating withdraw.
Show hint 6Hint 6
Decide your interest rounding rule before you write it, because 2.5 per cent of 999 cents is 24.975 cents and there is no right answer, only a consistent one. Write the rule in a comment and make your tests match it.
How to test it
Run these checks yourself, or turn them into automated tests once you know how.
- Open an account at 0, deposit 5000 cents, and the balance prints as 50.00 with exactly one row in the history.
- From a balance of 5000, withdraw 7500: the operation is rejected, the balance still prints as 50.00, and the history still has exactly one row.
- Deposit -100, then deposit 0: both rejected with your exception, and neither the balance nor the history changes. The zero case catches code that only tested for negative amounts.
- Deposit 1 cent onto 5000 and the balance prints as 50.01, proving you reject only zero and below rather than small amounts.
- Give a current account an overdraft limit of 10000 cents and a balance of 5000, then withdraw 12000: allowed, balance prints as -70.00. Now withdraw 3001: rejected, because that would reach -100.01. Withdraw 3000 instead: allowed, and the balance prints as exactly -100.00.
- Give a savings account 120000 cents and add 2.5 per cent interest: the interest is 3000 cents and the balance prints as 1230.00, with one extra history row.
- Add 2.5 per cent interest to a savings balance of 999 cents. The exact value is 24.975 cents, so check the result matches the rounding rule in your comment: 1023 cents if you truncate, 1024 if you round half up.
- Hold onto the list returned by your history getter and try to add to it: it should refuse, so the history cannot be rewritten from outside.
Stretch goals
- Add a transfer method between two accounts that either moves the money and records two transactions, or changes neither account.
- Add a monthly statement that filters the history by date and shows an opening and closing balance.
- Add an interest-bearing account whose rate depends on the balance band, with the bands in a data table.
- Add a daily withdrawal limit tracked across transactions, and reject the operation that would exceed it.
- Write a small assertion helper and turn the whole test list below into a self-checking run that prints a pass count.