CHAPTER 02 · BEGINNER4 min read

Guided Project: Bank Account Transaction Ledger

Build a robust, object-oriented banking transaction ledger enforcing deposit invariants, overdraft rules, and audit logs.

OpenJDK 21+ (LTS)Free to readModule 1 of 3
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Model domain entities including BankAccount, Transaction, and AccountType.
  • Enforce strict domain invariants preventing illegal negative balances and malformed transactions.
  • Expose clean ledger audit reporting methods suitable for external integration.

01Designing the Banking Domain Model

Financial software demands uncompromising precision, strict boundary enforcement, and complete auditability. Every single monetary movement must be recorded as an immutable transaction that cannot be altered or retroactively erased.

In this guided project, you will architect a production-grade BankAccount system. It encapsulates account balances, verifies deposit and withdrawal invariants, and maintains an unalterable historical ledger of all operations.

In enterprise systems running on the Java Virtual Machine, clean architecture and predictable memory dynamics directly influence operational stability. By establishing strict access boundaries, validating preconditions, and managing object lifecycles deliberately, engineers ensure that high-throughput applications remain stable under heavy load without degrading garbage collection metrics.

example.js☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02Enforcing Account Invariants and Balance Protection

A core duty of an encapsulated class is to preserve its own invariants under all circumstances. For a bank account, this means an account cannot accept zero or negative transaction amounts, and standard checking accounts must never exceed allowable overdraft limits.

By validating each operation prior to state alteration, we guarantee that account balances remain mathematically consistent and completely immune to invalid operations.

Furthermore, designing with interface contracts and encapsulation shields internal implementation details from external changes. When downstream services rely on abstractions, engineering teams can optimize underlying algorithms and internal structures without introducing breaking changes across distributed microservice boundaries.

example.js☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Recording Immutable Ledger Entries

Whenever money moves, a new Transaction record is instantiated with a unique UUID, an ISO timestamp, the transaction category, the transfer amount, and the post-transaction balance. This entry is appended to the internal ledger.

To prevent external callers from modifying the historical ledger, our class returns an unmodifiable view via Collections.unmodifiableList(). This defensive copying pattern preserves audit integrity.

example.js☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Assembling and Validating the Complete Account Service

Finally, we implement withdrawal operations with balance verification, deposit methods with currency checks, and a comprehensive summary report generator. We test edge cases, including insufficient funds and invalid zero-dollar transactions.

The completed module provides an industrial-strength example of clean object-oriented architecture on the Java Virtual Machine.

In conclusion, adopting these engineering practices transforms prototype scripts into resilient, production-ready modules. Continual testing against real-world edge cases, monitoring telemetry metrics, and documenting architectural decisions ensures the long-term maintainability and performance of the codebase as system demands expand.

In conclusion, adopting these engineering practices transforms prototype scripts into resilient, production-ready modules. Continual testing against real-world edge cases, monitoring telemetry metrics, and documenting architectural decisions ensures the long-term maintainability and performance of the codebase as system demands expand.

example.js☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
LET IT CLICK

The ideas worth keeping.

  • Enforce domain invariants inside constructors and methods before state mutation occurs.
  • Record immutable audit logs for sensitive operations like financial transactions.
  • Protect internal collections from external mutation using Collections.unmodifiableList().
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Account Number Masker

Write a static method maskAccount(String acc) that returns '****1234' showing only the final 4 digits of an account number.

your_turn_1.java☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.javaJava
public static String maskAccount(String acc) {    if (acc == null || acc.length() <= 4) return "****";    return "****" + acc.substring(acc.length() - 4);}

Masking sensitive customer account numbers protects personally identifiable information in logging and reporting systems.

02

Ledger Volume Aggregator

Write a method calculateTotalDeposits(List<Transaction> txs) that sums the amounts of all DEPOSIT transactions using streams.

your_turn_2.java☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.javaJava
public static double calculateTotalDeposits(List<Transaction> txs) {    return txs.stream()        .filter(t -> t.type() == TransactionType.DEPOSIT)        .mapToDouble(Transaction::amount)        .sum();}

Java streams provide a declarative, expressive mechanism to filter and aggregate collections without imperative loop boilerplate.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why should collections stored inside domain models be returned using Collections.unmodifiableList()?
02What is an invariant in object-oriented software engineering?
03What is the primary benefit of Java records over traditional JavaBean classes?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Account Number Masker

Write a static method maskAccount(String acc) that returns '****1234' showing only the final 4 digits of an account number.

EXPECTED OUTPUT
Build successful. (Java 15)
[Process exited normally]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

MODULE 01 · PRACTICAL SELF-CHECK

Pause. Put the pieces together.

You’ve reached the end of Object-Oriented Programming & Java Core. Before moving on, use this task to check what you can do independently.

Pass encapsulation tests and construct the guided banking transaction engine.

Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.

Review this module

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.