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

  1. The complete specification
  2. Iteration 1: the menu skeleton
  3. Iteration 2: validating every input
  4. Iteration 3: the return logic
  5. Iteration 4: the session statistics
  6. Iteration 5: polishing the output
  7. The complete final code, commented
  8. Compiling and running
  9. A complete test session
  10. Limitations: what it cannot do and which module will solve it
  11. Common Mistakes and Tips
  12. Exercises

  1. 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:

  1. Show a header with the company, the application name and the version.
  2. Show a menu in a loop with four options and not finish until the user chooses to exit:
1. Register a return
2. Simulate a loan
3. View session summary
0. Exit
  1. 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.
  2. 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.
  3. Option 3 (session summary): show the accumulated figures for everything done since the program started.
  4. Option 0 (exit): print a closing message with the total collected and finish.
  5. 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()"]

  1. 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.

  1. 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.

  1. 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

  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;
}

  1. 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 %-18s so that the labels line up in a column.
  • %.2f for every amount. A receipt saying 3.0 EUR is not acceptable.
  • Separators with "=".repeat(WIDTH) and a WIDTH constant, 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.

  1. 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();
    }
}

  1. 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.BiblioTechApp

From 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.

  1. 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

  1. 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 default message.
  • 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:

  1. invalidOptions: how many times the user has entered something that fell into the default.
  2. 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:

System.out.println("  4. View the fine table");
// ---------- 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:

default -> System.out.println("  Option not recognised. Use 0, 1, 2, 3 or 4.");

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  MAXIMUM

Key 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:

int invalidOptions  = 0;
int totalOperations = 0;

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):

    Valid operations:          4
    Invalid options:           1
    Success rate:              80.0 %

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:

String lastIsbn = "";

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:

  1. 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).
  2. The comparison is isbn.equals(lastIsbn), never ==: that is the rule from lesson 02-01. And since lastIsbn is initialised to "" and not to null, the call is safe from the very first pass.
  3. The confirmation uses equalsIgnoreCase in a validation loop, so YES, Yes and yes are all equally valid, and anything else asks again.
  4. lastIsbn = isbn; is assigned only if the return is actually registered, inside the else. 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

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved