Workshop · Java · Advanced
JV300 Java, advanced: building a service
Build Orders, an order-processing service, from the JDK alone: layered architecture with a hand-wired composition root, executors and virtual threads with deadlines, a repository boundary with in-memory and atomic file-backed implementations, an HTTP API on com.sun.net.httpserver with a client on java.net.http, a test suite with fakes, integration tests and an 85% gate, performance measured with nanoTime and JFR, and a session on reading, reviewing and verifying code written by other people and by machines. Forty-eight steps across eight sessions, each ending in a check on your own project, finishing with a capstone that is graded on inputs you never saw.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick JV300 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
Developers who have finished JV100 or write Java at work and want to build a production-shaped service with nothing but the JDK: clear layers, concurrency they can reason about, persistence behind a boundary, an HTTP API, a test suite that scales, measured performance, and the habit of reviewing code — their own, a colleague's and a machine's — before trusting it.
What you will be able to do
- Lay out a service in layers with interfaces at the boundaries, configuration from arguments and environment, a hand-written composition root and structured logs
- Run work concurrently with ExecutorService, virtual threads and CompletableFuture, with deadlines, cancellation and no data races
- Hide storage behind a repository interface with contract tests, and make file persistence atomic and crash-safe
- Serve and consume an HTTP API with the JDK: routes, status codes, JSON bodies, validation at the edge, problem responses
- Scale a test suite: fakes for clock and storage, integration tests over real files, table-driven rules, a coverage gate at 85% and a mutation check by hand
- Measure before optimising: nanoTime harnesses, JFR recordings, complexity in practice, allocation and I/O batching
- Read unfamiliar code to its invariants, find defects by checklist, and verify machine-written code with tests before trusting it
- Deliver a service with an exit-code contract, published expected outputs and a reject file with reason codes
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. Architecture of a service
- 2. Concurrency
- 3. Persistence behind a boundary
- 4. An HTTP API with the JDK
- 5. Testing at scale
- 6. Performance
- 7. Reading, reviewing and the AI era
- 8. Capstone: Orders
Session 1 · Architecture of a service
Six steps on shape before code. Layers and the dependency rule: domain in the middle, application services around it, adapters (files, HTTP, CLI) at the edge, every arrow pointing inward. Interfaces as ports the domain owns and adapters implement. Configuration from arguments and environment with defaults and validation, and the rule that a secret never appears in source or in a log. Wiring by hand in one composition root instead of a framework. Structured logging with java.util.logging and a key=value formatter. Live in the seat: a package diagram turned into folders, an interface moved to the package that owns it, a config value read from three places in priority order, a composition root that is the only place with new for adapters, and a log line parsed by a one-line script.
Lab The Orders skeleton: packages domain, app, adapters.cli, adapters.file, adapters.http; Config from args and env; a Wiring class; structured logs; orders --help exits 0 and orders with no command exits 2. Session 1 checkpoint.
6 mini steps · ~53 min
1.1Layers and the dependency rule9 minchecked · T3
Predict
The domain package imports java.nio.file.Path to save an order. Which rule is broken?
Lesson
Three rings: domain (orders, pricing rules, pure), application (use cases, orchestration), adapters (CLI, files, HTTP). Dependencies point inward only. The test: can the domain compile with the adapters deleted? If not, something leaked.
Demo
The tutor opens java/jv300/demo/orders with the five packages, deletes the adapters folder, runs mvn -o compile to show the domain and app still compile, restores it, and draws the arrows in ARCH.md.
Hands-on
Create java/jv300/orders from the seat starter with packages orders.domain, orders.app, orders.adapters.cli, orders.adapters.file, orders.adapters.http and a placeholder class in each. Write ARCH.md with the layers and the allowed arrows. mvn -o -q package green.
Explain
What does the dependency rule buy you in a year?
Check
Five packages exist, ARCH.md states the rule, the build is green — checked (T3):
seat_fileseat_filecraftseat_build1.2Interfaces as ports9 minchecked · T3
Predict
Who should own the OrderRepository interface: the domain that uses it, or the file adapter that implements it?
Lesson
A port is an interface the inner ring owns: OrderRepository (save, findById, all), Clock (JDK), Notifier. Adapters implement them. Keep ports small and in the language of the domain (orders, not rows or files).
Demo
The tutor writes OrderRepository and PricingRules interfaces in the demo's app package, an InMemoryOrderRepository in adapters, and shows the import directions in the editor's dependency view.
Hands-on
Add orders.domain.Order (record: id, customerId, lines, createdAt) and OrderLine (sku, qty, unitPrice as Money), and orders.app.OrderRepository with save, Optional<Order> findById(String), List<Order> all(). Implement orders.adapters.file.InMemoryOrderRepository. Tests inmemory_save_then_find and inmemory_find_absent_is_empty.
Explain
Why keep a port's methods in domain language?
Check
OrderRepository is owned by app and implemented in adapters; tests pass — checked (T3):
seat_fileseat_fileseat_test1.3Configuration without secrets9 minchecked · T3
Predict
Priority: command-line flag, then environment variable ORDERS_DATA_DIR, then default ./data. Flag and env both set. Which wins?
Lesson
Config is a record built once at startup from args, then System.getenv, then defaults, validated (a port is 1–65535, a directory exists or is creatable). Secrets come only from the environment or a file outside the repo and are never logged or printed.
Demo
The tutor writes Config.from(args, env) in the demo with dataDir, port and logLevel, runs it with the three sources, shows toString redacting a token field, and greps the repo for the token to prove it is absent.
Hands-on
Add orders.app.Config record (dataDir, port, logLevel, apiToken) with static Config from(String[] args, Map<String,String> env) using the priority above, validation, and a toString that redacts apiToken. Tests config_flag_beats_env, config_env_beats_default, config_invalid_port_rejected, config_tostring_redacts_token.
Explain
Why is a secret never allowed in source, even in a test?
Check
Config resolves flag > env > default, validates, redacts — checked (T3):
seat_filecraftseat_test1.4Wiring by hand8 minchecked · T3
Predict
Where may new FileOrderRepository(...) appear?
Lesson
No framework: one class, Wiring, builds the object graph from Config — repository, clock, pricing, services — and hands the CLI an application object. Everything else takes dependencies through constructors. Swapping an adapter is one line.
Demo
The tutor writes Wiring.build(Config) in the demo returning an OrdersApp, shows the CLI calling it, then swaps in-memory for file storage by changing one line and runs again.
Hands-on
Add orders.app.OrdersApp (constructor takes OrderRepository and Clock; method placeOrder returns the saved Order) and orders.Wiring with static OrdersApp build(Config). The craft rule forbids new of adapter classes outside Wiring and tests. Test wiring_builds_app_with_inmemory_repo.
Explain
What does constructor injection make easy?
Check
Wiring is the only place adapters are constructed — checked (T3):
seat_filecraftseat_test1.5Structured logging8 minchecked · T3
Predict
Which log line is easier to query across a million lines?
Lesson
java.util.logging with a custom Formatter: ts=… level=… logger=… event=… k=v …. Log events at INFO, details at FINE, failures at WARNING/SEVERE with the exception. Never log secrets or whole request bodies; log identifiers and counts.
Demo
The tutor installs a KvFormatter in the demo via Log.setup(Config), logs order.placed with fields, pipes the output through grep and cut in the terminal to count events, and shows a WARNING with a stack trace at the end.
Hands-on
Add orders.adapters.log.KvFormatter and orders.Log.setup(Level) writing "ts=<iso> level=<L> logger=<name> <message>". In OrdersApp.placeOrder log event=order.placed order=<id> lines=<n> at INFO. Tests kvformatter_one_line_with_fields and placeOrder_logs_event (capture Handler).
Explain
What two things must never be logged?
Check
KvFormatter emits one line with fields; placeOrder logs event=order.placed — checked (T3):
seat_fileseat_fileseat_test1.6Lab: the Orders skeleton10 minchecked · T3
Predict
orders is run with no command. The skeleton should?
Lesson
Everything so far, runnable: Main → Config.from → Log.setup → Wiring.build → Cli.run(args) returning an exit code. --help exits 0 with usage on stdout; no command exits 2 with usage on stderr; --version prints the version. The jar builds with finalName orders.
Demo
The tutor assembles the demo skeleton, runs java -jar target/orders.jar --help and with no args, shows the two exit codes with echo $?, and the first structured log line on startup.
Hands-on
Write orders.adapters.cli.Cli with static int run(String[] args, OrdersApp app) and orders.Main wiring everything; jar finalName orders with mainClass orders.Main. Tests cli_help_exits_0, cli_no_command_exits_2, cli_version_prints. mvn -o verify green.
Explain
Trace one run from main to the exit code in four steps.
Check
The skeleton builds, tests pass, --help exits 0 and no command exits 2 — checked (T3):
seat_fileseat_fileseat_buildseat_testseat_runseat_run
Session 2 · Concurrency
Six steps on doing several things at once without lying to yourself. Threads and the problem of shared state: a counter that loses increments, and the three cures (confinement, immutability, synchronisation). ExecutorService and Future: submit work, collect results, shut down properly. Virtual threads: thousands of blocking tasks for the price of a few platform threads. CompletableFuture pipelines: thenApply, thenCombine, exceptionally. Timeouts and cancellation: every wait has a deadline, and cancellation is cooperative. Live in the seat: a racy counter printing 9,874 instead of 10,000, an AtomicLong fixing it, a virtual-thread executor running 10,000 sleeps in one second, a pipeline that fails fast, a task cancelled after its deadline and the thread that ignored it.
Lab The parallel pricer: price a batch of orders concurrently with a bounded executor and a deadline per order, producing a deterministic result list in input order, rejecting orders that time out with a reason, and proving the absence of races with a hidden stress test. Session 2 checkpoint.
6 mini steps · ~54 min
2.1Threads and shared state9 minchecked · T3
Predict
Two threads each do count++ 5,000 times on a shared int. Final count?
Lesson
A data race is two threads touching the same mutable state without coordination. Cures in order of preference: do not share (confinement), share only immutable values (records), or synchronise (Atomic*, locks, concurrent collections). Measure, never assume.
Demo
The tutor runs the demo's racy counter five times (different wrong totals), fixes it with AtomicInteger, then shows the same work done with each thread owning its own partial sum combined at the end.
Hands-on
Add orders.app.concurrent.Counters with racyTotal(int threads, int perThread) (deliberately unsafe, package-private), atomicTotal and partitionedTotal (each thread sums its own range). Tests atomic_total_exact and partitioned_total_exact at 8 × 100,000.
Explain
Why is confinement preferable to locking?
Check
Atomic and partitioned totals are exact under 8 threads — checked (T3):
seat_fileseat_test2.2ExecutorService and Future9 minchecked · T3
Predict
An ExecutorService is never shut down. What happens when main finishes?
Lesson
Executors.newFixedThreadPool(n) bounds parallelism; submit returns a Future; get blocks for the result or rethrows the task's exception wrapped in ExecutionException. Use try (var pool = …) so the pool is closed; invokeAll keeps result order.
Demo
The tutor prices eight demo orders with a fixed pool of 4 using invokeAll, shows results in input order, forgets the close to show the hanging JVM, then uses try-with-resources.
Hands-on
Add orders.app.pricing.Pricer (interface: Priced price(Order)) and orders.app.pricing.BatchPricer with List<Priced> priceAll(List<Order>, int parallelism) using a fixed pool in try-with-resources and invokeAll. Tests priceAll_keeps_input_order and priceAll_propagates_task_failure (assertThrows ExecutionException or a wrapped cause).
Explain
Why does invokeAll make ordering easy?
Check
BatchPricer uses a closed fixed pool and keeps order — checked (T3):
seat_fileseat_test2.3Virtual threads8 minchecked · T3
Predict
10,000 tasks that each sleep 100 ms on a virtual-thread-per-task executor finish in roughly?
Lesson
Executors.newVirtualThreadPerTaskExecutor() gives one cheap thread per task; blocking (sleep, I/O) parks the virtual thread without holding a platform thread. Use them for many blocking tasks; CPU-bound work still wants a bounded pool.
Demo
The tutor times 10,000 sleeping tasks on a fixed pool of 8 (long) and on virtual threads (short) with System.nanoTime in the demo, then shows Thread.currentThread().isVirtual() inside a task.
Hands-on
Add BatchPricer.priceAllVirtual(List<Order>) using newVirtualThreadPerTaskExecutor, and orders.app.concurrent.Bench with static long millis(Runnable). Test virtual_runs_2000_sleeping_tasks_under_2s (each task sleeps 50 ms).
Explain
When would a virtual thread not help?
Check
Virtual-thread pricing exists and 2,000 sleeping tasks finish under 2 s — checked (T3):
seat_fileseat_test2.4CompletableFuture pipelines9 minchecked · T3
Predict
cf.thenApply(f).exceptionally(e -> fallback). f throws. What does join() return?
Lesson
CompletableFuture.supplyAsync(task, executor) starts work; thenApply transforms, thenCombine joins two, allOf waits for many, exceptionally or handle recovers. Always pass your executor; the common pool is a shared resource.
Demo
The tutor builds a demo pipeline: fetch customer discount and price lines in parallel, thenCombine them, with exceptionally turning a missing customer into a reject, and shows allOf collecting a batch.
Hands-on
Add orders.app.pricing.AsyncPricer with CompletableFuture<Priced> priceAsync(Order, Executor) combining a discount lookup (DiscountPort, injected) and line pricing via thenCombine, recovering a missing discount to zero with exceptionally. Tests async_combines_discount_and_lines and async_missing_discount_falls_back_to_zero.
Explain
Why pass an executor rather than use the default?
Check
AsyncPricer combines two futures and recovers — checked (T3):
seat_fileseat_test2.5Timeouts and cancellation9 minchecked · T3
Predict
future.cancel(true) is called on a task running a tight loop that never checks Thread.interrupted(). Does it stop?
Lesson
Every wait needs a deadline: future.get(timeout, unit) or orTimeout on a CompletableFuture. On timeout, cancel(true) interrupts; the task must check Thread.currentThread().isInterrupted() in loops and stop. Record timeouts as rejects, never as hangs.
Demo
The tutor gives the demo pricer a slow rule, waits with get(200, MILLISECONDS), catches TimeoutException, cancels, and shows the loop that ignores interruption still running, then adds the interrupted check.
Hands-on
Add a per-order deadline to BatchPricer.priceAll(List<Order>, int parallelism, Duration deadline): a timed-out order becomes Priced.rejected(order, "timeout") and is cancelled with interrupt. Make the pricing loop interruptible. Tests timeout_becomes_reject and cancelled_task_stops (observe via a counter).
Explain
What does cooperative cancellation require from the task?
Check
Timeouts become rejects and cancelled tasks stop — checked (T3):
seat_fileseat_test2.6Lab: the parallel pricer10 minproven · T4
Predict
The grader runs your pricer on 1,000 orders with parallelism 16, fifty times. What must be true every time?
Lesson
BatchPricer finished: bounded parallelism, per-order deadline, interruptible work, results in input order, rejects with reasons, the pool always closed, no shared mutable state outside concurrent primitives. A stress test runs it many times and compares.
Demo
The tutor runs the demo's stress test (50 × 1,000 orders) and a version with a deliberate race to show it failing intermittently, then explains what the hidden test will do.
Hands-on
Finish BatchPricer per the lesson; add PricingStats (priced, rejected, elapsed) returned alongside the results. Tests stress_50_runs_identical_results, no_pool_threads_left_after_priceAll (count threads before/after), rejects_have_reasons. Hidden stress test by the grader.
Explain
Which invariant did your stress test protect?
Check
The pricer is deterministic under stress and leaks no threads — proven (T4):
seat_fileseat_buildseat_test
Session 3 · Persistence behind a boundary
Six steps on storing state without letting storage into the domain. The repository interface as a contract and a contract test suite any implementation must pass. The in-memory implementation that makes the contract concrete and fast. A file-backed implementation: one JSON document per order via the course MiniJson, an index file, atomic writes so a crash never corrupts a record. Transactions as atomic file replacement of the index. Why there is no database driver in this seat and exactly how a SQL repository would plug into the same port. Live in the seat: the contract tests run against two implementations from one abstract class, a write killed at a breakpoint leaving the old file intact, an index rebuilt from the documents, and the SQL adapter's class skeleton with its parameterised statements written but not run.
Lab FileOrderRepository passes the full repository contract, survives a simulated crash mid-write, rebuilds its index from the documents, and is selected by Wiring when Config.dataDir is set. Hidden contract tests run against it. Session 3 checkpoint.
6 mini steps · ~54 min
3.1The repository contract9 minchecked · T3
Predict
An abstract test class RepositoryContract has tests and an abstract newRepository(). What does a subclass for the file store need?
Lesson
Write the behaviour of OrderRepository as tests against the interface: save then findById returns equal; findById of an unknown id is empty; all returns everything saved; save of the same id replaces. Put them in an abstract class with an abstract factory method.
Demo
The tutor writes RepositoryContract in the demo's test tree with five tests, subclasses it for InMemory, and runs it; then adds a sixth test (ordering of all()) and watches it run for every implementation.
Hands-on
Add src/test/java/orders/app/RepositoryContract (abstract, with protected abstract OrderRepository newRepository()) and tests contract_save_then_find_equal, contract_unknown_is_empty, contract_all_returns_saved, contract_save_replaces_same_id, contract_all_sorted_by_id. Subclass InMemoryOrderRepositoryContractTest.
Explain
What is a contract test protecting?
Check
The contract has five tests and the in-memory store passes them — checked (T3):
seat_fileseat_test3.2Documents as JSON with MiniJson9 minchecked · T3
Predict
An Order holds a Money unitPrice. In the JSON document it should be stored as?
Lesson
Copy java/jv300/samples/s3/MiniJson.java into orders.adapters.file. OrderJson maps Order to a Map and back with stable key order; money as strings; timestamps as ISO text; lines as an array. A round trip must produce equal objects and identical bytes.
Demo
The tutor writes OrderJson.toMap and fromMap in the demo, round-trips an order with three lines, diffs two serialisations, and shows a mistyped field producing a clear IllegalArgumentException with the field name.
Hands-on
Add orders.adapters.file.OrderJson with static String write(Order) and static Order read(String) via MiniJson. Tests orderjson_round_trip_equal, orderjson_bytes_stable, orderjson_missing_field_names_it, orderjson_money_is_string.
Explain
Why is byte stability worth a test?
Check
OrderJson round-trips exactly and reports bad shapes — checked (T3):
seat_fileseat_fileseat_test3.3The file-backed repository9 minchecked · T3
Predict
Orders are stored as dataDir/orders/<id>.json. all() must return them sorted by id. Where does the sort happen?
Lesson
FileOrderRepository(Path dataDir): save writes <id>.json; findById reads one file; all lists the directory with Files.list in try-with-resources, sorts, reads each. Ids are validated against a safe pattern so an id can never become a path.
Demo
The tutor implements the demo file store, subclasses RepositoryContract for it with @TempDir, watches all five pass, then tries an id of ../x to show the validation reject.
Hands-on
Add orders.adapters.file.FileOrderRepository implementing OrderRepository over dataDir/orders/*.json with an id pattern ^[A-Za-z0-9_-]{1,40}$. Subclass the contract as FileOrderRepositoryContractTest (@TempDir). Test file_rejects_unsafe_id.
Explain
Why validate ids in the file adapter and not only in the domain?
Check
FileOrderRepository passes the contract and rejects unsafe ids — checked (T3):
seat_fileseat_fileseat_test3.4Atomic writes and the index9 minchecked · T3
Predict
The process dies after writing half of <id>.json directly. On restart, findById(id)?
Lesson
Every write goes to a sibling .tmp then an atomic move. An index.json (id → createdAt) is rewritten the same way after each save, which is the transaction: either the new index is visible or the old one is. On start, if the index is missing, rebuild it from the documents.
Demo
The tutor adds atomic writes to the demo store, stops at a breakpoint between tmp write and move, kills the run, restarts and shows the old file intact; then deletes index.json and shows the rebuild.
Hands-on
Make every write in FileOrderRepository atomic (tmp + ATOMIC_MOVE + REPLACE_EXISTING), maintain index.json, and rebuild it when absent. Tests crash_between_tmp_and_move_keeps_old (simulate by writing a .tmp and calling a package-private recover()), index_rebuilt_when_missing, no_tmp_files_left_after_save.
Explain
What makes the index rewrite a transaction?
Check
Writes are atomic, the index is maintained and rebuilt — checked (T3):
seat_fileseat_test3.5Where SQL would plug in8 minchecked · T3
Predict
A SQL repository builds its query as "… WHERE id = '" + id + "'". The risk?
Lesson
This seat has no JDBC driver, by design: the point of the port is that it does not matter. A SqlOrderRepository would implement the same interface with PreparedStatement and parameters, a transaction per save (document + index row), and the same contract tests. We write the class with the statements as text and mark it not runnable here.
Demo
The tutor writes SqlOrderRepository in the demo as a class whose methods throw UnsupportedOperationException("no JDBC driver in this seat") but whose SQL constants and parameter binding are complete, then points at the contract subclass marked @Disabled with the reason.
Hands-on
Add orders.adapters.sql.SqlOrderRepository with the DDL and the four statements as constants using ? placeholders, every method throwing UnsupportedOperationException naming the reason, and a @Disabled("no JDBC driver in the seat") contract subclass. Add a "Persistence" section to ARCH.md explaining the swap. No string concatenation into SQL anywhere.
Explain
What did the port design let you do here that a direct-to-files design would not?
Check
SqlOrderRepository exists with parameterised statements and a disabled contract — checked (T3):
seat_filecraftseat_fileseat_build3.6Lab: the file store passes the contract10 minproven · T4
Predict
Config.dataDir is set. Which repository should Wiring build?
Lesson
Wiring picks FileOrderRepository when dataDir is set and in-memory otherwise. The CLI gains place <customer> <sku:qty>… and show <id>. The grader runs hidden contract tests against your file store and a hidden crash scenario.
Demo
The tutor wires the demo, runs orders place and orders show from the terminal twice to show persistence across runs, and shows the hidden-style crash test.
Hands-on
Update Wiring per the lesson; add Cli commands place and show; tests wiring_picks_file_store_when_dataDir_set, cli_place_then_show_round_trip (@TempDir dataDir via Config). mvn -o verify green.
Explain
What did you change in the domain to add file persistence?
Check
Wiring selects the file store; CLI round-trips; hidden contract and crash tests pass — proven (T4):
seat_fileseat_buildseat_testseat_run
Session 4 · An HTTP API with the JDK
Six steps on speaking HTTP without a framework. HTTP in one page: request line, headers, body, status codes, and what idempotent means. com.sun.net.httpserver: contexts, handlers, reading the body, writing a response. JSON bodies with MiniJson and validation at the edge that turns bad input into 400 with a problem body. Calling your own API from a test with java.net.http.HttpClient. Error mapping: 404, 409, 422, 500, and never leaking a stack trace. Live in the seat: a raw request typed into a socket, a handler returning 201 with a Location header, a bad body rejected with a field name, an integration test starting the server on port 0, and a 500 that logs the trace but shows the client only an id.
Lab The Orders API: POST /orders, GET /orders/{id}, GET /report, with validation, problem responses and a structured access log, proven by an integration test over HttpClient against a server on an ephemeral port. Session 4 checkpoint.
6 mini steps · ~52 min
4.1HTTP in one page8 minchecked · T3
Predict
A client retries POST /orders after a timeout. Compared with retrying GET, the risk is?
Lesson
Request: method, path, headers, optional body. Response: status, headers, body. 2xx success (201 created with Location), 4xx the client's fault (400 malformed, 404 unknown, 409 conflict, 422 invalid), 5xx ours. Idempotent methods can be retried; POST cannot without a key.
Demo
The tutor opens a raw TCP connection to the demo server from a tiny Java client, types GET /health HTTP/1.1 by hand, reads the response bytes, then does the same with a POST and a body to show Content-Length.
Hands-on
Write orders.adapters.http.RawClient with static String send(String host, int port, String rawRequest) over a plain Socket, and use it in a test against a minimal HttpServer that answers /health with 200 and "ok". Test raw_get_health_is_200.
Explain
Why does a 4xx versus 5xx distinction matter to the client?
Check
A raw socket request reaches a minimal HttpServer — checked (T3):
seat_fileseat_test4.2HttpServer: routes and handlers9 minchecked · T3
Predict
HttpServer.create(new InetSocketAddress(0), 0) then server.getAddress().getPort() gives?
Lesson
HttpServer.create, createContext("/orders", handler), setExecutor(virtual threads), start. A handler reads exchange.getRequestMethod and URI, dispatches on method and path, writes sendResponseHeaders(status, length) then the body, and always closes the exchange.
Demo
The tutor writes OrdersHandler in the demo with a small Router (method + path pattern → function), serves GET /orders/{id} from the repository, and starts the server on port 0 in a test.
Hands-on
Add orders.adapters.http.Router (register(method, pathRegex, handler), dispatch) and orders.adapters.http.ApiServer (start on a port, virtual-thread executor, stop). Route GET /health → 200 ok and GET /orders/{id} → 200 JSON or 404. Tests router_matches_path_params and server_get_unknown_order_is_404.
Explain
Why does the handler delegate to OrdersApp instead of touching the repository?
Check
Router and ApiServer serve health and orders by id — checked (T3):
seat_fileseat_test4.3JSON bodies and validation at the edge9 minchecked · T3
Predict
POST /orders with a body whose qty is "two". The best response?
Lesson
Read the body with a size cap (reject above 64 KiB with 413), parse with MiniJson (400 on ParseException), map to a command with field-by-field checks (422 with {"error": …, "field": …}), then call the app. Respond 201 with Location: /orders/{id}.
Demo
The tutor implements POST /orders in the demo, sends a good body, then bad JSON, then a wrong type, then a huge body, reading each status and problem body from HttpClient.
Hands-on
Add POST /orders to ApiServer with the body cap, MiniJson parsing, orders.adapters.http.OrderRequest validation and a 201 + Location response. Tests post_valid_is_201_with_location, post_bad_json_is_400, post_wrong_type_is_422_naming_field, post_too_large_is_413.
Explain
Why cap the body size before parsing?
Check
POST /orders validates and responds 201/400/413/422 correctly — checked (T3):
seat_fileseat_test4.4HttpClient: calling your own API8 minchecked · T3
Predict
HttpClient.send(request, BodyHandlers.ofString()) returns a 500. Does send throw?
Lesson
java.net.http.HttpClient: build once, HttpRequest with URI, method, headers, body publisher, timeout; send or sendAsync; BodyHandlers.ofString. Always set a timeout, always check statusCode, parse the body only on success.
Demo
The tutor writes OrdersClient in the demo wrapping HttpClient with typed methods placeOrder and getOrder, with a 2 s timeout, and uses it in an integration test against ApiServer on port 0.
Hands-on
Add orders.adapters.http.OrdersClient (constructor: base URI, Duration timeout) with placeOrder(OrderRequest) returning the id and Optional<Order> getOrder(String id). Tests client_place_then_get (server on port 0) and client_times_out_on_slow_server (a handler that sleeps).
Explain
Why build one HttpClient and reuse it?
Check
OrdersClient places and fetches orders with a timeout — checked (T3):
seat_fileseat_test4.5Errors: 404, 409, 500 and no leaks8 minchecked · T3
Predict
An unexpected NullPointerException happens in a handler. The client should see?
Lesson
One error boundary around every handler: domain exceptions map to statuses (UnknownOrder → 404, DuplicateOrder → 409, validation → 422); anything else → 500 with {"error":"internal","incident":"<id>"} and a SEVERE log with the trace and the id. Access log per request.
Demo
The tutor adds the boundary to the demo, throws deliberately inside a handler, shows the client body with an incident id and the matching log line, then the access log with method, path, status and millis.
Hands-on
Add orders.adapters.http.ErrorBoundary wrapping handlers with the mapping above and an access log line "event=http.access method=… path=… status=… ms=…". Tests unknown_order_is_404_problem, unexpected_exception_is_500_with_incident_id_and_no_trace, access_log_written.
Explain
What does the incident id do for you at 3 a.m.?
Check
Errors map to statuses; 500s carry an incident id and leak nothing — checked (T3):
seat_filecraftseat_test4.6Lab: the Orders API10 minchecked · T3
Predict
The integration test starts the server, places two orders, gets the report. Which port does it use?
Lesson
POST /orders, GET /orders/{id}, GET /report (JSON: count, total, byCustomer), the error boundary, the access log, and orders serve --port N from the CLI. One integration test proves the whole path through OrdersClient.
Demo
The tutor runs the demo's integration test, then orders serve from the terminal and hits it with OrdersClient from jshell, showing the access log scroll by.
Hands-on
Add GET /report and the serve command (Config.port). Integration test api_end_to_end: start on port 0, place two orders via OrdersClient, GET /report shows count 2 and the total, GET an unknown id is 404. mvn -o verify green.
Explain
Which layer did the HTTP adapter never touch directly?
Check
The API serves three routes and the end-to-end test passes — checked (T3):
seat_fileseat_fileseat_buildseat_test
Session 5 · Testing at scale
Six steps on a suite that stays trustworthy as the service grows. The pyramid for this service: many fast domain tests, fewer application tests with fakes, a handful of integration tests over real files and the real server. Fakes for the clock and the repository that you own and understand. Integration tests over the file store with @TempDir. Table-driven pricing rules with @MethodSource. The coverage gate at 85% and a mutation check by hand: break a rule, see which test fails, or write the one that should have. Live in the seat: a test that passed for the wrong reason, a FakeClock that makes an expiry test deterministic, a pricing table with twelve rows, a mutation that survived and the test written to kill it.
Lab The test suite: fakes for clock and repository, integration tests over the file store and the API, a twelve-row pricing table, coverage at 85% enforced, and a hidden test suite the grader runs against your public API. Session 5 checkpoint.
6 mini steps · ~52 min
5.1The pyramid for this service8 minchecked · T3
Predict
Which layer should have the most tests?
Lesson
Count your tests by layer. Domain and app: most, milliseconds each, no files or sockets. Adapters: contract and integration tests, few. End to end: one or two. Tag slow tests with @Tag("integration") so the fast set can run alone.
Demo
The tutor counts the demo's tests per package with a shell one-liner, tags the integration ones, and runs only the fast set with -Dgroups and surefire's groups parameter, then everything.
Hands-on
Tag every test that opens a file or a socket with @Tag("integration"). Write TESTING.md with a table of tests per layer and the two commands to run fast-only and all. Confirm mvn -o -q test -Dgroups='!integration' passes.
Explain
Why keep a fast-only test command?
Check
Integration tests are tagged and TESTING.md documents the pyramid — checked (T3):
craftseat_fileseat_test5.2Fakes you own9 minchecked · T3
Predict
An order expires 30 minutes after creation. How do you test expiry without waiting?
Lesson
A fake is a real, simple implementation of a port you write for tests: FakeClock (advance(Duration)), the in-memory repository, a RecordingNotifier (captures calls). No mocking library in the seat and none needed; fakes are clearer and reusable.
Demo
The tutor writes FakeClock in the demo tests, uses it to test order expiry in one millisecond, then a RecordingNotifier to assert that placing an order notifies exactly once.
Hands-on
Add test fakes orders.testing.FakeClock (extends Clock, advance(Duration)) and RecordingNotifier (implements Notifier, List<String> sent). Add OrdersApp.isExpired(Order) with a 30-minute rule. Tests expired_after_30_minutes_with_fake_clock, not_expired_at_29_minutes, placeOrder_notifies_once.
Explain
Why a hand-written fake rather than a mocking library?
Check
FakeClock and RecordingNotifier drive deterministic tests — checked (T3):
seat_fileseat_test5.3Integration tests over real files8 minchecked · T3
Predict
An integration test asserts that after placing 100 orders, dataDir/orders has 100 files and index.json has 100 entries. What is it proving?
Lesson
Integration tests use the real adapter and a @TempDir: place orders through OrdersApp wired with FileOrderRepository, then assert on the file system and on a fresh repository instance reading the same directory. Few, slow, decisive.
Demo
The tutor writes the demo's FileStoreIntegrationTest placing 100 orders, counting files, reopening the directory with a new repository and comparing all().
Hands-on
Add FileStoreIntegrationTest (@Tag("integration"), @TempDir): place 100 orders through OrdersApp over FileOrderRepository, assert 100 documents and 100 index entries, reopen and compare. Tests hundred_orders_persist_and_reload, reopen_sees_same_orders.
Explain
What bug class does this test catch that unit tests miss?
Check
The file-store integration tests pass — checked (T3):
seat_fileseat_test5.4Table-driven pricing rules9 minchecked · T3
Predict
Pricing rules: 5% off at 10+ units of a sku, 10% off orders over 500.00, both can apply. Which is applied first?
Lesson
Put the rules in orders.domain.Pricing as pure functions and the cases in a @MethodSource table: lines, expected subtotal, expected discount, expected total. Twelve rows covering each rule alone, both together, boundaries (exactly 10 units, exactly 500.00), and zero.
Demo
The tutor writes the demo's twelve-row table from java/jv300/samples/s5/PRICING.md, runs it, and finds the boundary row at exactly 500.00 failing because of a > versus >=.
Hands-on
Implement orders.domain.Pricing per samples/s5/PRICING.md and a @MethodSource test pricing_table with at least twelve rows including both boundaries and the combined case. All rows green.
Explain
Which boundary row caught a bug, and what was the fix?
Check
Pricing implements the spec and the twelve-row table passes — checked (T3):
seat_fileseat_fileseat_test5.5The 85% gate and mutation by hand8 minchecked · T3
Predict
You change >= 500 to > 500 in Pricing and every test still passes. What does that tell you?
Lesson
Raise coverage.line.minimum to 0.85 and make verify pass. Then mutate by hand: flip a comparison, delete a line, change a constant; run the tests; if nothing fails, write the test that would. Record each mutation and its killer in TESTING.md.
Demo
The tutor raises the demo's floor, fixes two red lines, then mutates three places in Pricing and Config, finds one survivor, and writes the test that kills it.
Hands-on
Set the floor to 0.85 and make mvn -o verify pass. Perform five hand mutations in domain and app, record each (location, change, killed by which test or new test) in TESTING.md under "Mutations". Every mutation must end up killed.
Explain
What is the difference between covered and tested?
Check
Coverage is enforced at 85% and five mutations are recorded as killed — checked (T3):
seat_fileseat_coverageseat_file5.6Lab: the test suite10 minproven · T4
Predict
The hidden suite calls OrdersApp.placeOrder, Pricing.price and FileOrderRepository through their public signatures. If you renamed placeOrder to place?
Lesson
Everything in this session enforced together: fakes, integration tests tagged, the pricing table, 85% coverage, the mutation log, and a hidden suite the grader compiles against your public API: OrdersApp, Pricing, OrderRepository implementations, Config.
Demo
The tutor runs the demo's full verify, then drops in a hidden-style suite and runs it, explaining each public signature it relies on.
Hands-on
Make the whole suite green at 85% with fast-only and full commands documented, and confirm the public signatures listed in samples/s5/PUBLIC-API.md are present exactly. mvn -o verify green.
Explain
Which test in your suite would you keep if you could keep only three?
Check
The suite is green at 85% and the hidden suite passes — proven (T4):
seat_buildseat_coverageseat_test
Session 6 · Performance
Six steps on making it fast without guessing. Measure first: a nanoTime harness with warm-up and repetitions, medians not means. Complexity in practice: List.contains inside a loop versus a HashSet, and the moment the curve bends. Allocation and strings: StringBuilder, avoiding boxing, the cost of a stream for a two-element list. Java Flight Recorder with jcmd: start a recording on the running JVM, print hot methods and allocation by class. I/O batching: buffered writers and reading in one pass. Live in the seat: a benchmark whose first run is 20× slower than its tenth, a quadratic loop over 50,000 ids timed against a set, a JFR recording read with jfr print, a report writer made 15× faster by buffering.
Lab The 100,000-line report: generate 100,000 orders, write the report in under two seconds with a buffered writer and set-based lookups, with a benchmark test that records the median of five runs and a JFR recording checked in as evidence. Session 6 checkpoint.
6 mini steps · ~51 min
6.1Measure first8 minchecked · T3
Predict
A micro-benchmark runs a method once and reports 180 ms; run ten times, the last run takes 9 ms. Why?
Lesson
Bench.measure(name, warmup, reps, Runnable): run warm-up iterations, then reps, record each with System.nanoTime, report median and p90. Change one thing between measurements. Never trust a single run or a mean.
Demo
The tutor extends the demo's Bench with warm-up and median, benchmarks report rendering before and after a change, and shows the first-run outlier disappear.
Hands-on
Extend orders.app.concurrent.Bench with record Result(String name, long medianNanos, long p90Nanos) and static Result measure(String name, int warmup, int reps, Runnable). Tests bench_median_ignores_outlier (a Runnable slow on its first call) and bench_reps_counted.
Explain
Why the median rather than the mean?
Check
Bench reports median and p90 after warm-up — checked (T3):
seat_fileseat_test6.2Complexity in practice8 minchecked · T3
Predict
For each of 50,000 orders, check whether its customer is in a List of 10,000 ids with contains. Roughly how many comparisons?
Lesson
Know the cost of the operation, not just the name: ArrayList.contains O(n), HashSet.contains O(1), TreeMap O(log n), LinkedList.get(i) O(n). Nested loops over big collections are where quadratic time hides; measure the curve at 1k, 10k, 50k.
Demo
The tutor benchmarks the demo's "orders for known customers" filter with a List and with a HashSet at three sizes, plots the numbers in NOTES as a table, and shows the bend.
Hands-on
Add orders.app.report.KnownCustomers with filterList(List<Order>, List<String>) and filterSet(List<Order>, Set<String>). Benchmark both at 1,000 / 10,000 / 50,000 orders and record the medians in PERF.md. Test set_is_at_least_10x_faster_at_50k.
Explain
What did the table show at 50k that 1k hid?
Check
The set-based filter is measured and at least 10× faster at 50k — checked (T3):
seat_fileseat_test6.3Allocation and strings8 minchecked · T3
Predict
String s = ""; for 100,000 lines: s += line; What is the cost?
Lesson
Allocation is not free: += on strings, boxing in Map<String, Integer> counters, streams over tiny lists, Optional in hot loops. Use StringBuilder, primitive accumulators, and plain loops in the hot path; keep streams where readability matters more.
Demo
The tutor renders the demo report with += and with StringBuilder under Bench (two orders of magnitude), then shows allocation per operation in a JFR recording later in 6.4.
Hands-on
Make orders.app.report.ReportRenderer use a StringBuilder (or write directly to an Appendable) with no string concatenation in loops, and add a Bench comparison to PERF.md. Test renderer_100k_lines_under_500ms.
Explain
Where would you still use a stream after this session?
Check
ReportRenderer uses StringBuilder/Appendable and renders 100k lines fast — checked (T3):
seat_filecraftseat_test6.4Java Flight Recorder with jcmd9 minchecked · T3
Predict
jcmd <pid> JFR.start duration=30s filename=rec.jfr is run on a live JVM. What is the overhead?
Lesson
jcmd lists JVMs; jcmd <pid> JFR.start duration=… filename=… records; jfr print --events jdk.ExecutionSample rec.jfr shows hot stacks; jfr summary shows allocation and GC. Record, read the top frames, fix the top one, measure again.
Demo
The tutor runs the demo report generator with a sleep at start, attaches JFR with jcmd from a second terminal, prints execution samples with jfr, and finds the hot method is CSV quoting.
Hands-on
Add a "bench" CLI command that generates 100,000 orders and renders the report in a loop for 20 s. Record it with jcmd JFR.start, save the recording as perf/report.jfr, and write the top three hot frames from jfr print into PERF.md under "JFR".
Explain
What did the hot frames tell you that the benchmark could not?
Check
A JFR recording exists and PERF.md lists the hot frames — checked (T3):
seat_fileseat_fileseat_file6.5I/O batching8 minchecked · T3
Predict
Files.writeString called once per line for 100,000 lines versus one BufferedWriter. Rough ratio?
Lesson
Open once, write many, close once: Files.newBufferedWriter in try-with-resources; read with a BufferedReader in one pass; avoid flush per line; for the report write directly to the writer instead of building a giant string first.
Demo
The tutor writes the demo report per-line with Files.writeString(APPEND) and then with one BufferedWriter under Bench, and shows the ratio; then streams the renderer straight into the writer.
Hands-on
Add ReportWriter.write(Path, List<Order>) streaming ReportRenderer into one BufferedWriter. Benchmark against a per-line version in PERF.md. Test writer_100k_lines_under_1s and writer_output_matches_renderer.
Explain
Why render into the writer instead of into a String first?
Check
ReportWriter streams through one buffered writer — checked (T3):
seat_fileseat_test6.6Lab: the 100,000-line report under two seconds10 minchecked · T3
Predict
A performance test asserts median < 2 s on the seat. Why median of five and not a single run?
Lesson
The report command over 100,000 generated orders: set-based lookups, StringBuilder rendering, one buffered writer, under two seconds median of five on the seat, with the JFR recording and PERF.md as evidence of what changed and why.
Demo
The tutor runs the demo's report over 100k orders with time, shows the JFR before/after frames, and the benchmark test's output.
Hands-on
Add the CLI command report --generate 100000 <out.txt> and a @Tag("perf") test report_100k_median_under_2s (five runs, Bench). Update PERF.md with before/after medians and the JFR frames. mvn -o verify green.
Explain
Which single change gave the biggest win, and how do you know?
Check
The 100k report is under two seconds median with evidence in PERF.md — checked (T3):
seat_fileseat_buildseat_testseat_run
Session 7 · Reading, reviewing and the AI era
Six steps on code you did not write. Reading unfamiliar code: entry point, data flow, invariants, before changing a line. A review checklist and how to write a finding that someone can act on. Finding five planted defects in ReviewMe.java, a 180-line class with realistic bugs: an off-by-one, a swallowed exception, a race, a money double, a resource leak. Verifying machine-written code: a generated method that looks right and is not, and the test that proves it. Security of input at every edge: size, type, range, path traversal, parameterised queries. The review report as an artefact. Live in the seat: a call graph sketched from the entry point, a finding rewritten from "this is bad" to a line, a reason and a fix, a generated method failing its first boundary test, a path with ../ rejected.
Lab The review report: REVIEW.md with the five planted defects located by line, explained and fixed, the fixed ReviewMe passing the supplied tests, and a verified replacement for the generated method with the tests that prove it. Session 7 checkpoint.
6 mini steps · ~53 min
7.1Reading unfamiliar code9 minchecked · T3
Predict
You open a 2,000-line class you have never seen. First move?
Lesson
Entry points (main, handlers, public methods), then the data: what comes in, how it is transformed, what goes out. Write the invariants you infer ("balance never negative", "ids unique") before touching anything; they become tests.
Demo
The tutor opens java/jv300/samples/s7/ReviewMe.java, finds its two entry points, sketches the data flow in six lines of READING.md, and writes three invariants the code seems to assume.
Hands-on
Copy samples/s7/ReviewMe.java into java/jv300/review/src/main/java (a fresh small Maven project from the starter). Write READING.md: entry points, data flow in ≤ 8 lines, and at least three inferred invariants. Build green.
Explain
Why write invariants before fixing anything?
Check
The review project builds and READING.md has entry points and three invariants — checked (T3):
seat_fileseat_fileseat_build7.2A review checklist and actionable findings8 minchecked · T3
Predict
Which finding can the author act on?
Lesson
samples/s7/REVIEW-CHECKLIST.md: boundaries, nulls, off-by-one, exceptions swallowed, resources, concurrency, money, input validation, logging secrets, tests. A finding = file:line, what, why it matters, how to fix, severity. Prefer questions when unsure.
Demo
The tutor applies the checklist to the first 60 lines of ReviewMe, writes two findings in the format, and rewrites a vague one into an actionable one.
Hands-on
Create REVIEW.md with a findings table (location, what, why, fix, severity) and write the first two findings you can confirm, each with a line number from ReviewMe.java.
Explain
What are the five parts of an actionable finding?
Check
REVIEW.md has a findings table with two line-referenced findings — checked (T3):
seat_file7.3Find the five defects9 minchecked · T3
Predict
ReviewMe has a catch (Exception e) { } around the file read. Which checklist item is that?
Lesson
Five planted defects, one per family: an off-by-one in a loop bound, a swallowed exception, a shared mutable field used from a pool, money as double, a reader never closed. Find each by checklist, confirm it with a failing test before fixing.
Demo
The tutor finds the off-by-one in the demo copy, writes the test that fails because of it, fixes it, and shows the test pass; then outlines where the other four hide without revealing them.
Hands-on
Find all five defects in ReviewMe.java. For each: add a row to REVIEW.md with its line, and a test in ReviewMeTest named defect_<n>_<family> that fails before and passes after the fix. Fix them. All tests green.
Explain
Which defect was hardest to confirm with a test, and how did you do it?
Check
Five defect tests exist and pass against the fixed ReviewMe — checked (T3):
seat_fileseat_fileseat_test7.4Verifying machine-written code9 minchecked · T3
Predict
A generated method comes with a confident comment: "handles all edge cases". What is that comment worth?
Lesson
Treat generated code as a pull request: read it to its invariants, list the claims it makes, write a boundary test per claim, run them, and only then adopt or rewrite. Keep the tests; they are what you actually verified.
Demo
The tutor opens samples/s7/Generated.java (a parseMoney method with a confident comment), lists its four claims, writes a test per claim, watches two fail (negative zero and a trailing dot), and rewrites the method.
Hands-on
Copy samples/s7/Generated.java into the review project. Write GeneratedTest with one test per claim in its comment (at least four), find the failing ones, and fix or rewrite parseMoney until all pass. Record the claims and outcomes in REVIEW.md under "Generated code".
Explain
Which claim failed, and what did the fix need?
Check
Each claim of the generated method has a passing test — checked (T3):
seat_fileseat_fileseat_test7.5Untrusted input at every edge8 minchecked · T3
Predict
GET /files/{name} resolves dataDir.resolve(name). name is "../../etc/passwd". Result without a check?
Lesson
Every edge validates: size (cap bodies and lines), type (parse strictly), range (quantities, dates), shape (ids by pattern), paths (normalize + startsWith base), SQL (parameters only), output encoding (JSON escaping). Reject early with a clear reason.
Demo
The tutor adds a /files route to the demo server without a check, fetches ../pom.xml, then adds SafePath.resolve and shows the 400, then greps the project for every place input crosses an edge.
Hands-on
In the orders project add orders.adapters.SafePath with static Path resolveInside(Path base, String name) that rejects traversal and non-pattern names. Use it wherever a name becomes a path. Tests safepath_rejects_dotdot, safepath_rejects_absolute, safepath_accepts_plain.
Explain
Name three edges in the orders service where input arrives.
Check
SafePath blocks traversal and is used for every name-to-path conversion — checked (T3):
seat_filecraftseat_test7.6Lab: the review report10 minchecked · T3
Predict
The supplied ReviewMeSuppliedTest passes on your fixed class. Does that prove all five defects are fixed?
Lesson
REVIEW.md complete: five defects with location, what, why, fix and severity; the generated-code claims and outcomes; the fixed ReviewMe passing both your defect tests and samples/s7/ReviewMeSuppliedTest; the key compared only after.
Demo
The tutor compares their demo REVIEW.md with samples/s7/REVIEW-KEY.md, shows one finding described differently but at the same line, and explains how the grader matches lines not wording.
Hands-on
Copy samples/s7/ReviewMeSuppliedTest.java into the review project and make it pass alongside your tests. Finish REVIEW.md. Then open REVIEW-KEY.md and add a "Compared with the key" line per defect.
Explain
Which defect would have reached production without the checklist, and why?
Check
The review report is complete and both test suites pass on the fixed class — checked (T3):
seat_fileseat_buildseat_test
Session 8 · Capstone: Orders
Six steps to finish Orders as a production-shaped batch service. The specification and the exit-code contract 0 (all posted), 4 (some rejected), 8 (fatal). Validation with reason codes that a downstream team can act on. Pricing rules and the posting report, byte-exact. The reject file carrying the whole original record plus its reason code. End-to-end runs over the five published test files with a test log. Live in the seat: the spec turned into a behaviour list, a transaction with three problems rejected with the first reason only, the report diffed to zero, a reject file opened beside its input, the five files run from one script with the codes checked.
Lab Orders complete: orders post <transactions.csv> <customers.csv> --report <path> --rejects <path> over the five published files produces the published reports, reject files and exit codes; your suite is green at 85%; the grader's hidden files and hidden tests also pass. Session 8 checkpoint and the course placement step.
6 mini steps · ~53 min
8.1The specification and the contract8 minchecked · T3
Predict
customers.csv is missing. Exit code?
Lesson
samples/s8/ORDERS-SPEC.md: inputs (transactions, customers), validation rules with reason codes R01–R07, pricing, the report format, the reject file format, exit 0/4/8. Every sentence becomes a test name; every ambiguity a recorded decision.
Demo
The tutor reads the spec, writes sixteen @Disabled test names in the demo's CapstoneTest, and records one decision (duplicate transaction ids) in ARCH.md.
Hands-on
Read ORDERS-SPEC.md. Create CapstoneTest with at least sixteen @Disabled @Test methods named for behaviours, and a "Decisions" section in ARCH.md answering the duplicate-transaction question.
Explain
Why does the contract separate 4 from 8?
Check
CapstoneTest lists sixteen behaviours and ARCH.md records a decision — checked (T3):
seat_fileseat_fileseat_build8.2Validation with reason codes9 minchecked · T3
Predict
A transaction has an unknown customer (R02) and a negative quantity (R04). The reject file shows?
Lesson
orders.domain.Validation returns Optional<Reason> for a transaction against the customer set, applying rules in spec order R01 malformed, R02 unknown customer, R03 unknown sku, R04 quantity, R05 price, R06 date, R07 duplicate id. Reason is an enum with code and text.
Demo
The tutor implements the first three rules in the demo with a @CsvSource table from the spec's examples, then adds the rest and shows the order-of-rules test.
Hands-on
Implement orders.domain.Reason (enum R01–R07 with text) and Validation.check(Transaction, Set<String> customers, Set<String> skus, Set<String> seenIds) returning Optional<Reason>. Table test validation_rules with one row per rule and validation_first_rule_wins.
Explain
Why one reason per reject rather than all of them?
Check
Validation applies R01–R07 in order; the table and ordering tests pass — checked (T3):
seat_fileseat_fileseat_test8.3Pricing and the posting report9 minchecked · T3
Predict
The report total must equal the sum of the printed line totals, each rounded to cents. Round where?
Lesson
Pricing from session 5 applied per transaction; the report: a header, one row per posted transaction in input order with the spec's format, a blank line, then Posted, Rejected, Total. Compare with samples/s8/expected/report-std.txt until the diff is empty.
Demo
The tutor renders the demo report over transactions-std.csv, diffs against the expected file, fixes a rounding and a width, and reaches zero.
Hands-on
Implement the posting report in orders.app.report.PostingReport (returns String) and the post command's --report output. Test report_std_matches_published against samples/s8/expected/report-std.txt.
Explain
Which detail did the diff catch?
Check
The posting report matches the published standard report — checked (T3):
seat_testseat_runseat_file8.4The reject file8 minchecked · T3
Predict
The reject file row for a bad transaction contains?
Lesson
rejects.csv: the input header plus reason_code,reason_text; one row per reject with the original fields unchanged (re-quoted correctly) and the reason. Written atomically; empty but with a header when nothing was rejected.
Demo
The tutor writes the demo reject file for transactions-std.csv, opens it beside the input, and shows a quoted field surviving the round trip.
Hands-on
Implement orders.app.report.RejectFile.write(Path, List<Rejected>) per the spec with Csv quoting and an atomic write. Tests rejects_std_matches_published (expected/rejects-std.csv) and rejects_empty_has_header_only.
Explain
Why include the whole original row?
Check
The reject file matches the published one and handles the empty case — checked (T3):
seat_fileseat_test8.5End to end over the five files9 minchecked · T3
Predict
transactions-empty.csv (header only). Expected exit and report?
Lesson
Five published files: std (exit 4), empty (0), all-rejects (4), missing customers file (8), unsorted/duplicate ids (4). One @MethodSource test runs each through Main.run with @TempDir outputs and compares exit code, report and reject file to the expected assets. Keep a TEST-LOG.md.
Demo
The tutor runs the demo's five-file test, shows one mismatch on the duplicate-id file, fixes it, and fills the test log.
Hands-on
Add CapstoneTest.five_published_files (@MethodSource) comparing exit, report and rejects for each file to samples/s8/expected. Write TEST-LOG.md with a row per file: exit expected/actual, report match, rejects match. All green.
Explain
Which file exposed a rule you had not implemented?
Check
All five published files pass end to end and the test log is filled — checked (T3):
seat_fileseat_test8.6Lab: Orders complete10 minproven · T4
Predict
The grader runs your jar on a hidden transactions file with a rule combination you never saw. What protects you?
Lesson
Final assembly: every CapstoneTest enabled and green, 85% coverage, the jar built, the five published runs identical, README current, and the hidden runs — files you have not seen, a hidden test suite over your public API.
Demo
The tutor runs the demo's final checklist and one hidden-style file, then walks through what the grader records: exit codes, byte-equal outputs, test report, coverage.
Hands-on
Complete java/jv300/orders: all tests enabled and green, coverage ≥ 85%, java -jar target/orders.jar post over each published file matches expected outputs and exit codes, README and ARCH.md current. mvn -o verify one last time.
Explain
What is the one practice from this course you will carry into your next service?
Check
Orders passes the published runs, coverage, hidden files and hidden tests — proven (T4):
seat_buildseat_coverageseat_testseat_runseat_file
Outcome
By the end of the course the tutor's checks will have verified: a layered service that builds offline; eight session labs that each grow the last — the skeleton with config and logs, the parallel pricer, the file store that passes the repository contract, the Orders API with an integration test, the test suite at 85%, the 100,000-line report under budget, the review report with five found defects; and the Orders capstone, whose validation, pricing, report, reject file and exit codes 0/4/8 match the published values on five test files and on hidden ones.
Certificate
Agile System Software Certificate — Java, advanced (JV300) attests that you can design, build, test, measure and review a production-shaped Java service — layered architecture, concurrency with deadlines, crash-safe persistence behind a repository, an HTTP API, a test suite at an enforced 85% gate — verified by a machine from your own seat including hidden inputs and 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 Orders capstone check (8.6) including the hidden input files and hidden tests the grader supplies.
- Score 80% or better on the 30-question reading quiz.
Gates (step · tier required):
Plus a 30-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 (virtual threads, HttpServer, HttpClient, JFR), Maven 3.9 offline, JUnit 5 and JaCoCo; no frameworks and no database driver, by design. Your work lives in java/jv300; 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 JV300 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.