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.
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).
- 1. Idiomatic Java and the tooling
- 2. Types and data modelling
- 3. Collections and streams
- 4. Errors and resources
- 5. Testing
- 6. Files, data formats and time
- 7. Packages, modules and docs
- 8. Capstone: Stockroom
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.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_buildseat_file1.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_fileseat_build1.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_fileseat_run1.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_fileseat_fileseat_file1.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_filecraftseat_run1.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_fileseat_fileseat_buildseat_testseat_run
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
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_fileseat_test2.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_fileseat_test2.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_fileseat_test2.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_fileseat_test2.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_filecraftseat_test2.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_fileseat_buildseat_test
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
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_fileseat_test3.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_fileseat_test3.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_filecraftseat_test3.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_fileseat_test3.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_fileseat_test3.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_fileseat_buildseat_test
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
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_fileseat_fileseat_test4.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_fileseat_fileseat_test4.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_fileseat_test4.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_fileseat_fileseat_test4.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_fileseat_fileseat_test4.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_fileseat_buildseat_test
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
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_filecraftseat_test5.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_fileseat_fileseat_test5.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_filecraftseat_test5.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_fileseat_test5.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_fileseat_buildseat_coverage5.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_fileseat_fileseat_coverageseat_test
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
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_fileseat_test6.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_fileseat_test6.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_fileseat_test6.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_fileseat_test6.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_fileseat_fileseat_test6.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_fileseat_coverageseat_test
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
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_fileseat_filecraftseat_build7.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_fileseat_fileseat_build7.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):
craftcraftseat_file7.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_fileseat_buildseat_fileseat_file7.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_file7.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_filecraftseat_buildseat_testseat_run
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
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_fileseat_fileseat_build8.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_fileseat_testseat_run8.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_testseat_run8.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_fileseat_test8.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):
craftseat_file8.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_buildseat_coverageseat_testseat_run
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.