Demo: drawing the UML, then coding Customer and Account
On This Lesson 11 topics
Lesson Overview
This is a follow-along demo that puts the whole module together. You will draw the diagram for a small bank first, then write the code from the diagram, in the same order a real designer would. On the way you will make one deliberate mistake that every beginner makes with two-way links, watch it fail, and fix it.
In the family of contacts picture, you are now writing down who knows whom, who owns whom, and checking that what you wrote in the family tree matches what the family actually does.
Why this matters: in real projects the diagram and the code drift apart unless someone checks. The habit you practise here, "draw, code, compare", prepares you for the lab, Bank Accounts v4, and for the entity models you will meet when you work with databases.
This lesson builds on the four earlier lessons of the module and on the Account classes from earlier modules. List is still a preview here, as in lesson 2.
Learning Objectives
By the end of this lesson you will be able to:
- Draw a diagram for a small domain before writing code.
- Code immutable
Transaction, compositeAccount, andCustomerwithopenAccount, from a diagram. - Implement a two-way link between
CustomerandAccountso that both sides stay in sync. - Recognise and fix the "back link never set" bug.
- Name each relationship in a note and confirm that diagram and code match.
- Decide when to drop a back link.
Written Explanation
The task and the plan
The bank rules, in plain English:
- A customer can open many accounts. Each account belongs to exactly one customer.
- An account keeps its history of many transactions. A transaction has a type, an amount, and a date, and it never changes after it is created.
The plan has six steps:
- Sketch the diagram.
- Code
Transactionas a small immutable class. - Code
Accountwith a private list of transactions. - Code
Customerwith a private list of accounts andopenAccount. - Deliberately introduce the back-link mistake, watch it fail, and fix it.
- Compare diagram and code.
Step 1: the diagram
Customer 1 -------- * Account (association: owner / accounts)
Account 1 ◆------- * Transaction (composition: history)Note that we draw no fields. The lines say everything. Customer to account is an association: we assume an account object is not simply part of a customer's body, and that it has its own life in the bank's rules. Account to transaction is composition: the account creates its transactions and no one else touches them.
Step 2: Transaction, immutable
A transaction is a fact about the past. If it could change, the history would not be trustworthy. So it gets final fields, no setters, and no list inside it. Three strings and a number are immutable already, so no defensive copies are needed.
Step 3: Account, the composite
Account creates its Transaction objects inside deposit and withdraw, keeps the list private, and returns a copy. It also holds a Customer field: the other half of the two-way link.
Step 4: Customer, the one-to-many holder
Customer keeps a List<Account>. openAccount creates the account and adds it to the list. It does not return the list itself.
Step 5: the planned mistake
We make Account hold its Customer, and then forget to set it. This happens often with bidirectional links: two fields must always describe one fact. If they are filled in two separate places, one will eventually be missing. The cure is to fill both in one method.
Step 6: compare
Count the lines in the diagram and the link fields in the code. They must match one for one.
Examples
Example 1: the first, broken version (simple)
class Account {
private final String number;
private Customer owner; // back link, easy to forget
private final List<Transaction> transactions = new ArrayList<>();
private double balance;
Account(String number) { this.number = number; }
void deposit(double amount, String date) {
balance += amount;
transactions.add(new Transaction("DEPOSIT", amount, date));
}
String summary() { return number + " (" + owner.getName() + ") balance " + balance; }
}
class Customer {
private final String name;
private final List<Account> accounts = new ArrayList<>();
Customer(String name) { this.name = name; }
String getName() { return name; }
Account openAccount(String number) {
Account account = new Account(number);
accounts.add(account); // forgot: tell the account who its owner is
return account;
}
}Running this in main:
Customer amal = new Customer("Amal");
Account acc = amal.openAccount("A-1");
acc.deposit(100, "2026-03-01");
System.out.println(acc.summary());fails (JDK 21):
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Customer.getName()" because "this.owner" is null
at Account.summary(Main.java:28)
at Main.main(Main.java:49)The customer knows its account, but the account does not know its customer: the two sides are out of sync, and nobody ever set owner.
Example 2: the fix, both sides set in one method (practical)
The account requires its owner at construction, and openAccount passes this:
class Account {
private final String number;
private final Customer owner;
private final List<Transaction> transactions = new ArrayList<>();
private double balance;
Account(String number, Customer owner) {
if (owner == null) {
throw new IllegalArgumentException("an account needs an owner");
}
this.number = number;
this.owner = owner;
}
// deposit, withdraw, summary, transactions() as before
}
class Customer {
// ...
Account openAccount(String number) {
Account account = new Account(number, this); // both sides set in one place
accounts.add(account);
return account;
}
List<Account> accounts() { return List.copyOf(accounts); }
}The Transaction class is as sketched in step 2:
class Transaction {
private final String type;
private final double amount;
private final String date;
Transaction(String type, double amount, String date) {
this.type = type;
this.amount = amount;
this.date = date;
}
String describe() { return date + " " + type + " " + amount; }
}The date is a plain String for now, to avoid new topics. Running a small main (two accounts, a deposit, a withdrawal, then printing each account and its transactions) gave:
A-1 (Amal) balance 70.0
2026-03-01 DEPOSIT 100.0
2026-03-02 WITHDRAW 30.0
A-2 (Amal) balance 50.0
2026-03-03 DEPOSIT 50.0An account cannot exist without an owner, and the only ordinary path to a new account sets both sides.
Example 3: what the fix does not prevent (realistic)
Nothing stops other code from calling the constructor directly:
Account stray = new Account("A-9", amal); // bypasses openAccount
System.out.println(stray.summary() + " | customer lists " + amal.accounts().size() + " accounts");A-9 (Amal) balance 0.0 | customer lists 2 accountsThe account says its owner is Amal, but Amal's list does not contain it: out of sync again, in the other direction. Two honest options exist at this level:
- Document the rule that accounts are created only through
openAccount, and review code for violations. - Drop the back link if nobody needs
account.getOwner(). One-directional links cannot disagree. In the diagram, that means a line with an arrow fromCustomertoAccount.
A later module on packages shows how to make the constructor invisible outside the package, which closes the gap. Here we keep the first option and the diagram unchanged.
Finally, the comparison step. The diagram has two lines. The code has a List<Account> in Customer (and the Customer field in Account for the other end of the first line), and List<Transaction> in Account. Each line has matching fields, and Transaction has no link fields at all. Diagram and code match.
Backend Insight
The two-way links in this lesson are exactly what object-relational frameworks have to manage when they map objects to tables: a customer row, many account rows each pointing back with a customer id. When both sides exist in memory, someone has to keep them consistent, and the safest design is the one you just wrote: one method creates the child and sets both ends.
A second lesson for backend design: prefer one direction unless a use case needs both. Many production bugs come from a back link that was added "just in case" and never kept in sync.
Common Mistakes
Mistake 1: Setting the back link in a separate step
account.setOwner(this) called by someone else after construction is easy to forget. Set the link in the constructor, from the same method that adds to the list.
Mistake 2: Adding the account to the list but not the owner (or the reverse)
Both directions describe one fact. If you update one, update the other in the same method.
Mistake 3: Returning the live list from accounts()
Outsiders could add an account behind the customer's back and skip openAccount. Return List.copyOf(accounts).
Mistake 4: Letting the diagram drift
If you add a field link in code, add the line in the diagram, and the reverse. Compare before you call a task finished.
Mistake 5: Making Transaction mutable
A setter on Transaction lets history be rewritten. Keep it immutable.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Put the demo steps in order: code Customer; sketch the diagram; compare; code Transaction; code Account.
Show solution A1
A1. Sketch the diagram; code Transaction; code Account; code Customer; compare.
A2. Why is Transaction immutable?
Show solution A2
A2. It is a record of something that happened. If it could be changed, the account's history could no longer be trusted.
A3. Which relationship does Account 1 ◆---- * Transaction show, and what does the diamond tell you about who creates the transactions?
Show solution A3
A3. Composition. The diamond sits on the Account end, so the account is the whole: it creates and owns its transactions.
Level B: Application
B1. Write Customer.accounts() so that callers cannot change the customer's list. What does List.copyOf do when the caller tries add?
Show solution B1
B1. List<Account> accounts() { return List.copyOf(accounts); }. An attempt to call add on the returned list throws UnsupportedOperationException, as in lesson 2.
B2. A Member has a list of Loan objects, and each Loan has a Member field. Show how Member.borrow(String title) creates the loan so both sides are set.
Show solution B2
B2. (We compiled and ran this.)
Loan borrow(String title) {
Loan loan = new Loan(this, title); // the loan gets its member here
loans.add(loan); // the member gets its loan here
return loan;
}With new Member("Lina"), m.borrow("Java").describe() gives Lina has Java and loanCount() is 1.
B3. In Example 3, why does stray not appear in amal.accounts()? Name two ways to avoid the problem.
Show solution B3
B3. It was created by calling the constructor directly, so no code added it to the customer's list. Avoid the problem by creating accounts only through openAccount (a rule, enforced by review), or by removing the back link so there is nothing to disagree. Making the constructor inaccessible outside the package is a technique for a later module.
Level C: Challenge
C1. Add Account findAccount(String number) to Customer, returning the account or null when none matches. Also state what the caller must do with the result.
Show solution C1
C1. (Compiled and run.)
Account findAccount(String number) {
for (Account account : accounts) {
if (account.getNumber().equals(number)) {
return account;
}
}
return null;
}The method assumes Account has a getNumber() getter. For an unknown number it returns null, and calling findAccount("X") on a customer without that account printed null in our test. The caller must check for null before using the result.
C2. A teammate wants a getOwner() on Account "just in case". Make the argument for and against keeping the back link, then pick one.
Show solution C2
C2. For: account.getOwner() makes statements and messages easy (summary() uses it). Against: a second field to keep in sync, and a bug source if someone creates accounts outside openAccount. A fair pick: keep it only if some code needs it, and only with the constructor-sets-it design from Example 2. If nobody calls getOwner(), drop the back link.
Mini Challenge
Start from an empty file and build the lab, Bank Accounts v4, in the order of this demo, with one change: make withdraw refuse any amount larger than the balance (as in the code we ran for Example 2). Before writing code, write one note that names each relationship type (Customer-Account, Account-Transaction). After coding, write a three-line list: lines in the diagram, link fields in the code, and whether they match.
Check your work: the account cannot exist without its owner, the transaction list is private and copied on the way out, and the note says "association" and "composition" in the right places.
Summary & Next Lesson
Key takeaways
- Draw first, then code, then compare. Every diagram line should have matching fields.
Transactionis immutable;Accountcreates and owns its transactions;Customerholds its accounts.- A two-way link must be set on both sides in one method, ideally the constructor path.
- If nobody needs the back link, drop it: one direction cannot get out of sync.
- Hand out copies of lists, not the lists themselves.
Next lesson
This is the last lesson of the module. Finish the lab, Bank Accounts v4, and then check the quiz. The course outline lists "Java object model" as module BEG-026, whose first lesson is "The Object class". It studies what every Java class inherits, including how to compare objects for equality.
Self-check
- In what order do you do the steps of this demo?
- Why did the first version throw
NullPointerException? - How does
openAccountkeep both sides in sync? - When should you drop a back link?
- How do you hand out the customer's accounts safely?
Show answers
- Draw the diagram, code
Transaction,Account, andCustomer, then compare diagram and code. Accounthad anownerfield that nobody set, soowner.getName()was called onnull.- It creates the account with
thisas the owner and adds it to the list in the same method. - When no code needs to go from an account back to its customer.
- Return
List.copyOf(accounts).
References
- Oracle, List (Java SE 21 API). Supports that
List.copyOfreturns an unmodifiable list, so mutators such asaddthrowUnsupportedOperationException, and that later changes to the source are not reflected in the copy. - Oracle, ArrayList (Java SE 21 API). Supports
ArrayListas aListimplementation with an empty no-argument constructor. - Wikipedia, Class diagram. Supports the diamond and multiplicity notation used in the diagram.
- All program output and the exception text in this lesson come from compiling and running the code with JDK 21.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.