This is the module's integrating lesson. Everything you have learnt in the previous five —conditionals, loops, switch, break and continue, and the method for debugging when something does not add up— comes together here in a single complete application: version 2.0 of BiblioTechApp, the program Nexus Software will use at the desk of its technical library. It no longer ends after a single loan: it shows a menu, handles operations one after another, demands correct data before accepting it, accumulates statistics for the whole session and signs off with an orderly close when the librarian decides to leave. We will not write it in one go: we will build it in five iterations, each adding a layer on top of the previous one, exactly as real software is developed. And we will finish with the most honest part of all: the list of what this program still cannot do, with the course module that solves each shortcoming.
Contents
- The complete specification
- Iteration 1: the menu skeleton
- Iteration 2: validating every input
- Iteration 3: the return logic
- Iteration 4: the session statistics
- Iteration 5: polishing the output
- The complete final code, commented
- Compiling and running
- A complete test session
- Limitations: what it cannot do and which module will solve it
- Common Mistakes and Tips
- Exercises
- The complete specification
As in lesson 01-07, you write what the program must do first and only afterwards how.
Name: BiblioTechApp (version 2.0)
Package: com.nexussoftware.bibliotech
Expected behaviour:
- Show a header with the company, the application name and the version.
- Show a menu in a loop with four options and not finish until the user chooses to exit:
- Option 1 (register a return): ask for the employee, the title, the ISBN and the elapsed days; validate each value; calculate the delay, the capped fine, the status and the severity; print a formatted receipt with a unique reference.
- Option 2 (simulate a loan): ask for the employee and the title; report the term, the rate that will apply and when the cap would be reached.
- Option 3 (session summary): show the accumulated figures for everything done since the program started.
- Option 0 (exit): print a closing message with the total collected and finish.
- Any other input must show a warning and go back to the menu without breaking anything.
Business rules, the same ones since module 1:
| Rule | Value | Constant |
|---|---|---|
| Standard loan duration | 15 days | LOAN_DAYS |
| Rate per day late | €0.25 | DAILY_RATE |
| Maximum fine per return | €20.00 | MAX_FINE |
| Minor delay threshold | 7 days | MINOR_THRESHOLD |
Validation rules:
| Value | Condition to accept it |
|---|---|
| Employee | At least 3 characters after trim() |
| Title | Not empty or blank |
| ISBN | Starts with 978 and is exactly 14 characters long |
| Elapsed days | Digits only, and a value between 0 and 3650 |
Session statistics (all in scalar variables, which is all you have):
| Variable | What it holds |
|---|---|
returnsRegistered |
Counter of returns processed |
lateReturns |
How many of them arrived late |
capsReached |
How many reached MAX_FINE |
loansSimulated |
Counter of loans |
totalFines |
Accumulator of amounts |
highestDelay |
Maximum days late seen |
highestDelayEmployee |
Name associated with that maximum |
highestDelayBook |
Title associated with that maximum |
Technical constraints: everything inside main, no classes of your own, no arrays or collections, no exceptions. Only what has been learnt so far.
The program's complete flow:
flowchart TD
INI["Welcome header<br/>and counter initialisation"] --> MENU["Show the menu"]
MENU --> LEER["Read the option with nextLine()"]
LEER --> SW{"switch (option)"}
SW -- "1" --> V1["Validate employee, title,<br/>ISBN and days (do-while loops)"]
V1 --> C1["Calculate delay, fine,<br/>status and severity"]
C1 --> E1["Update the statistics"]
E1 --> R1["Print the receipt"] --> MENU
SW -- "2" --> V2["Validate employee and title"]
V2 --> R2["Report the term<br/>and the rate"] --> MENU
SW -- "3" --> R3["Print the session<br/>summary"] --> MENU
SW -- "other" --> ERR["Warning: option not recognised"] --> MENU
SW -- "0" --> FIN["exit = true"]
FIN --> CIERRE["Close with the total collected<br/>and scanner.close()"]
- Iteration 1: the menu skeleton
First, the frame: show the menu, read an option, dispatch it and repeat. No business logic, just placeholder messages.
package com.nexussoftware.bibliotech;
import java.util.Scanner;
public class BiblioTechApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
boolean exit = false;
while (true) {
System.out.println();
System.out.println(" 1. Register a return");
System.out.println(" 2. Simulate a loan");
System.out.println(" 3. View session summary");
System.out.println(" 0. Exit");
System.out.print(" Option: ");
String option = scanner.nextLine().trim();
switch (option) {
case "1" -> System.out.println(" [register a return]");
case "2" -> System.out.println(" [simulate a loan]");
case "3" -> System.out.println(" [session summary]");
case "0" -> exit = true;
default -> System.out.println(" Option not recognised.");
}
if (exit) {
break;
}
}
System.out.println("Session closed.");
scanner.close();
}
}Three design decisions worth understanding before going on, because they condition everything else:
1. The option is read as a String, not as an int. If you used Integer.parseInt directly and the user typed hello, the program would stop with a NumberFormatException. Reading it as text, any odd input falls cleanly into the default.
2. The exit is controlled with the exit flag and a break placed in the loop's body, not inside the switch. This is the direct application of the trap you studied in lesson 02-04: a break written inside a switch branch —even with the arrow syntax, where case "0" -> { … break; } compiles perfectly— would leave the switch and not the while, and the menu would be impossible to leave. By putting if (exit) { break; } after the switch, the break is directly in the loop's body and there is no ambiguity.
3. The switch uses arrows. No break between branches, no possible fall-through, and with braces delimiting each branch's scope, so that variables declared in option 1 do not clash with those in option 2.
Test it before continuing: it must cycle through the menu indefinitely, warn about unknown options and close with 0. If it does not close, review point 2.
- Iteration 2: validating every input
Now the gaps are filled with data reading, applying the validation loop from lesson 02-02: ask, check, and ask again while the value is no good.
String employee;
do {
System.out.print(" Employee: ");
employee = scanner.nextLine().trim();
if (employee.length() < 3) {
System.out.println(" The name must have at least 3 characters.");
}
} while (employee.length() < 3);The interesting case is the number. Remember that neither an if nor a loop can prevent Integer.parseInt("twenty") from stopping the program: the failure happens inside the conversion. But you can check beforehand that the string is made up only of digits, going through it character by character with a for and charAt:
int elapsedDays = 0;
boolean validDays = false;
do {
System.out.print(" Elapsed days (0-3650): ");
String input = scanner.nextLine().trim();
// Prior check: are all the characters digits?
boolean onlyDigits = !input.isEmpty();
for (int i = 0; i < input.length(); i++) {
if (!Character.isDigit(input.charAt(i))) {
onlyDigits = false;
break; // as soon as one fails, there is no need to continue
}
}
if (!onlyDigits) {
System.out.println(" Type a whole number with no signs or letters.");
continue; // discard this pass and ask again
}
// Here it is already safe to convert: parseInt cannot fail.
elapsedDays = Integer.parseInt(input);
validDays = elapsedDays <= 3650;
if (!validDays) {
System.out.println(" Value out of range.");
}
} while (!validDays);This fragment condenses half the module: a validation do-while, a traversal for, a break that cuts the check as soon as it finds an invalid character, a continue that discards the iteration when the input is not numeric and a boolean flag that governs the outer loop's condition.
And notice an elegant side effect: since the - sign is not a digit, negative days are rejected automatically by the very same check. That promise from lesson 02-01 —"the program no longer accepts negative days"— is fulfilled without a single extra condition.
This prior check is a home-made defence. In module 6 you will replace it with a try-catch, which is the correct and general way of handling malformed input.
- Iteration 3: the return logic
With the data now guaranteed valid, the module 1 business rules and the lesson 02-01 decision ladder are applied:
// 1. Delay clamped to zero: never negative.
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0;
}
// 2. Breakdown into weeks and leftover days (integer division and modulo).
int weeksLate = daysLate / 7;
int leftoverDays = daysLate % 7;
// 3. Capped fine.
double fine = daysLate * DAILY_RATE;
boolean capped = fine >= MAX_FINE;
if (capped) {
fine = MAX_FINE;
}
// 4. Status and severity: the decision table from lesson 02-01.
String status;
String severity;
if (daysLate == 0) {
status = "ON TIME";
severity = "NONE";
} else {
status = "OVERDUE";
if (capped) {
severity = "MAXIMUM";
} else if (daysLate <= MINOR_THRESHOLD) {
severity = "MINOR";
} else {
severity = "SEVERE";
}
}Notice the order of the inner ladder: the cap is checked before the minor threshold, because a capped fine is always the priority classification, regardless of the days. It is exactly the decision table you built in lesson 02-01, and here you can see why it was worth writing it before the code.
The receipt reference is composed with module 1's String methods and the session counter, so that every receipt in the session is unique:
String reference = "RET-" + isbn.substring(isbn.length() - 4)
+ "-" + (returnsRegistered + 1);
// With ISBN 978-0000000001 and the first return -> RET-0001-1
- Iteration 4: the session statistics
Here the accumulation patterns from lesson 02-02 come in: counter, accumulator and maximum with associated data. All these variables are declared before the while, because they must survive every pass through the menu; if they were inside the loop, they would reset on every operation (which is exactly the bug you debugged in lesson 02-05).
// OUTSIDE the menu loop: they survive the whole session.
int returnsRegistered = 0;
int lateReturns = 0;
int capsReached = 0;
int loansSimulated = 0;
double totalFines = 0.0;
int highestDelay = 0;
String highestDelayEmployee = "-";
String highestDelayBook = "-";And the update, inside the return branch, after the calculation:
returnsRegistered++; // counter
totalFines += fine; // accumulator
if (daysLate > 0) {
lateReturns++;
}
if (capped) {
capsReached++;
}
// Maximum WITH associated data: the three variables are updated together.
if (daysLate > highestDelay) {
highestDelay = daysLate;
highestDelayEmployee = employee;
highestDelayBook = title;
}The maximum-with-associated-data pattern deserves attention: when you find a new record, you have to update all the variables describing it in the same if. Forgetting one produces an inconsistent summary ("highest delay: 105 days, employee: Marta Ruiz" when it was really Diego Alonso). And that awkwardness —three loose variables that are really one single piece of data— is the exact argument that opens module 3: the natural thing would be an object with three fields.
In the summary you have to protect yourself from division by zero, just as you learnt while debugging:
double averageFine;
if (returnsRegistered == 0) {
averageFine = 0.0; // with no returns there is no average
} else {
averageFine = totalFines / returnsRegistered;
}
- Iteration 5: polishing the output
The last iteration adds no functionality: it makes the program look professional. The techniques are module 1's, printf and String.repeat, applied with judgement.
- Fixed widths with
%-18sso that the labels line up in a column. %.2ffor every amount. A receipt saying3.0 EURis not acceptable.- Separators with
"=".repeat(WIDTH)and aWIDTHconstant, to avoid repeating the magic number everywhere. - Indentation by level: the menu at two spaces, the form fields at four. It helps you understand the hierarchy of what you are reading without any graphical decoration.
- A prominent warning only when the cap is reached, instead of repeating irrelevant information on every receipt.
- The complete final code, commented
package com.nexussoftware.bibliotech;
import java.util.Scanner;
/**
* BiblioTech 2.0 - Management of the Nexus Software technical library.
*
* Module 2 version (Control Flow): interactive menu with input validation
* and session statistics. All the code lives in main and uses only scalar
* variables, conditionals and loops.
*/
public class BiblioTechApp {
public static void main(String[] args) {
// ===== CONSTANTS: business rules and configuration =====
final String COMPANY_NAME = "Nexus Software";
final String VERSION = "2.0 (module 2)";
final int LOAN_DAYS = 15;
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
final int MINOR_THRESHOLD = 7;
final int MAX_DAYS = 3650;
final int WIDTH = 52;
Scanner scanner = new Scanner(System.in);
// ===== SESSION STATISTICS =====
// Declared OUTSIDE the loop: they must survive every operation.
int returnsRegistered = 0;
int lateReturns = 0;
int capsReached = 0;
int loansSimulated = 0;
double totalFines = 0.0;
int highestDelay = 0;
String highestDelayEmployee = "-";
String highestDelayBook = "-";
// ===== HEADER =====
System.out.println("=".repeat(WIDTH));
System.out.println(" BIBLIOTECH " + VERSION);
System.out.println(" Technical library of " + COMPANY_NAME);
System.out.println("=".repeat(WIDTH));
boolean exit = false;
// ===== MAIN MENU LOOP =====
while (true) {
System.out.println();
System.out.println("-".repeat(WIDTH));
System.out.println(" MAIN MENU");
System.out.println("-".repeat(WIDTH));
System.out.println(" 1. Register a return");
System.out.println(" 2. Simulate a loan");
System.out.println(" 3. View session summary");
System.out.println(" 0. Exit");
System.out.print(" Option: ");
// Read as text: any odd input falls into the default.
String option = scanner.nextLine().trim();
switch (option) {
// ---------- OPTION 1: REGISTER A RETURN ----------
case "1" -> {
System.out.println();
System.out.println(" --- REGISTER A RETURN ---");
// Employee name validation
String employee;
do {
System.out.print(" Employee: ");
employee = scanner.nextLine().trim();
if (employee.length() < 3) {
System.out.println(" The name must have at least 3 characters.");
}
} while (employee.length() < 3);
// Title validation
String title;
do {
System.out.print(" Book title: ");
title = scanner.nextLine().trim();
if (title.isBlank()) {
System.out.println(" The title cannot be empty.");
}
} while (title.isBlank());
// ISBN validation: 978 prefix and exact length
String isbn;
do {
System.out.print(" ISBN (978-XXXXXXXXXX): ");
isbn = scanner.nextLine().trim();
if (!isbn.startsWith("978") || isbn.length() != 14) {
System.out.println(" ISBN not valid for this library.");
}
} while (!isbn.startsWith("978") || isbn.length() != 14);
// Days validation: first that they are digits, then the range
int elapsedDays = 0;
boolean validDays = false;
do {
System.out.print(" Elapsed days (0-" + MAX_DAYS + "): ");
String input = scanner.nextLine().trim();
boolean onlyDigits = !input.isEmpty();
for (int i = 0; i < input.length(); i++) {
if (!Character.isDigit(input.charAt(i))) {
onlyDigits = false;
break; // one bad character is enough: stop here
}
}
if (!onlyDigits) {
System.out.println(" Type a whole number with no signs or letters.");
continue; // ask again
}
elapsedDays = Integer.parseInt(input);
validDays = elapsedDays <= MAX_DAYS;
if (!validDays) {
System.out.println(" Value out of range.");
}
} while (!validDays);
// ----- Business rules -----
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0; // clamped to zero
}
int weeksLate = daysLate / 7;
int leftoverDays = daysLate % 7;
double fine = daysLate * DAILY_RATE;
boolean capped = fine >= MAX_FINE;
if (capped) {
fine = MAX_FINE; // cap applied
}
String status;
String severity;
if (daysLate == 0) {
status = "ON TIME";
severity = "NONE";
} else {
status = "OVERDUE";
if (capped) {
severity = "MAXIMUM"; // the cap outranks the days
} else if (daysLate <= MINOR_THRESHOLD) {
severity = "MINOR";
} else {
severity = "SEVERE";
}
}
String reference = "RET-" + isbn.substring(isbn.length() - 4)
+ "-" + (returnsRegistered + 1);
// ----- Session statistics update -----
returnsRegistered++;
totalFines += fine;
if (daysLate > 0) {
lateReturns++;
}
if (capped) {
capsReached++;
}
if (daysLate > highestDelay) {
highestDelay = daysLate;
highestDelayEmployee = employee;
highestDelayBook = title;
}
// ----- Receipt -----
System.out.println();
System.out.println(" " + "=".repeat(WIDTH - 4));
System.out.println(" RETURN RECEIPT " + reference);
System.out.println(" " + "=".repeat(WIDTH - 4));
System.out.printf(" %-18s %s%n", "Employee:", employee);
System.out.printf(" %-18s %s%n", "Book:", title);
System.out.printf(" %-18s %s%n", "ISBN:", isbn);
System.out.printf(" %-18s %d%n", "Days on loan:", elapsedDays);
System.out.printf(" %-18s %d (%d wk. and %d days)%n",
"Days late:", daysLate,
weeksLate, leftoverDays);
System.out.printf(" %-18s %s / %s%n", "Status:", status, severity);
System.out.printf(" %-18s %.2f EUR%n", "Fine:", fine);
if (capped) {
System.out.println(" WARNING: fine capped. Notify HR.");
}
System.out.println(" " + "=".repeat(WIDTH - 4));
}
// ---------- OPTION 2: SIMULATE A LOAN ----------
case "2" -> {
System.out.println();
System.out.println(" --- SIMULATE A LOAN ---");
String employee;
do {
System.out.print(" Employee: ");
employee = scanner.nextLine().trim();
if (employee.length() < 3) {
System.out.println(" The name must have at least 3 characters.");
}
} while (employee.length() < 3);
String title;
do {
System.out.print(" Book title: ");
title = scanner.nextLine().trim();
if (title.isBlank()) {
System.out.println(" The title cannot be empty.");
}
} while (title.isBlank());
loansSimulated++;
System.out.println();
System.out.printf(" Loan #%d authorised.%n", loansSimulated);
System.out.printf(" %s -> %s%n", title, employee);
System.out.printf(" Term: %d days. From day %d onwards "
+ "%.2f EUR/day applies.%n",
LOAN_DAYS, LOAN_DAYS + 1, DAILY_RATE);
System.out.printf(" The %.2f EUR cap is reached after %d days "
+ "late.%n",
MAX_FINE,
(int) Math.ceil(MAX_FINE / DAILY_RATE));
}
// ---------- OPTION 3: SESSION SUMMARY ----------
case "3" -> {
// Guard against division by zero.
double averageFine;
if (returnsRegistered == 0) {
averageFine = 0.0;
} else {
averageFine = totalFines / returnsRegistered;
}
System.out.println();
System.out.println(" " + "=".repeat(WIDTH - 4));
System.out.println(" SESSION SUMMARY");
System.out.println(" " + "=".repeat(WIDTH - 4));
System.out.printf(" %-26s %d%n", "Returns registered:",
returnsRegistered);
System.out.printf(" %-26s %d%n", " late:",
lateReturns);
System.out.printf(" %-26s %d%n", " with capped fine:",
capsReached);
System.out.printf(" %-26s %d%n", "Loans simulated:",
loansSimulated);
System.out.printf(" %-26s %.2f EUR%n", "Total fines:", totalFines);
System.out.printf(" %-26s %.2f EUR%n", "Average fine:", averageFine);
if (highestDelay > 0) {
System.out.printf(" %-26s %d days%n", "Highest delay:", highestDelay);
System.out.printf(" %-26s %s%n", " employee:", highestDelayEmployee);
System.out.printf(" %-26s %s%n", " book:", highestDelayBook);
} else {
System.out.printf(" %-26s none%n", "Highest delay:");
}
System.out.println(" " + "=".repeat(WIDTH - 4));
}
// ---------- OPTION 0: EXIT ----------
// It only raises the flag: the break goes AFTER the switch.
case "0" -> exit = true;
// ---------- ANY OTHER INPUT ----------
default -> System.out.println(" Option not recognised. Use 0, 1, 2 or 3.");
}
// The break is in the loop's body, not inside the switch:
// that way it breaks the WHILE and not the SWITCH (lesson 02-04).
if (exit) {
break;
}
}
// ===== CLOSING =====
System.out.println();
System.out.println("=".repeat(WIDTH));
System.out.printf(" Session closed. %d returns, %.2f EUR collected.%n",
returnsRegistered, totalFines);
System.out.println(" Thank you for using BiblioTech - " + COMPANY_NAME);
System.out.println("=".repeat(WIDTH));
scanner.close();
}
}
- Compiling and running
From the root of the bibliotech project you created in module 1:
cd bibliotech
javac -d out src/main/java/com/nexussoftware/bibliotech/BiblioTechApp.java
java -cp out com.nexussoftware.bibliotech.BiblioTechAppFrom the IDE, the run button on the class is enough. And if something does not add up, you already know what to do: a breakpoint on the accumulator's line, the condition returnsRegistered == 2, and off to the variables window.
- A complete test session
This is the program's test table: each line is a user input and what should happen. Run it exactly as it is and compare.
| # | User input | Expected response |
|---|---|---|
| 1 | 1 |
Enters the return form |
| 2 | Marta Ruiz |
Accepted |
| 3 | Effective Java |
Accepted |
| 4 | 978-0000000001 |
Accepted |
| 5 | twenty |
Rejected: "Type a whole number with no signs or letters." |
| 6 | 27 |
Accepted. Receipt RET-0001-1: 12 days late, €3.00, OVERDUE / SEVERE |
| 7 | 1 |
A second return form |
| 8 | Diego Alonso |
Accepted |
| 9 | Refactoring |
Accepted |
| 10 | 978-0000000003 |
Accepted |
| 11 | -5 |
Rejected: the - sign is not a digit |
| 12 | 120 |
Accepted. Receipt RET-0003-2: 105 days, €20.00, MAXIMUM, with an HR warning |
| 13 | 2 |
Loan form |
| 14 | Nuria Vidal |
Accepted |
| 15 | Design Patterns |
Loan #1 authorised, 15-day term, cap after 80 days |
| 16 | 9 |
"Option not recognised. Use 0, 1, 2 or 3." |
| 17 | 3 |
Summary: 2 returns (2 late, 1 capped), 1 loan, €23.00 total, €11.50 average, highest delay 105 days by Diego Alonso with "Refactoring" |
| 18 | 0 |
Closing: "Session closed. 2 returns, 23.00 EUR collected." |
The most significant stretches of the actual output:
====================================================
BIBLIOTECH 2.0 (module 2)
Technical library of Nexus Software
====================================================
----------------------------------------------------
MAIN MENU
----------------------------------------------------
1. Register a return
2. Simulate a loan
3. View session summary
0. Exit
Option: 1
--- REGISTER A RETURN ---
Employee: Marta Ruiz
Book title: Effective Java
ISBN (978-XXXXXXXXXX): 978-0000000001
Elapsed days (0-3650): twenty
Type a whole number with no signs or letters.
Elapsed days (0-3650): 27
================================================
RETURN RECEIPT RET-0001-1
================================================
Employee: Marta Ruiz
Book: Effective Java
ISBN: 978-0000000001
Days on loan: 27
Days late: 12 (1 wk. and 5 days)
Status: OVERDUE / SEVERE
Fine: 3.00 EUR
================================================The second receipt, with the cap applied:
================================================
RETURN RECEIPT RET-0003-2
================================================
Employee: Diego Alonso
Book: Refactoring
ISBN: 978-0000000003
Days on loan: 120
Days late: 105 (15 wk. and 0 days)
Status: OVERDUE / MAXIMUM
Fine: 20.00 EUR
WARNING: fine capped. Notify HR.
================================================The session summary and the closing:
================================================
SESSION SUMMARY
================================================
Returns registered: 2
late: 2
with capped fine: 1
Loans simulated: 1
Total fines: 23.00 EUR
Average fine: 11.50 EUR
Highest delay: 105 days
employee: Diego Alonso
book: Refactoring
================================================
====================================================
Session closed. 2 returns, 23.00 EUR collected.
Thank you for using BiblioTech - Nexus Software
====================================================Manual verification of the figures, which is step 2 of the debugging method: Marta Ruiz's return is 27 − 15 = 12 days × 0.25 = €3.00; Diego Alonso's, 120 − 15 = 105 days × 0.25 = €26.25, capped at €20.00. Total €23.00, average €11.50. It matches.
Also try these edge cases, which is where the bugs live:
| Edge case | Correct behaviour |
|---|---|
Option 3 right after starting |
Everything at zero, "Highest delay: none", no division by zero |
0 elapsed days |
0 days late, ON TIME / NONE, €0.00 |
Exactly 15 days |
0 days late (day 15 is still within the term) |
16 days |
1 day late, €0.25, MINOR |
22 days |
7 late: MINOR (the threshold is <=) |
23 days |
8 late: SEVERE |
95 days |
80 late, exactly €20.00: MAXIMUM |
| Pressing Enter without typing anything | Falls into the default or repeats the question, never breaks |
- Limitations: what it cannot do and which module will solve it
This section is as important as the code. A good developer knows the shortcomings of what they have built and knows why they exist.
1. All the state consists of loose variables. highestDelayEmployee, highestDelayBook and highestDelay are three independent variables that really describe one single thing. Nothing stops you updating one and forgetting the other two, and the compiler cannot help you. The natural thing would be a Return object with its employee, book and daysLate fields grouped and consistent. → Module 3: Object-Oriented Programming, where you will create the Book, Employee and Loan classes.
2. Only one book fits at a time. Each return overwrites the previous one. You cannot list what was processed, nor search for a specific ISBN, nor sort by fine, nor consult a real catalog: the "catalog" is three hand-written lines. You need structures that hold many values under a single name. → Module 5: Data Structures and Collections, with arrays, ArrayList and HashMap.
3. Malformed input can still break the program. The digit check is home-made and only covers the case you anticipated. An Integer.parseInt of a twenty-digit number overflows the int range, and any other unforeseen operation stops the application with a stack trace. You need a general mechanism for handling the exceptional. → Module 6: Exception Handling, with try-catch, custom exceptions and try-with-resources (which will also close the Scanner properly).
4. Everything is lost on exit. The session summary disappears with the process. Tomorrow the librarian starts from scratch: no outstanding fines, no history, no persistent catalog. You need to save to disk and read back. → Module 7: File Input/Output, with reading and writing, CSV and Properties.
5. main is more than two hundred lines long. Each switch branch is a huge block and the forms in options 1 and 2 literally repeat the same employee and title validation loop. Duplicating code duplicates the places where a fault has to be fixed. → Lesson 03-03: Methods, which will let you write readValidText("Employee: ", 3) once and call it four times.
6. There are no automated tests. Every change forces you to repeat the test session by hand, typing eighteen inputs. It is slow and you end up not doing it. → Module 11: JUnit, and the quality techniques of module 12.
7. It is single-user, local and interface-less. One librarian, one console, one computer. → Module 9 (Networking) and module 12 (Web Application).
None of these limitations is a fault in the program: they are the exact frontiers of what can be built with control flow and scalar variables. And every remaining module of the course is designed to knock one of them down.
Common Mistakes and Tips
1. Declaring the statistics inside the menu loop. They reset on every operation and the summary always shows the last one's data. It is the bug you debugged in lesson 02-05, and in this program it is especially easy to commit.
2. Putting the exit break inside the switch. The menu never closes. Remember that with the arrow syntax case "0" -> { … break; } compiles and does exactly what you do not want.
3. Reading the option with nextInt(). It reintroduces the pending line-break problem you solved in module 1 and, on top of that, breaks the program on any text. The course convention is firm: always nextLine() and an explicit conversion.
4. Validating after converting. If you convert to a number before checking that the string is numeric, the check comes too late. The order is: read → check → convert → use.
5. Forgetting trim(). A trailing space turns "1 " into an unrecognised option and the user will think the program is broken.
6. Updating the maximum only halfway. If you update highestDelay but not highestDelayEmployee, the summary will lie. Keep the three assignments together in the same if and at the same indentation, so that a missing one jumps out.
7. Closing the Scanner inside the loop. A scanner.close() in the exit branch, before the break, works; inside another branch, it leaves the program with no input for the next pass and everything fails from there on. Close it once only, at the end of main.
8. Tip: test each iteration before writing the next one. That is the reason for building in five steps. If the menu does not close, fix it before adding the validation; otherwise you will have two intertwined bugs and no idea which is which.
9. Tip: extract the magic numbers into constants. WIDTH, MAX_DAYS and the four business rules are right at the top, with names. Changing the rate means touching one line, not hunting for 0.25 across the whole file.
10. Tip: save this version in Git before touching it. git add and git commit -m "BiblioTech 2.0: interactive menu" give you a safe point of return for doing the exercises without fear.
Exercises
Exercise 1: a new option "4. View the fine table"
Add a fourth option to the menu that shows BiblioTech's fine scale without asking for any data: a table with the days late in steps of five up to 90, showing the uncapped fine, the applied fine and the severity (MINOR, SEVERE or MAXIMUM). It must be generated with a for loop and use the existing constants, with no hand-written numbers.
Requirements:
- The option must appear in the menu and in the
defaultmessage. - The table must be aligned with
printf. - Mark with a
<--the first row in which the cap is reached, and only that one (hint: a boolean flag).
Exercise 2: counters for invalid options and session activity
Add two more statistics to the summary and the closing:
invalidOptions: how many times the user has entered something that fell into thedefault.totalOperations: the sum of returns, loans and summary lookups (that is, every valid option other than exit).
Show both in option 3 and in the closing. Also add a line computing the user's success rate: totalOperations * 100.0 / (totalOperations + invalidOptions), guarding against division by zero when nothing has been done yet.
Exercise 3: rejecting duplicate returns within the same session
With what you have (scalar variables, no collections), implement a minimal check: store the ISBN of the last registered return in a String lastIsbn variable, initialised to "". If the user tries to register a return with the same ISBN twice in a row, show a warning and ask whether they want to continue anyway (yes/no, validated in a loop and case-insensitive). If they answer no, the operation is discarded and it goes back to the menu without touching any statistic.
When you are done, answer in writing: why does this check only detect consecutive duplicates, and what would you need to detect any duplicate in the session?
Solutions
Solution 1
The line is added to the menu, the default message is extended and the new branch is introduced in the switch:
// ---------- OPTION 4: FINE TABLE ----------
case "4" -> {
System.out.println();
System.out.println(" " + "=".repeat(WIDTH - 4));
System.out.println(" BIBLIOTECH FINE SCALE");
System.out.println(" " + "=".repeat(WIDTH - 4));
System.out.printf(" %6s %12s %12s %s%n",
"DAYS", "UNCAPPED", "APPLIED", "SEVERITY");
boolean capAlreadyMarked = false; // flag: only the first row gets marked
for (int days = 5; days <= 90; days += 5) {
double uncapped = days * DAILY_RATE;
double applied = uncapped;
String severity;
String marker = "";
if (uncapped >= MAX_FINE) {
applied = MAX_FINE;
severity = "MAXIMUM";
if (!capAlreadyMarked) {
marker = " <--"; // only the FIRST time
capAlreadyMarked = true;
}
} else if (days <= MINOR_THRESHOLD) {
severity = "MINOR";
} else {
severity = "SEVERE";
}
System.out.printf(" %6d %12.2f %12.2f %-8s%s%n",
days, uncapped, applied, severity, marker);
}
System.out.println(" " + "=".repeat(WIDTH - 4));
}And the updated default:
Output (fragment):
BIBLIOTECH FINE SCALE
================================================
DAYS UNCAPPED APPLIED SEVERITY
5 1.25 1.25 MINOR
10 2.50 2.50 SEVERE
15 3.75 3.75 SEVERE
...
75 18.75 18.75 SEVERE
80 20.00 20.00 MAXIMUM <--
85 21.25 20.00 MAXIMUM
90 22.50 20.00 MAXIMUMKey points: the capAlreadyMarked flag is the lesson 02-02 pattern (once raised it is never lowered again), and marker is initialised to an empty string so that the printf works the same on every row without needing two different printfs. Notice too that the 5-day row is MINOR and the 10-day one is already SEVERE: the threshold is at 7, between the two.
Solution 2
New variables, declared alongside the rest of the statistics, outside the loop:
Increments in each valid branch and in the default:
case "1" -> {
totalOperations++;
// ... the rest of the branch unchanged ...
}
case "2" -> {
totalOperations++;
// ...
}
case "3" -> {
totalOperations++;
// ...
}
default -> {
invalidOptions++;
System.out.println(" Option not recognised. Use 0, 1, 2 or 3.");
}Block added to option 3's summary:
// Success rate, with a guard against division by zero.
int attempts = totalOperations + invalidOptions;
double successRate;
if (attempts == 0) {
successRate = 100.0; // nothing done yet: there are no errors
} else {
successRate = totalOperations * 100.0 / attempts;
}
System.out.printf(" %-26s %d%n", "Valid operations:", totalOperations);
System.out.printf(" %-26s %d%n", "Invalid options:", invalidOptions);
System.out.printf(" %-26s %.1f %%%n", "Success rate:", successRate);And in the closing:
System.out.printf(" %d valid operations and %d invalid options.%n",
totalOperations, invalidOptions);An example with the test session from section 9 (two returns, one loan, one wrong 9 and one summary lookup):
Two details to reason about. First, totalOperations++ goes at the start of each branch: if you put it at the end and the user abandoned halfway through a form, it would not count. Second, the multiplication totalOperations * 100.0 uses 100.0 and not 100: being a double, it promotes the whole expression and avoids the integer division that in module 1 turned 5/2 into 2. With 100 you would always get 0 % or 100 %.
Solution 3
A new variable alongside the statistics:
And, inside the case "1" branch, right after validating the ISBN and before asking for the days:
// --- Consecutive duplicate check ---
boolean proceed = true;
if (isbn.equals(lastIsbn)) {
System.out.println(" WARNING: this ISBN is the same as the previous return's.");
String answer;
do {
System.out.print(" Register it anyway? (yes/no): ");
answer = scanner.nextLine().trim();
if (!answer.equalsIgnoreCase("yes") && !answer.equalsIgnoreCase("no")) {
System.out.println(" Answer 'yes' or 'no'.");
}
} while (!answer.equalsIgnoreCase("yes") && !answer.equalsIgnoreCase("no"));
proceed = answer.equalsIgnoreCase("yes");
}
if (!proceed) {
System.out.println(" Return discarded. No statistic modified.");
} else {
// ... ALL the rest of the branch goes here: ask for the days, calculate,
// update the statistics and print the receipt ...
lastIsbn = isbn; // remembered for the next check
}A sample session:
Option: 1
Employee: Marta Ruiz
Book title: Effective Java
ISBN (978-XXXXXXXXXX): 978-0000000001
WARNING: this ISBN is the same as the previous return's.
Register it anyway? (yes/no): maybe
Answer 'yes' or 'no'.
Register it anyway? (yes/no): no
Return discarded. No statistic modified.Keys to the solution:
- The check is done after validating the ISBN (you need the clean value) and before asking for the days (so as not to bother the user with more questions if the operation is going to be discarded).
- The comparison is
isbn.equals(lastIsbn), never==: that is the rule from lesson 02-01. And sincelastIsbnis initialised to""and not tonull, the call is safe from the very first pass. - The confirmation uses
equalsIgnoreCasein a validation loop, soYES,Yesandyesare all equally valid, and anything else asks again. lastIsbn = isbn;is assigned only if the return is actually registered, inside theelse. If you put it outside, a discarded operation would contaminate the next check.
Answer to the final question. The check only detects consecutive duplicates because lastIsbn is a single scalar variable: it can only remember one value, the most recent, and every new return overwrites it. If the librarian registers 978-0000000001, then 978-0000000002 and then 978-0000000001 again, the third record will pass without a warning, because the first one has already been forgotten.
To detect any duplicate in the session you would need to remember all the registered ISBNs, not just the last one. That requires a structure capable of storing an indeterminate number of values and answering quickly the question "is this ISBN already in there?". That structure exists and is called HashSet (lesson 05-06), and its version with associated data is HashMap (lesson 05-05). It is, word for word, shortcoming number 2 in the list of limitations: the program can only remember one element at a time.
Conclusion
You have built BiblioTechApp 2.0, a complete and working console application. It shows a menu in a while loop and dispatches it with an arrow switch; it validates every input with do-while loops that insist until they get a correct value, including a character-by-character check that stops non-numeric text from bringing the program down; it applies Nexus Software's business rules with the module's decision table —delay clamped to zero, capped fine, status and severity—; it accumulates eight session statistics with counters, accumulators and the maximum-with-associated-data pattern; it prints receipts and summaries aligned with printf; and it closes tidily with a boolean flag and a break placed exactly where it should be. None of this existed six lessons ago.
With this you close module 2. Your program now decides (if, else if, the ternary, correct String comparison), repeats (while, do-while, for, nested loops, counters, accumulators and flags), selects (switch, classic and modern, as a statement and as an expression), controls the flow inside loops (break, continue, labels) and, when something fails, you know how to look inside it with traces and the debugger instead of guessing.
And you know exactly what it lacks, because you have listed it precisely: the state is loose variables that should be objects, only one book fits at a time where there should be a list, unforeseen input can still break it, everything is lost on exit and main is two hundred lines long, crying out to be broken into methods.
In module 3, Object-Oriented Programming, the course's most important transformation begins: you will stop thinking in terms of variables and steps, and start thinking in terms of objects that know their data and know how to operate on it. You will create the Book, Employee and Loan classes; you will give them constructors so they are always born in a valid state and methods to encapsulate the business rules that today are scattered around main; you will discover inheritance and polymorphism, encapsulation and abstraction; and you will learn to override toString, equals and hashCode so that your objects print and compare properly. By the end of that module, those three awkward variables —highestDelay, highestDelayEmployee and highestDelayBook— will be a single coherent object, and BiblioTech will start to look like a real system.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
