Agile System SoftwareLearn

Workshop · Java · Intermediate

JV100 Java I

Java as a working language: records, enums and sealed interfaces that make illegal states impossible; streams that read like the sentence you meant; exceptions and try-with-resources with intent; JUnit 5 with parameterised tests and a JaCoCo gate the build enforces; files, CSV, JSON and java.time done properly; packages, Javadoc and a runnable jar. Forty-eight steps across eight sessions, each ending in a check on your own project, building Stockroom, a tested inventory library with a command-line front end.

Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick JV100 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.

  • LevelIntermediate
  • Durationself-paced ~30–35 h
  • Size8 sessions · 48 mini steps · ~393 min of lessons
  • EnvironmentA VS Code seat in your browser with JDK 25, Maven 3.9 offline, JUnit 5 and JaCoCo; the Stockroom project builds, tests and measures coverage with mvn -o
  • PrerequisitesJV050, or the beginner course of any other language in this catalogue (PY050, DN050, RS050, NG050). Comfortable with a terminal.
    Courses: JV050 or PY050 or DN050 or RS050 or NG050
  • CertificateAgile System Software Certificate — Java I (JV100)

Who it is for

People who can already write a small program in Java or in another language — variables, loops, functions, a list — and want to write Java the way a team expects it: typed models, streams, exceptions with intent, tests that gate the build, packages that make sense. Bridging from Python, C#, Rust or TypeScript is expected; session 1 re-teaches the Java syntax you need in Java terms.

Bridge. Coming from PY050, DN050, RS050, NG050: the concepts you already proved there are credited here (9 of them), and the first session re-teaches the syntax.

Where it leads. JV300 Java, advanced: building a service

What you will be able to do

  • Drive Maven offline through the lifecycle, read the surefire and JaCoCo reports, and use the debugger instead of println
  • Model a domain with records, enums, sealed interfaces, Optional and BigDecimal so that invalid states do not compile
  • Use generics, Iterable and streams (map, filter, sorted, collect, groupingBy) to express queries without index loops
  • Design exceptions: checked versus unchecked, your own hierarchy, fail fast at the boundary, resources closed by try-with-resources, events logged
  • Write JUnit 5 tests that are parameterised, use temporary directories, are written first, and meet a coverage floor
  • Read and write files, CSV with quoting, JSON with a minimal hand-written codec, and dates with java.time
  • Lay out packages by responsibility, control visibility, write Javadoc and a README, and ship a runnable jar
  • Build a tested library with a thin CLI and an exit-code contract, and review your own code against a checklist

Syllabus

Every session ends in a lab the machine checks on your own code. Open a session's mini steps to read each lesson, demo and hands-on task; the badge shows how far the engine can verify it: checked · T3 a check runs over your files, proven · T4 the grader also tries inputs you did not see, tutored · T0 done on your word. Each step has its own link (for example #s1-1).

Session 1 · Idiomatic Java and the tooling

Six steps that make the Maven project a tool you understand rather than a ritual. The lifecycle phases and what -o, -q and the goal names mean. The seat starter's pom read line by line: pinned plugin versions, release 21, JUnit 5, JaCoCo, exec. The debugger instead of println: breakpoints, step over, the variables view. JaCoCo's report and what a coverage floor does to the build. The conventions a team expects: naming, formatting, one class per file, final where it helps. Live in the seat: a build run phase by phase, a breakpoint caught in a loop, a coverage report opened in the browser, and a project skeleton that builds, tests and runs.

Lab Create java/jv100/stockroom from the seat starter: package stockroom, an App that prints "Stockroom 0.1", one passing test, JaCoCo reporting on mvn -o verify, and a README with the three commands. Session 1 checkpoint.

6 mini steps · ~49 min
  1. 1.1The Maven lifecycle, offline8 minchecked · T3

    Predict

    mvn -o -q package runs which phases?

    Lesson

    Maven's default lifecycle is a ladder: validate, compile, test, package, verify, install. Naming a phase runs all the rungs below it. -o stays offline (the seat has no network), -q hides the noise; plugin goals like exec:java run on their own.

    Demo

    The tutor runs mvn -o compile, then test, then package in java/jv100/demo/lifecycle, pointing at target/ growing each time, then mvn -o -q exec:java, and finally mvn -o clean to show target/ disappear.

    On screen

    Terminal → cd java/jv100/demo/lifecycle → mvn -o compile → ls target → mvn -o test → mvn -o -q package → ls target/*.jar → mvn -o clean.

    Hands-on

    Copy the seat starter to java/jv100/lifecycle and run mvn -o compile, mvn -o test and mvn -o -q package in turn. Write what appeared in target/ after each phase into java/jv100/lifecycle/NOTES.md as three bullet lines.

    Explain

    Why does package run the tests?

    Check

    The copied project packages offline and NOTES.md has three phase notes — checked (T3): seat_build seat_file

    #s1-1

  2. 1.2Reading the pom8 minchecked · T3

    Predict

    The starter pins maven-surefire-plugin 3.5.2. What happens offline if you delete the version?

    Lesson

    A pom has coordinates (groupId, artifactId, version), properties (maven.compiler.release 21), dependencies (junit-jupiter, test scope) and plugins with pinned versions. The seat's repository holds exactly those versions: copy the pom and change coordinates only.

    Demo

    The tutor walks the starter pom top to bottom in the editor, folds each block, renames groupId and artifactId for the demo, changes exec.mainClass, and rebuilds; then shows the dependency tree with mvn -o dependency:tree.

    Hands-on

    In java/jv100/lifecycle edit the pom: groupId com.example.jv100, artifactId lifecycle, exec.mainClass to your main class, and add a <description>. Rebuild offline and run mvn -o dependency:tree, saving its output to target/tree.txt.

    Explain

    What is the difference between a dependency and a plugin?

    Check

    The pom carries new coordinates and still builds offline — checked (T3): seat_file seat_build

    #s1-2

  3. 1.3The debugger instead of println9 minchecked · T3

    Predict

    A breakpoint sits inside a loop that runs 100 times. How often does the debugger stop?

    Lesson

    Set a breakpoint in the gutter, press Run and Debug, and the program stops there with every variable visible. Step over runs one line; step into enters a method; a conditional breakpoint stops only when an expression is true. It replaces a dozen printlns.

    Demo

    The tutor opens java/jv100/demo/debug/Sum.java, which sums the wrong total, sets a breakpoint in the loop, steps three times watching the variables view, adds the condition i == 5, and finds the off-by-one.

    Hands-on

    Copy java/jv100/samples/s1/Averager.java into java/jv100/lifecycle/src/main/java and run it: it prints the wrong average. Use a breakpoint in the loop, not println, to find the bug; fix it so it prints "Average: 4.5"; write the bug in one line at the top of the file as a comment.

    Explain

    What did you see in the variables view that println would have hidden?

    Check

    Averager.java is fixed, prints Average: 4.5 and names the bug in a comment — checked (T3): seat_file seat_run

    #s1-3

  4. 1.4JaCoCo and the coverage floor8 minchecked · T3

    Predict

    coverage.line.minimum is 0.50 and your tests cover 40% of lines. mvn -o verify does what?

    Lesson

    JaCoCo instruments the tests (prepare-agent), writes target/site/jacoco/index.html and jacoco.csv after test, and in verify checks the line ratio against coverage.line.minimum. Coverage finds untested paths; it is a floor, not a score.

    Demo

    The tutor runs mvn -o verify in the demo, opens jacoco/index.html in the browser preview, points at a red method, raises the minimum to 0.90 and shows the build fail with the ratio in the message, then sets it back.

    Hands-on

    In java/jv100/lifecycle run mvn -o verify, open target/site/jacoco/index.html, and write the line coverage percentage into NOTES.md as "coverage: NN%". Then set coverage.line.minimum to 0.95 in the pom and confirm the build fails; set it back to 0.50.

    Explain

    Why is 100% coverage not the goal?

    Check

    The coverage report exists and NOTES.md records the percentage — checked (T3): seat_file seat_file seat_file

    #s1-4

  5. 1.5Conventions a team expects7 minchecked · T3

    Predict

    A class named inventoryService in file Inventory.java. What does the compiler say?

    Lesson

    Conventions: UpperCamelCase types, lowerCamelCase members, UPPER_SNAKE constants, one public class per file, packages in reverse-domain lower case, final for values that do not change. Let Format Document settle whitespace; argue about names, not braces.

    Demo

    The tutor opens java/jv100/samples/s1/Messy.java — wrong names, mixed indentation, a magic number — and fixes it live with Format Document, a rename refactoring and a named constant, explaining each rule as it applies.

    Hands-on

    Copy java/jv100/samples/s1/Messy.java to java/jv100/lifecycle/src/main/java/Tidy.java and fix it: class Tidy, a static final int MAX_ITEMS constant instead of the literal 50, lowerCamelCase method and variable names, Format Document applied. It must still print "Items: 50".

    Explain

    Why name a constant instead of writing 50 twice?

    Check

    Tidy.java follows the conventions and prints Items: 50 — checked (T3): seat_file craft seat_run

    #s1-5

  6. 1.6Lab: the Stockroom skeleton9 minchecked · T3

    Predict

    Which command builds the jar, runs the tests and enforces the coverage floor in one go?

    Lesson

    The project you will grow for seven sessions: package stockroom, an App that prints the version, a Version class with one tested method, JaCoCo on verify, exec configured, and a README that says how to build, test and run. Small, green, understood.

    Demo

    The tutor creates java/jv100/demo/stockroom from the starter, renames everything, writes Version.current() and its test, runs mvn -o verify and exec:java, and drafts the README's three commands.

    Hands-on

    Create java/jv100/stockroom from the starter: package stockroom; stockroom.App prints "Stockroom 0.1" using stockroom.Version.current(); VersionTest asserts current() equals "0.1"; coverage floor 0.50; README.md with build, test and run commands. mvn -o verify must pass.

    Explain

    Name the three things the README must let a stranger do.

    Check

    Stockroom builds, tests, measures coverage and runs — checked (T3): seat_file seat_file seat_build seat_test seat_run

    #s1-6

Session 2 · Types and data modelling

Six steps on making illegal states impossible. Records for immutable data with validation in a compact constructor. Enums with fields and methods instead of string codes. Sealed interfaces and pattern matching for switch, so the compiler tells you when you forgot a case. Optional for a value that may be absent, instead of null and hope. Money as a BigDecimal record with a fixed scale and arithmetic that cannot mix currencies. Live in the seat: a record that rejects a negative quantity at construction, an enum that carries its own reorder threshold, a switch that fails to compile until every event is handled, a null pointer replaced by Optional.map.

Lab Model the Stockroom domain: Sku, Quantity, Money, Item, Category enum and a sealed StockEvent (Received, Issued, Adjusted) with a tested apply method that uses pattern matching. Session 2 checkpoint.

6 mini steps · ~50 min
  1. 2.1Records with validation8 minchecked · T3

    Predict

    record Quantity(int units) { Quantity { if (units < 0) throw new IllegalArgumentException(); } } new Quantity(-1) does?

    Lesson

    A record is immutable data with accessors, equals and toString written for you. A compact constructor validates once: after it, every Quantity in the program is non-negative, and no method needs to check again.

    Demo

    The tutor writes Quantity and Sku records in the demo project with compact constructors, shows new Quantity(-1) throwing in a test, and adds a plus method returning a new Quantity.

    Hands-on

    In java/jv100/stockroom add stockroom.model.Sku(String code) (non-blank, upper-case, 3–12 chars) and stockroom.model.Quantity(int units) (non-negative) with compact constructors and Quantity.plus/minus; tests sku_rejects_blank, quantity_rejects_negative and quantity_minus_cannot_go_negative.

    Explain

    Why validate in the constructor rather than where the value is used?

    Check

    Sku and Quantity validate in compact constructors; three tests pass — checked (T3): seat_file seat_test

    #s2-1

  2. 2.2Enums with behaviour8 minchecked · T3

    Predict

    enum Category { TOOLS(5), PARTS(20); } Category.valueOf("tools") returns?

    Lesson

    An enum is a type with a fixed set of values, and each value can carry fields and methods: Category.TOOLS has a reorder threshold. Switch over enums is exhaustive. Parse external text with your own method, never bare valueOf.

    Demo

    The tutor writes Category with a reorderAt field and a static Optional<Category> fromCode(String) in the demo, then replaces a string comparison in an if with a switch over the enum.

    Hands-on

    Add stockroom.model.Category enum TOOLS(5), PARTS(20), CONSUMABLES(50) with int reorderAt() and static Optional<Category> fromCode(String) that trims and upper-cases. Tests category_from_code_is_lenient and category_unknown_is_empty.

    Explain

    What does an enum give you that a String code does not?

    Check

    Category carries reorderAt and a lenient fromCode — checked (T3): seat_file seat_test

    #s2-2

  3. 2.3Sealed interfaces and pattern matching9 minchecked · T3

    Predict

    sealed interface StockEvent permits Received, Issued, Adjusted. A switch handles Received and Issued only. Compiles?

    Lesson

    A sealed interface lists exactly which records implement it. switch (event) { case Received r -> …; case Issued i -> …; } is checked for exhaustiveness: add a fourth event and every switch that forgot it stops compiling. That is the point.

    Demo

    The tutor declares sealed StockEvent with three records in the demo, writes a switch that describes each, removes one case to show the compile error, and adds a fourth event to watch the switch break on purpose.

    Hands-on

    Add stockroom.model.StockEvent, a sealed interface permitting records Received(Sku, Quantity), Issued(Sku, Quantity) and Adjusted(Sku, int delta, String reason), and a static String describe(StockEvent) using a switch with patterns and no default. Tests describe_received and describe_adjusted.

    Explain

    Why no default branch in the switch?

    Check

    StockEvent is sealed and describe switches exhaustively — checked (T3): seat_file seat_test

    #s2-3

  4. 2.4Optional: a value that may be absent8 minchecked · T3

    Predict

    Optional<Item> find(Sku) returns empty. find(sku).map(Item::name).orElse("?") gives?

    Lesson

    Return Optional<T> when a lookup can legitimately find nothing; callers then map, filter, orElse or orElseThrow instead of checking for null. Never return null for "not found", and never call get() without a check.

    Demo

    The tutor replaces a null-returning findBySku in the demo's Inventory with Optional, fixes a NullPointerException in the caller with map/orElse, and shows orElseThrow with a custom message.

    Hands-on

    Add stockroom.model.Item(Sku sku, String name, Category category, Quantity onHand) and stockroom.Inventory holding a Map<Sku, Item> with Optional<Item> find(Sku) and void put(Item). Tests find_present_maps_name and find_absent_is_empty.

    Explain

    What does Optional communicate that a nullable return does not?

    Check

    Inventory.find returns Optional and the tests pass — checked (T3): seat_file seat_test

    #s2-4

  5. 2.5Money as a type8 minchecked · T3

    Predict

    Money.of("19.99").times(3).amount() with scale fixed at 2 gives?

    Lesson

    record Money(BigDecimal amount) with a compact constructor that fixes scale 2 and HALF_UP, static of(String), and plus/times/compareTo that return Money. One type, one rounding rule, and no double anywhere near a price.

    Demo

    The tutor writes Money in the demo, multiplies 19.99 by 3 and adds 0.10 a hundred times, prints the scale after each, then tries Money.of("1.005") to show the constructor rounding it to 1.01.

    Hands-on

    Add stockroom.model.Money(BigDecimal amount) with scale 2 HALF_UP in the compact constructor, static Money of(String), Money plus(Money), Money times(int) and Money times(BigDecimal). Tests money_keeps_scale_2, money_times_3 and money_rounds_half_up.

    Explain

    Why fix the scale in the constructor rather than when printing?

    Check

    Money fixes scale 2 and rounds HALF_UP; three tests pass — checked (T3): seat_file craft seat_test

    #s2-5

  6. 2.6Lab: the Stockroom domain model9 minchecked · T3

    Predict

    apply(item, new Issued(sku, Quantity(10))) on an item with 4 on hand should?

    Lesson

    Inventory.apply(StockEvent) switches over the sealed events: Received adds, Issued subtracts or throws when insufficient, Adjusted applies a signed delta with a reason. Items carry Money unitCost. All of it pure except the map update, and all of it tested.

    Demo

    The tutor writes apply in the demo with a pattern switch, tests received/issued/insufficient/adjusted, and shows the compiler catching a missing case when a fourth event is added and removed.

    Hands-on

    Extend Item with Money unitCost. In Inventory write Item apply(StockEvent e) that updates and returns the stored item per the rules above (unknown Sku → NoSuchElementException). Tests apply_received_adds, apply_issued_subtracts, apply_issued_insufficient_throws, apply_adjusted_negative_delta and apply_unknown_sku_throws. mvn -o verify green.

    Explain

    Which rule does the type system enforce for you, and which did you have to write?

    Check

    Inventory.apply handles every event and five tests pass — checked (T3): seat_file seat_build seat_test

    #s2-6

Session 3 · Collections and streams

Six steps on expressing queries. Generics: List<T>, a generic Pair, bounded types and why the compiler stops you putting an Item in a List<Money>. Iterable and for-each versus the iterator when you remove while walking. Streams: map, filter, sorted, collect, and the shape of a pipeline that reads like the sentence you meant. Comparator composition: comparing, thenComparing, reversed. Collectors: groupingBy, summing, joining, toMap. Live in the seat: a ConcurrentModificationException and its iterator fix, an index loop rewritten as three stream lines, items grouped by category with totals, a top-three by value.

Lab Inventory queries as streams: lowStock(), valueByCategory(), topByValue(n) and search(text), each one a method returning collections (never printing), with tests over a fixed ten-item fixture. Session 3 checkpoint.

6 mini steps · ~50 min
  1. 3.1Generics8 minchecked · T3

    Predict

    List<Money> prices = new ArrayList<>(); prices.add(new Item(…)); What happens?

    Lesson

    Generics let a class or method work for any type while the compiler keeps the type exact: List<Item>, Map<Sku, Item>, <T extends Comparable<T>> T max(List<T>). Write generic code once; get a compile error instead of a ClassCastException.

    Demo

    The tutor writes record Pair<A, B>(A first, B second) and a generic static <T extends Comparable<T>> T largest(List<T>) in the demo, calls largest on Money and on Integer lists, and shows the compile error for a mixed list.

    Hands-on

    Add stockroom.util.Pair<A, B> as a record and stockroom.util.Lists with static <T extends Comparable<T>> Optional<T> largest(List<T>) and static <T> List<Pair<Integer, T>> indexed(List<T>). Tests largest_of_money, largest_of_empty_is_empty and indexed_pairs.

    Explain

    What does <T extends Comparable<T>> buy you?

    Check

    Pair and Lists are generic and tested — checked (T3): seat_file seat_test

    #s3-1

  2. 3.2Iterable, for-each and the iterator7 minchecked · T3

    Predict

    for (Item i : items) { if (i.onHand().units() == 0) items.remove(i); } What happens on the first removal?

    Lesson

    for-each works on anything Iterable, including your own classes. It hides an Iterator; modify the collection while walking and it throws. Use Iterator.remove, removeIf, or build a new list. Make Inventory Iterable<Item> so callers can walk it.

    Demo

    The tutor triggers ConcurrentModificationException in the demo, fixes it with removeIf, then implements Iterable<Item> on Inventory and walks it with for-each.

    Hands-on

    Make Inventory implement Iterable<Item> (sorted by Sku code) and add int removeEmpty() using removeIf on the backing map's values. Tests iterable_in_sku_order and removeEmpty_counts_and_removes.

    Explain

    Why does removeIf not throw where the loop did?

    Check

    Inventory is Iterable and removeEmpty uses removeIf — checked (T3): seat_file seat_test

    #s3-2

  3. 3.3Streams: map, filter, collect9 minchecked · T3

    Predict

    items.stream().filter(i -> i.onHand().units() == 0).map(Item::name).toList() returns?

    Lesson

    A stream is a pipeline: a source, zero or more lazy steps (filter, map, sorted, distinct, limit), and one terminal operation (toList, count, sum, collect, forEach). Nothing runs until the terminal call. Write it so it reads as a sentence.

    Demo

    The tutor rewrites an index loop in the demo that collected names of empty items as a three-line stream, adds sorted and limit, and shows with peek that nothing happens before toList.

    Hands-on

    In Inventory add List<Item> lowStock() (onHand below the category's reorderAt, sorted by Sku) and List<String> names() using streams, no index loops. Tests lowStock_uses_category_threshold and names_sorted.

    Explain

    What makes a stream lazy, and why does it matter?

    Check

    lowStock and names are stream pipelines with passing tests — checked (T3): seat_file craft seat_test

    #s3-3

  4. 3.4Comparators8 minchecked · T3

    Predict

    Comparator.comparing(Item::category).thenComparing(Item::name).reversed() orders how?

    Lesson

    Comparator.comparing(keyExtractor) builds an order from a field; thenComparing adds tie-breakers; reversed flips; nullsFirst handles gaps. Put the comparator in a named constant so the order has a name and a test.

    Demo

    The tutor sorts the demo items by category then name, then by value descending with a composed comparator, and extracts it as public static final Comparator<Item> BY_VALUE_DESC.

    Hands-on

    Add to stockroom.model.Item two constants: Comparator<Item> BY_CATEGORY_THEN_NAME and BY_VALUE_DESC (value = unitCost times onHand). Add Inventory.sorted(Comparator<Item>) returning a List. Tests sorted_by_category_then_name and sorted_by_value_desc.

    Explain

    Why name a comparator as a constant?

    Check

    Named comparators exist and the sort tests pass — checked (T3): seat_file seat_test

    #s3-4

  5. 3.5Collectors: groupingBy and friends9 minchecked · T3

    Predict

    items.stream().collect(groupingBy(Item::category, counting())) gives a Map of?

    Lesson

    Collectors turn a stream into a structure: toList, toSet, joining, toMap(key, value, merge), groupingBy(key, downstream) with counting, summing or reducing. For money, reduce with Money::plus from Money.ZERO; there is no summingBigDecimal.

    Demo

    The tutor groups the demo items by category with counting, then with a reducing collector over Money, then joins names with ", ", and shows toMap throwing on a duplicate key without a merge function.

    Hands-on

    Add Inventory.valueByCategory() returning Map<Category, Money> (TreeMap, reduce with Money::plus) and Inventory.countByCategory() returning Map<Category, Long>. Tests valueByCategory_sums and countByCategory_counts.

    Explain

    Why reduce with Money::plus instead of summingDouble?

    Check

    valueByCategory and countByCategory use collectors — checked (T3): seat_file seat_test

    #s3-5

  6. 3.6Lab: inventory queries9 minchecked · T3

    Predict

    topByValue(3) on a two-item inventory returns?

    Lesson

    Four queries, four pipelines, one fixture: lowStock, valueByCategory, topByValue(n) (BY_VALUE_DESC, limit) and search(text) (case-insensitive on name or Sku code). Each returns a collection and prints nothing; the CLI will print later.

    Demo

    The tutor builds a ten-item fixture in the demo tests as a static method, writes the four queries and their tests, and shows one test failing when limit is forgotten.

    Hands-on

    In java/jv100/stockroom add Inventory.topByValue(int n) and Inventory.search(String text). Create a stockroom.Fixtures test helper with tenItems(). Tests topByValue_limits_and_orders, topByValue_more_than_size, search_is_case_insensitive and search_matches_sku_code. mvn -o verify green.

    Explain

    Why do the queries return collections instead of printing?

    Check

    Four query methods, four stream pipelines, tests green — checked (T3): seat_file seat_build seat_test

    #s3-6

Session 4 · Errors and resources

Six steps on failing well. Checked versus unchecked exceptions and the rule of thumb: checked for conditions the caller can recover from, unchecked for programming errors. Your own hierarchy: StockroomException with InsufficientStock and UnknownSku, each carrying the data a message needs. Fail fast at the boundary with requireNonNull and argument checks. try-with-resources for anything Closeable, including your own. java.util.logging with levels and a one-line formatter. Live in the seat: a stack trace read top-down to the first frame in your code, a catch that swallowed an error and the bug it hid, a reader left open and the fix, log lines appearing at INFO and vanishing at WARNING.

Lab The robust loader: Inventory.load(Path) reads items.csv, rejects bad rows with logged reasons, closes the file whatever happens, and reports a LoadResult with counts; a missing file is a checked StockroomException the CLI turns into exit 2. Session 4 checkpoint.

6 mini steps · ~48 min
  1. 4.1Checked versus unchecked8 minchecked · T3

    Predict

    A method calls Files.readAllLines and neither catches IOException nor declares throws. Result?

    Lesson

    Checked exceptions (IOException) must be caught or declared; unchecked (RuntimeException: IllegalArgument, IllegalState, NullPointer) need not be. Use checked for recoverable, expected failures at boundaries; unchecked for bugs. Never catch Exception and continue silently.

    Demo

    The tutor writes a method that reads a file without handling in the demo, reads the compile error, declares throws, then shows a catch (Exception e) {} swallowing a NullPointerException and the confusing behaviour it causes.

    Hands-on

    Add stockroom.io.FileText with static List<String> lines(Path) that declares throws IOException, and a test lines_missing_file_throws_NoSuchFileException using assertThrows. In NOTES.md write one checked and one unchecked exception from the project and why each is which.

    Explain

    When is a checked exception the right choice?

    Check

    FileText.lines declares IOException and the assertThrows test passes — checked (T3): seat_file seat_file seat_test

    #s4-1

  2. 4.2Your own exception hierarchy8 minchecked · T3

    Predict

    catch (StockroomException e) is written above catch (InsufficientStock e). Compiles?

    Lesson

    A domain base exception (StockroomException, checked) with subclasses that carry data: InsufficientStock(sku, requested, available), UnknownSku(sku). Messages are built from the fields, callers can catch the family or one member, and tests assert on fields, not strings.

    Demo

    The tutor creates the hierarchy in the demo, replaces IllegalStateException in apply with InsufficientStock, shows the compiler ordering rule, and asserts on e.available() in a test.

    Hands-on

    Add stockroom.StockroomException (checked) with subclasses InsufficientStock(Sku, Quantity requested, Quantity available) and UnknownSku(Sku), each with accessors and a message built from the fields. Change Inventory.apply to throw them. Tests insufficient_carries_quantities and unknown_sku_carries_sku.

    Explain

    Why carry fields on the exception instead of only a message?

    Check

    The hierarchy exists, apply throws it, two tests pass — checked (T3): seat_file seat_file seat_test

    #s4-2

  3. 4.3try-with-resources, including your own8 minchecked · T3

    Predict

    try (A a = open(); B b = open()) { … } — in what order are a and b closed?

    Lesson

    Anything AutoCloseable can sit in try (…) and is closed in reverse order even if the block throws; a close() exception is suppressed onto the primary one. Implement AutoCloseable on your own classes that hold a file or a lock.

    Demo

    The tutor writes a Journal class in the demo that appends events to a file through a BufferedWriter, implements AutoCloseable, forgets to close it once (file empty), then uses try-with-resources and sees the lines.

    Hands-on

    Add stockroom.io.Journal implements AutoCloseable: opened on a Path, void record(StockEvent) appends one line, close() flushes and closes. Test journal_writes_and_closes using @TempDir and try-with-resources, then reading the file back.

    Explain

    What happens to an exception thrown by close() when the body also threw?

    Check

    Journal is AutoCloseable and its test writes then reads back — checked (T3): seat_file seat_test

    #s4-3

  4. 4.4Fail fast at the boundary7 minchecked · T3

    Predict

    put(null) is called on Inventory without a null check. When does the failure appear?

    Lesson

    Validate at the edge, once: Objects.requireNonNull(item, "item") in public methods, range checks with a clear message, size limits on text from files. A failure at the boundary points at the cause; a failure deep inside points at a symptom.

    Demo

    The tutor passes null into the demo Inventory.put, shows the trace landing in a stream pipeline three calls later, adds requireNonNull and shows the trace now naming put and the parameter.

    Hands-on

    Add Objects.requireNonNull guards to every public Inventory method taking a reference, and a 200-character cap on Item.name in its compact constructor. Tests put_null_fails_fast (message contains "item") and item_name_too_long_rejected.

    Explain

    Why is failing early better than failing late?

    Check

    requireNonNull guards and the name cap are in place and tested — checked (T3): seat_file seat_file seat_test

    #s4-4

  5. 4.5Logging with java.util.logging8 minchecked · T3

    Predict

    The logger level is WARNING and the code calls log.info("loaded"). What appears?

    Lesson

    java.util.logging is in the JDK: Logger.getLogger(Class.getName()), levels SEVERE/WARNING/INFO/FINE, a ConsoleHandler to stderr and a Formatter you control. Log events with their data (sku=…, line=…), not prose; never log secrets.

    Demo

    The tutor configures a one-line Formatter in the demo (level, logger, message), logs at INFO and FINE in the loader, flips the level in a small Log.setup(Level) helper and shows lines appear and vanish.

    Hands-on

    Add stockroom.Log with static void setup(Level) installing a ConsoleHandler and a one-line formatter "LEVEL logger message". Log at INFO in Inventory.put ("put sku=…") and FINE in apply. Test log_format_is_one_line by capturing a Handler.

    Explain

    Why log fields like sku=ABC rather than a sentence?

    Check

    Log.setup installs a one-line formatter; put logs at INFO — checked (T3): seat_file seat_file seat_test

    #s4-5

  6. 4.6Lab: the robust loader9 minchecked · T3

    Predict

    items.csv has a row with a negative quantity. The loader should?

    Lesson

    Inventory.load(Path) with try-with-resources over a BufferedReader; header skipped; each row through a parser that returns a result or a reason; rejects logged at WARNING with the line number; a record LoadResult(int loaded, int rejected); a missing file becomes a checked StockroomException.

    Demo

    The tutor builds load in the demo over java/jv100/samples/s4/items.csv (one bad row), shows the WARNING line and LoadResult(9, 1), then the missing-file path throwing StockroomException with the path in its message.

    Hands-on

    Write Inventory.load(Path) and record stockroom.LoadResult(int loaded, int rejected) as described. Tests load_counts_good_and_bad over samples/s4/items.csv (9 loaded, 1 rejected), load_missing_file_throws_StockroomException, and load_closes_reader (use a @TempDir file and delete it after load).

    Explain

    Which failures in load are reported, and which are thrown?

    Check

    load reports 9/1 on the sample, throws on a missing file, closes the reader — checked (T3): seat_file seat_build seat_test

    #s4-6

Session 5 · Testing

Six steps on tests that earn their keep. The anatomy of a JUnit 5 test and what a good name says. Parameterised tests with @CsvSource and @MethodSource so one body covers a table of cases. @TempDir for tests that touch files. Test first on a real feature: the reorder report. The JaCoCo floor raised to 80% and what the red lines tell you. Live in the seat: a test renamed until it reads as a sentence, six if-cases folded into one parameterised test, a temp file that cleans itself up, a red-green-refactor cycle, a coverage report with one untested branch found and covered.

Lab The tested core: Inventory and the model at 80% line coverage enforced by mvn -o verify, parameterised tests for the parser's rules, and a hidden test the grader runs against your public API. Session 5 checkpoint.

6 mini steps · ~49 min
  1. 5.1Anatomy of a good test7 minchecked · T3

    Predict

    A test named test1 fails. What do you know from the name?

    Lesson

    Arrange, act, assert, one behaviour per test, named as what_when_then. Prefer assertEquals(expected, actual) in that order, assertThrows for failures, assertAll for several facts about one result. @BeforeEach builds the fixture, not the test.

    Demo

    The tutor opens a smelly test in the demo (three behaviours, prints, no assertion message), splits it into three named tests with a @BeforeEach fixture and assertAll, and runs them.

    Hands-on

    Refactor stockroom's InventoryTest: a @BeforeEach that builds the ten-item fixture, every test named subject_condition_outcome, no System.out in tests, and assertAll where a result has several facts. All tests still green.

    Explain

    What are the three parts of a test name, and why does it matter?

    Check

    InventoryTest uses a @BeforeEach fixture, descriptive names and no printing — checked (T3): seat_file craft seat_test

    #s5-1

  2. 5.2Parameterised tests8 minchecked · T3

    Predict

    @ParameterizedTest @CsvSource({"tools,TOOLS", "Parts ,PARTS"}) runs the body how many times?

    Lesson

    @ParameterizedTest with @CsvSource (inline rows), @ValueSource (one column) or @MethodSource (a static stream of Arguments) runs one body over a table. Name the invocation with name = "{0} -> {1}" so the report reads well.

    Demo

    The tutor folds four Category.fromCode tests in the demo into one @CsvSource test with six rows including blanks and mixed case, then uses @MethodSource for Money rounding cases with Arguments.of.

    Hands-on

    Replace the Category.fromCode tests with one @ParameterizedTest @CsvSource of at least six rows (name = "{0} -> {1}"), and add a @MethodSource test money_rounds for at least four (input, expected) pairs. Green.

    Explain

    When should a table test replace separate tests?

    Check

    A CsvSource test with six rows and a MethodSource test exist and pass — checked (T3): seat_file seat_file seat_test

    #s5-2

  3. 5.3@TempDir and tests that touch files8 minchecked · T3

    Predict

    A test writes to data/test.csv in the project folder. What goes wrong?

    Lesson

    @TempDir Path dir injects a fresh temporary directory per test (or per class) and cleans it up. Write the fixture file inside it, run the code, assert on the result or on the file's contents. Never write into the project tree from a test.

    Demo

    The tutor rewrites a Journal test in the demo from a hard-coded path to @TempDir, shows the folder name in the debugger, and reads the file back with Files.readAllLines in the assertion.

    Hands-on

    Rewrite every file-touching test in stockroom (Journal, load, FileText) to use @TempDir; no test may reference a path under data/ or samples/ except samples/s4/items.csv read-only. Green.

    Explain

    Why must tests not share a file path?

    Check

    File tests use @TempDir and write nowhere else — checked (T3): seat_file craft seat_test

    #s5-3

  4. 5.4Test first: the reorder report9 minchecked · T3

    Predict

    You write reorder_lists_items_below_threshold before ReorderReport exists. First result of mvn test?

    Lesson

    Red: a failing test for one behaviour. Green: the smallest code that passes. Refactor: tidy with the tests as a net. Three small cycles beat one big one; commit each green.

    Demo

    The tutor builds ReorderReport in the demo in three cycles: items below threshold, suggested quantity to reach twice the threshold, sorted by shortfall, running tests after every change.

    Hands-on

    Test-first, add stockroom.report.ReorderReport with static List<Line> of(Inventory) where record Line(Sku, int onHand, int reorderAt, int suggested) and suggested = 2 × reorderAt − onHand, sorted by largest shortfall. Tests reorder_lists_items_below_threshold, reorder_suggests_to_twice_threshold, reorder_sorted_by_shortfall. Write each test before its code.

    Explain

    What did the red step tell you each time?

    Check

    ReorderReport exists with three passing tests — checked (T3): seat_file seat_test

    #s5-4

  5. 5.5The coverage floor at 80%8 minchecked · T3

    Predict

    JaCoCo shows a yellow diamond on an if line. What does yellow mean?

    Lesson

    Raise coverage.line.minimum to 0.80; run mvn -o verify; open the report; go to the red and yellow lines; ask what behaviour they represent; write that test. Exclude only generated or trivially unreachable code, and say why in the pom.

    Demo

    The tutor raises the floor in the demo, watches verify fail at 74%, opens the report, finds an untested UnknownSku branch, writes the test, and verify passes at 83%.

    Hands-on

    Set coverage.line.minimum to 0.80 in java/jv100/stockroom and make mvn -o verify pass by adding tests for the red and yellow lines you find. Record the final percentage in NOTES.md as "coverage: NN%".

    Explain

    Which untested path did the report find, and what behaviour was it?

    Check

    verify passes with the 80% floor — checked (T3): seat_file seat_build seat_coverage

    #s5-5

  6. 5.6Lab: the tested core9 minproven · T4

    Predict

    The grader drops a hidden test into src/test before running mvn test. What does it rely on?

    Lesson

    Everything in this session, enforced: parameterised parser tests for every rule (wrong column count, bad number, negative, unknown category, blank name), @TempDir everywhere, 80% floor, and the public API exactly as specified so the hidden test compiles.

    Demo

    The tutor shows the demo's parser table test with eight rows, runs verify, then copies a hidden-style test in, runs again, and removes it, explaining what the grader does.

    Hands-on

    Add stockroom.io.ItemParser with static Parsed parse(int lineNo, String row) (record Parsed(Item item, String reason)) and a @CsvSource test parser_rejects of at least five bad rows with the expected reason keyword, plus parser_accepts_good_row. verify green at 80%.

    Explain

    What makes your code ready for a test you have not seen?

    Check

    Parser table tests, 80% coverage and the hidden test all pass — proven (T4): seat_file seat_file seat_coverage seat_test

    #s5-6

Session 6 · Files, data formats and time

Six steps on data at rest. java.nio.file properly: Path operations, Files.walk, atomic moves, charsets. CSV with quoting, embedded commas and a header, through a small parser you write and test instead of split. JSON without a library: the seat has no Jackson or Gson, so the course supplies MiniJson, a 200-line writer and reader for objects, arrays, strings, numbers, booleans and null; you read it, test it and use it. java.time: LocalDate, Instant, Duration, ISO-8601, and why you never store a date as a String you parse by hand. Live in the seat: a temp file moved atomically over the real one, a quoted CSV field with a comma inside, MiniJson round-tripping an item, a date parsed in two formats and compared.

Lab Import and export: Inventory.importCsv(Path) handles quoted fields; Inventory.exportJson(Path) writes items with an ISO timestamp via MiniJson; importJson reads it back equal. A round trip is byte-stable on the second pass. Session 6 checkpoint.

6 mini steps · ~50 min
  1. 6.1java.nio.file beyond readAllLines8 minchecked · T3

    Predict

    Files.move(tmp, target, ATOMIC_MOVE, REPLACE_EXISTING) — what does a reader of target ever see?

    Lesson

    Path.resolve, getParent, getFileName; Files.createDirectories, exists, size, walk (in try-with-resources: it is a stream of paths), newBufferedWriter with UTF_8. Write to a temp file then Files.move with ATOMIC_MOVE so a crash never leaves a half-written file.

    Demo

    The tutor writes a save method in the demo that writes to items.json.tmp and moves it atomically, kills the process mid-write with a breakpoint to show the target untouched, then lists a folder with Files.walk.

    Hands-on

    Add stockroom.io.Atomic with static void write(Path target, List<String> lines) that writes a sibling .tmp file in UTF-8 and moves it atomically with REPLACE_EXISTING. Test atomic_write_replaces_whole_file and atomic_write_leaves_no_tmp with @TempDir.

    Explain

    Why write to a temp file first?

    Check

    Atomic.write uses a temp file and an atomic move; two tests pass — checked (T3): seat_file seat_test

    #s6-1

  2. 6.2CSV done properly9 minchecked · T3

    Predict

    The row "AB-100","Bolt, M6",PARTS,40 is split with split(","). How many parts?

    Lesson

    Real CSV has quoted fields, commas inside quotes and doubled quotes for a literal quote. Write a small state-machine parser: walk the characters, toggle inQuotes, flush a field on a comma outside quotes. Test it with the awkward rows first.

    Demo

    The tutor writes Csv.split(String) as a character loop in the demo, feeds it five awkward rows from java/jv100/samples/s6/awkward.csv, and shows the doubled-quote rule last.

    Hands-on

    Add stockroom.io.Csv with static List<String> split(String row) handling quotes, embedded commas and doubled quotes, and static String join(List<String>) that quotes when needed. @CsvSource-free tests (quotes make CsvSource awkward): csv_split_plain, csv_split_quoted_comma, csv_split_doubled_quote, csv_join_quotes_when_needed, csv_round_trip.

    Explain

    Why not a regular expression for CSV?

    Check

    Csv.split and join handle quoting; five tests pass — checked (T3): seat_file seat_test

    #s6-2

  3. 6.3MiniJson: writing JSON by hand8 minchecked · T3

    Predict

    A name contains a double quote: Bolt 1/4". Written to JSON as is, the result is?

    Lesson

    There is no JSON library in the seat, so the course supplies stockroom/io/MiniJson.java: write(Object) turns Map, List, String, Number, Boolean and null into JSON with proper escaping and stable key order. Read it before using it; it is 200 lines.

    Demo

    The tutor copies MiniJson into the demo from java/jv100/samples/s6/MiniJson.java, writes an item as a LinkedHashMap, prints the JSON, adds a quote to the name to show escaping, and reads the write method together with the learner.

    Hands-on

    Copy java/jv100/samples/s6/MiniJson.java into stockroom.io. Add stockroom.io.ItemJson with static Map<String, Object> toMap(Item) (keys sku, name, category, onHand, unitCost as a string). Tests itemjson_writes_expected_text (exact string) and itemjson_escapes_quote.

    Explain

    Why is unitCost written as a JSON string and not a number?

    Check

    MiniJson is in the project and ItemJson writes the expected text — checked (T3): seat_file seat_test

    #s6-3

  4. 6.4MiniJson: reading JSON8 minchecked · T3

    Predict

    MiniJson.read("{\"a\": 1}") returns what Java type?

    Lesson

    MiniJson.read parses text into Map/List/String/BigDecimal/Boolean/null and throws MiniJson.ParseException with a position on bad input. Mapping to Item is your code: check types, convert, and turn a bad shape into a clear error.

    Demo

    The tutor reads an item back in the demo, maps it with casts and checks, then feeds malformed JSON and reads the ParseException position, and a wrong shape (onHand as a string) and the clear error from fromMap.

    Hands-on

    Add ItemJson.fromMap(Map<String, Object>) returning Item, throwing IllegalArgumentException with the field name on a missing or mistyped field. Tests itemjson_round_trip, itemjson_missing_field_names_it, itemjson_bad_json_position.

    Explain

    Why keep MiniJson generic and put the Item mapping in ItemJson?

    Check

    ItemJson.fromMap round-trips and reports bad shapes — checked (T3): seat_file seat_test

    #s6-4

  5. 6.5java.time8 minchecked · T3

    Predict

    LocalDate.parse("2026-11-08").plusMonths(1) is?

    Lesson

    LocalDate for dates without time, Instant for a point in time, Duration between instants, DateTimeFormatter.ISO_* for text. Store ISO-8601 text; parse at the edge; compare with isBefore/isAfter; inject a Clock so tests control "now".

    Demo

    The tutor adds a Clock to the demo Inventory, stamps events with Instant.now(clock), formats and parses ISO text, computes Duration between two stamps, and fixes the clock in a test with Clock.fixed.

    Hands-on

    Give Inventory a constructor taking a Clock (default Clock.systemUTC()) and Instant lastChanged() updated by apply. Add stockroom.util.Dates with static String iso(Instant) and Instant parse(String). Tests lastChanged_uses_clock (Clock.fixed) and dates_round_trip.

    Explain

    Why inject a Clock instead of calling Instant.now() directly?

    Check

    Inventory takes a Clock; Dates round-trips ISO text — checked (T3): seat_file seat_file seat_test

    #s6-5

  6. 6.6Lab: import and export9 minchecked · T3

    Predict

    exportJson then importJson then exportJson again. The two JSON files should be?

    Lesson

    Inventory.importCsv(Path) uses Csv.split and ItemParser; exportJson(Path) writes {"exportedAt": iso, "items": [...]} atomically; importJson(Path) reads it back with MiniJson and ItemJson. Tests prove equality and byte stability with a fixed clock.

    Demo

    The tutor runs the demo round trip over java/jv100/samples/s6/items-quoted.csv, diffs the two JSON exports in the editor (identical), then breaks key order on purpose to show the diff appear.

    Hands-on

    Implement importCsv, exportJson and importJson on Inventory as described. Tests import_csv_handles_quoted_names (samples/s6/items-quoted.csv, 10 items), export_import_round_trip_equal, and export_is_byte_stable (two exports with Clock.fixed are byte-equal). verify green at 80%.

    Explain

    What two design choices made the export byte-stable?

    Check

    CSV import, JSON export/import and byte-stable round trip pass — checked (T3): seat_file seat_coverage seat_test

    #s6-6

Session 7 · Packages, modules and docs

Six steps on shape. Packages by responsibility (model, io, report, cli), not by type. Visibility as an API decision: public for what callers need, package-private by default, private for the rest, and why a smaller surface is easier to change. Javadoc on the public API and what a good summary sentence does. README as the front door: what, build, test, run. A runnable jar with maven-jar-plugin's mainClass and java -jar. Dependency hygiene with mvn -o dependency:tree and the question "why is this here?". Live in the seat: a class moved between packages and the compiler finding every caller, a public method made package-private and a test that had to move, Javadoc rendered in the editor, a jar run from another folder.

Lab The library/CLI split: stockroom.cli.Main is the only class that prints or exits; everything else is the library with a documented public API; the jar runs from any folder with help, report and reorder commands. Session 7 checkpoint.

6 mini steps · ~46 min
  1. 7.1Packages by responsibility8 minchecked · T3

    Predict

    Which layout is by responsibility?

    Lesson

    A package is a unit of responsibility and of visibility. model holds the domain types, io the file formats, report the queries that render, cli the entry point. Dependencies point inward: cli → report → model; io → model; model depends on nothing of ours.

    Demo

    The tutor moves Inventory, Journal and ReorderReport in the demo into their packages with the editor's move refactoring, watches imports update, then draws the allowed arrows in NOTES.md and finds one that points the wrong way.

    Hands-on

    Arrange java/jv100/stockroom into stockroom.model, stockroom.io, stockroom.report, stockroom.cli and stockroom (Inventory, exceptions). In NOTES.md list each package's responsibility and the allowed dependency arrows. Build green.

    Explain

    Which package must depend on nothing else of yours, and why?

    Check

    Four packages exist and NOTES.md states their responsibilities — checked (T3): seat_file seat_file craft seat_build

    #s7-1

  2. 7.2Visibility is API design7 minchecked · T3

    Predict

    A helper method in stockroom.io is public and used only inside io. Risk?

    Lesson

    Default to package-private; make public only what another package needs; private for internals. A final class with a private constructor for utilities. Tests in the same package can see package-private members, which is often enough.

    Demo

    The tutor removes public from every helper in the demo's io package, fixes the two compile errors by moving a test into the package, and counts the public surface before and after.

    Hands-on

    Reduce stockroom's public surface: every method not used from another package becomes package-private or private; utility classes are final with a private constructor. Record the public method count before and after in NOTES.md. Green.

    Explain

    Why is a smaller public API easier to maintain?

    Check

    Utility classes are final with private constructors; NOTES.md records the surface — checked (T3): seat_file seat_file seat_build

    #s7-2

  3. 7.3Javadoc7 minchecked · T3

    Predict

    The first sentence of a Javadoc comment is special because?

    Lesson

    /** Summary sentence. Details. @param name what it means @return what comes back @throws X when */ on every public type and method. Say what and why, not how; the code says how. The editor shows it on hover, which is where most people read it.

    Demo

    The tutor documents Inventory.apply and Money in the demo, hovers over a call site to show the rendered doc, and rewrites a bad summary ("This method applies") into a good one ("Applies a stock event and returns the updated item").

    Hands-on

    Write Javadoc on every public class and public method in stockroom.model and on Inventory, each with a summary sentence and @param/@return/@throws where they apply. The craft check counts them.

    Explain

    What belongs in Javadoc that does not belong in a code comment?

    Check

    Every public type and method in model and Inventory has Javadoc — checked (T3): craft craft seat_file

    #s7-3

  4. 7.4README and the runnable jar8 minchecked · T3

    Predict

    java -jar target/stockroom-0.1.jar fails with "no main manifest attribute". Cause?

    Lesson

    README: one line on what it is, how to build (mvn -o -q package), test (mvn -o verify), run (java -jar …), the commands, and the exit codes. maven-jar-plugin's manifest mainClass makes the jar runnable; finalName gives it a stable name.

    Demo

    The tutor sets mainClass and finalName stockroom in the demo pom, packages, runs java -jar target/stockroom.jar from the home folder, and writes the README's run section from what just happened.

    Hands-on

    Configure the jar: mainClass stockroom.cli.Main, finalName stockroom. Package it and run java -jar target/stockroom.jar help from a different folder. Rewrite README.md with What / Build / Test / Run / Commands / Exit codes sections.

    Explain

    What should a stranger be able to do after reading the README for one minute?

    Check

    The jar is runnable and the README has the six sections — checked (T3): seat_file seat_build seat_file seat_file

    #s7-4

  5. 7.5Dependency hygiene7 minchecked · T3

    Predict

    mvn -o dependency:tree shows junit-jupiter under which scope?

    Lesson

    Every dependency is a liability: it must be there offline, pinned, licensed, and needed. dependency:tree shows what you pull in and why; test scope keeps test tools out of production; the question for each line is "what breaks if I remove it?".

    Demo

    The tutor runs mvn -o dependency:tree on the demo, reads the scopes, removes a dependency to see what breaks, and checks the jar's size and contents with jar tf.

    Hands-on

    Run mvn -o dependency:tree in stockroom and save it to NOTES.md under a "Dependencies" heading with one line per direct dependency saying why it is there. Confirm with jar tf target/stockroom.jar that no test classes are inside.

    Explain

    Why keep JUnit in test scope?

    Check

    NOTES.md documents the dependency tree with reasons — checked (T3): seat_file

    #s7-5

  6. 7.6Lab: the library and the CLI9 minchecked · T3

    Predict

    Which class is allowed to call System.exit and System.out.println?

    Lesson

    stockroom.cli.Main: parse args, call the library, render with a Renderer that returns Strings, print, exit with the code from run(). Commands help, report <items.csv>, reorder <items.csv>. Exit 0 ok, 2 file problem, 3 bad data, 4 unknown command.

    Demo

    The tutor builds Main.run in the demo returning an int, with Renderer.report and Renderer.reorder returning text, runs the jar with each command and shows the craft check that no other class prints.

    Hands-on

    Write stockroom.cli.Main with static int run(String[] args) and main calling System.exit(run(args)), and stockroom.report.Renderer with static String report(Inventory) and String reorder(List<Line>). Only Main prints or exits. Tests main_help_exits_0, main_unknown_command_exits_4, main_missing_file_exits_2. jar runs: java -jar target/stockroom.jar report samples/s4/items.csv.

    Explain

    What did keeping printing out of the library buy you in the tests?

    Check

    Only Main prints or exits; the jar runs all three commands; tests pass — checked (T3): seat_file craft seat_build seat_test seat_run

    #s7-6

Session 8 · Capstone: Stockroom

Six steps to finish Stockroom as a tested inventory library with a command-line front end. The specification turned into behaviours. Commands that mutate: receive, issue and adjust, applied from an events file and journaled. The report format fixed to the character so a grader can compare it. Edge cases hunted on purpose: empty files, unknown SKUs, insufficient stock, duplicate SKUs on import. A self-review against a checklist before the final check. Live in the seat: the spec read and the test list written, an events file applied with one rejected event, the exact report diffed against the published one, a self-review that finds a swallowed exception.

Lab Stockroom complete: report, reorder and apply commands over the published sample files produce the published outputs and exit codes; your tests are green at 80% coverage; the grader's hidden tests and hidden input files also pass. Session 8 checkpoint and the course placement step.

6 mini steps · ~51 min
  1. 8.1The specification8 minchecked · T3

    Predict

    The spec says apply exits 3 if any event was rejected but still writes the journal for accepted events. Why?

    Lesson

    Read java/jv100/samples/s8/STOCKROOM-SPEC.md: commands report, reorder, apply <items.csv> <events.csv> [--journal path]; exact output formats; exit codes 0/2/3/4. Turn every sentence into a test name before writing code; gaps in the spec are questions for the tutor.

    Demo

    The tutor reads the spec aloud, writes fourteen @Disabled test names into the demo's CapstoneTest, and flags one ambiguity (duplicate SKU on import) to show how a spec gap becomes a decision recorded in NOTES.md.

    Hands-on

    Read the spec. Create stockroom CapstoneTest with at least twelve @Disabled @Test methods named for the behaviours, and add a "Decisions" section to NOTES.md with your answer to the duplicate-SKU question.

    Explain

    Which spec sentence was ambiguous, and what did you decide?

    Check

    CapstoneTest lists twelve behaviours; NOTES.md records a decision — checked (T3): seat_file seat_file seat_build

    #s8-1

  2. 8.2Commands that mutate: apply and the journal9 minchecked · T3

    Predict

    events.csv row: ISSUED,AB-100,50 but only 40 on hand. apply should?

    Lesson

    apply reads items then events (type,sku,quantity[,reason]), parses each into a StockEvent, calls Inventory.apply, catches StockroomException per event as a reject with its line, journals accepted events through Journal in try-with-resources, and prints a summary.

    Demo

    The tutor implements the apply command in the demo over samples/s8/events.csv, shows the journal file lines, the rejected event on stderr with its line number, and the summary "Applied: 5 Rejected: 1".

    Hands-on

    Add stockroom.io.EventParser (row → StockEvent or reason) and the apply command in Main with the summary line "Applied: N Rejected: M" and exit 3 on any reject. Tests apply_applies_and_journals (@TempDir journal, read back), apply_rejects_insufficient_and_exits_3, apply_unknown_event_type_rejected.

    Explain

    Why catch StockroomException per event rather than around the whole loop?

    Check

    apply journals accepted events, rejects bad ones and exits 3 — checked (T3): seat_file seat_test seat_run

    #s8-2

  3. 8.3The report, to the character8 minchecked · T3

    Predict

    The published report uses "%-10s %-24s %-12s %6d %12s%n". Why fixed widths and not tabs?

    Lesson

    Renderer.report(Inventory) renders header, one row per item sorted by Sku with the spec's format, then a blank line, "Total items: N", "Total value: M", "Low stock: K", "Rejected: R". Compare with the published file in the editor's diff view until identical.

    Demo

    The tutor renders the demo report over samples/s8/items.csv, diffs it against samples/s8/expected/report.txt, fixes a width and a rounding, and reaches an empty diff.

    Hands-on

    Make Renderer.report match java/jv100/samples/s8/expected/report.txt byte for byte over samples/s8/items.csv, and Renderer.reorder match expected/reorder.txt. Test report_matches_published and reorder_matches_published by reading those files.

    Explain

    Which detail cost you the last diff?

    Check

    report and reorder match the published outputs — checked (T3): seat_test seat_run

    #s8-3

  4. 8.4Edge cases on purpose8 minchecked · T3

    Predict

    items.csv contains the same Sku twice with different quantities. Your NOTES.md decision was "reject the second". What exit code?

    Lesson

    List the edges before the grader does: header-only files, blank lines, duplicate Skus, unknown categories, zero quantities, a journal path in a folder that does not exist, an events file for an item not in items. One table test per family.

    Demo

    The tutor adds the demo's edge table: six files under samples/s8/edge with expected exit codes, a @MethodSource test that runs Main.run on each, and finds two behaviours that were wrong.

    Hands-on

    Add a @MethodSource test edge_cases_exit_codes over java/jv100/samples/s8/edge/*.csv with the expected exit code per file from EDGES.md, and fix every mismatch. Add duplicate_sku_handled_per_decision.

    Explain

    Which edge case surprised you, and what was the fix?

    Check

    The edge-case table passes — checked (T3): seat_file seat_test

    #s8-4

  5. 8.5Self-review against the checklist8 minchecked · T3

    Predict

    The checklist says "no catch block is empty". Your grep finds catch (IOException e) {}. What is the risk?

    Lesson

    Review your own code as a stranger: java/jv100/samples/s8/CHECKLIST.md — no empty catch, no System.out outside cli, no double for money, every public method documented, every resource in try-with-resources, no TODO left, names say what. Fix, then record in NOTES.md.

    Demo

    The tutor runs the checklist over the demo with grep and the editor's search, finds an empty catch and a stray println, fixes both, and writes the review log.

    Hands-on

    Apply CHECKLIST.md to stockroom. Record each item as pass or fixed in NOTES.md under "Self-review". The craft rules below encode the mechanical items; fix until they pass.

    Explain

    Which checklist item found a real problem?

    Check

    The mechanical checklist items pass and the review is recorded — checked (T3): craft seat_file

    #s8-5

  6. 8.6Lab: Stockroom complete10 minproven · T4

    Predict

    The grader runs your jar on an items file you have never seen and compares the report byte for byte. What must be true?

    Lesson

    Final assembly: every CapstoneTest enabled and green, coverage at 80%, the jar built, the four published sample runs identical to the published outputs, and the hidden runs — files you have not seen, a hidden test over your public API.

    Demo

    The tutor runs the demo's final checklist: verify, the four sample commands diffed, then a hidden-style file, and explains what each part of the grader reads.

    Hands-on

    Complete java/jv100/stockroom: all tests enabled and green, coverage ≥ 80%, java -jar target/stockroom.jar report|reorder|apply over samples/s8 match the expected files and exit codes, README current. Then run mvn -o verify one last time.

    Explain

    What part of the course changed how you write code most?

    Check

    Stockroom passes the published runs, coverage, hidden tests and hidden files — proven (T4): seat_build seat_coverage seat_test seat_run

    #s8-6

Outcome

By the end of the course the tutor's checks will have verified: a Maven project that builds offline with every step's code in it; eight session labs that each grow the last — the skeleton, the domain model, the inventory queries, the robust loader, the tested core at 80% coverage, the import/export round trip, the library/CLI split; and the Stockroom capstone, whose commands, report and exit codes pass hidden tests and input files you did not see.

Certificate

Agile System Software Certificate — Java I (JV100) attests that you can design, build, test and document a tested Java library with a command-line front end — typed domain model, streams, exception design, parameterised tests at an enforced coverage floor, CSV/JSON/date handling — verified by a machine from your own seat including hidden tests.

To earn it:

  • Pass the lab check of every session (steps 1.6, 2.6, 3.6, 4.6, 5.6, 6.6 and 7.6) on your own project.
  • Pass the Stockroom capstone check (8.6) including the hidden tests and input files the grader supplies.
  • Score 80% or better on the 25-question reading quiz.

Gates (step · tier required):

Plus a 25-question reading quiz at 80%, retake after 48 h. Certificates are issued once the seat check runner is live; until then every step records what the engine saw.

Environment

A VS Code seat in your browser with JDK 25, Maven 3.9 offline, JUnit 5 and JaCoCo; the Stockroom project builds, tests and measures coverage with mvn -o. Your work lives in java/jv100; the tutor's demo files live apart from yours and the tutor never edits your hands-on file while you are working on it.

Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick JV100 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.