- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Advanced Java Features Working Together
This is a logistics company's internal tool for turning raw shipment scan lines from warehouse scanners into typed events (created, delayed, delivered, lost), summarizing them for dispatchers, batching them safely for downstream processors, and auditing every processing run so failures are never silently swallowed.
Lab Info
Table of Contents
-
Challenge
Step 1: Meet the shipment pipeline
The shipment pipeline turns raw warehouse scanner text into shipment intelligence. A scanner emits a line like
CREATED,TRK-100,DOCK-4,2024-03-01T10:00:00Zand by the time the pipeline is done with it, a dispatcher has seen a validated, typed event, a batch processor has run it through a handler, and an audit log has recorded the processed events.If parsing or cleanup fails, the pipeline preserves exception information so callers can investigate.
You will implement key parts of this workflow using several Java features. You will work with six files, each adding one layer to the pipeline:
-
ShipmentEvent.javamodels the four kinds of scan events as records behind a sealed interface, allowing only the four permitted event types to implement the interface. -
DispatcherStatus.javamatches those variants using pattern-matchedinstanceofand an exhaustiveswitchto generate dispatcher text without requiring adefaultbranch. -
EventBatch.javais a generic container whose wildcard methods follow PECS, allowing it to accept and process events without variance errors. -
EventParser.javaturns raw strings intoOptional<ShipmentEvent>while wrapping checked parsing exceptions in unchecked exceptions that callers can handle. -
AuditLog.javaandPipelineRunner.javacomplete the pipeline with a custom@Tracedannotation and anAutoCloseableresource that preserves suppressed exceptions. Each step leaves you with working, testable code: -
Step 2: Create validated event data.
-
Step 3: Generate text from the event data.
-
Step 4: Batch and process multiple events.
-
Step 5: Combine parsing, annotation-driven tracing, and safe resource cleanup into one
runcall.
Open
src/main/java/lab/ShipmentEvent.javaand move on to Step 2. ### Running validation checksTo run all graded checks at any point, open the Terminal tab and run:
bash runTest.sh 'Step*Test'This command runs all the lab's step-level test classes (
Step2Test,Step3Test,Step4Test, andStep5Test).To run the validation for a specific task, such as Task 5.4, use:
bash runTest.sh task_5_4info> Note: Validation checks may fail until you complete the tasks because the starter code contains
UnsupportedOperationExceptionplaceholders. Successful checks may display no output. To confirm success, runecho $?immediately after validation. An exit code of0indicates success. Detailed results are available intarget/surefire-reports. -
-
Challenge
Step 2: Model immutable shipment events with records and sealed types
Open
src/main/java/lab/ShipmentEvent.java. It declaresShipmentEventas a plain interface with four nested record implementations:Created,Delayed,Delivered, andLost.Currently, other classes can also implement this interface. At the end of this step, you will seal it so the compiler knows the complete list of permitted variants. This allows later steps to use an exhaustive
switchwithout adefaultbranch.Records provide final fields and generated accessors, but you still need to validate their values. A compact constructor can reject invalid arguments before the record fields are assigned. The
Deliveredrecord is already complete and demonstrates this pattern: validate the arguments, then implementsummary()to generate dispatcher-facing text.You will apply the same approach to
Created,Delayed, andLost.Delayedcarries a reason and a duration instead of a location, but the same discipline applies: the compact constructor can prevent creation of an invalidDelayedbefore its fields are assigned. Blank-string and non-positive-number checks belong there, not scattered through code that consumes the event later.Lostis the fourth and final variant, completing the domain model used throughout the pipeline. LikeCreated, it has two required strings, a compact constructor, and asummary()method.After completing all four variants, you will seal
ShipmentEventso only those four record types can implement the interface. -
Challenge
Step 3: Match shipment variants safely with instanceof and switch
With
ShipmentEventnow sealed overCreated,Delayed,Delivered, andLost, the compiler now knows the complete list of shapes any event can take.That knowledge is only useful if the code that reads events can lean on it, and that is what
DispatcherStatus.javadoes: it turns a rawShipmentEventinto the short status line a dispatcher actually reads on a screen. Previously, inspecting an object's type required a check followed by an explicit cast:if (event instanceof Delayed) { Delayed delayed = (Delayed) event; // Process the delayed event }Pattern-matched
instanceofcombines the type check, cast, and variable declaration into one expression:if (event instanceof Delayed delayed). When the check succeeds,delayedis available to use without an explicit cast.You can also add a condition using
&&to check whether the event meets additional criteria, such as whether a delay requires urgent attention. Useinstanceofto identify a specific event type, such as an urgent delay. To generate a status line for every event type, use aswitchexpression.Java's
switchexpressions support pattern matching on the permitted subtypes of a sealed interface:switch (event) { case Created c -> ...; case Delayed d -> ...; // Additional cases }Because
ShipmentEventissealed, the compiler can verify that theswitchhandles every permitted subtype without requiring adefaultbranch.In this lab, omit the
defaultbranch so the compiler checks for exhaustive matching. If a new record type is added to the hierarchy later, theswitchmust be updated to handle it. -
Challenge
Step 4: Build a variance-safe generic batching API
Dispatchers rarely work with a single
ShipmentEventat a time. A warehouse scanner produces multiple scan lines, and each line must become an event, be grouped with other events from the same run, and be passed to downstream logic that monitors delays or losses.This batching functionality belongs to
EventBatch<T>, which you'll complete insrc/main/java/lab/EventBatch.java. Its methods currently acceptCollection<T>,Handler<T>, andEventBatch<T>...parameters. You'll update these methods to support type-safe variance.The goal is to make the API flexible enough to accept events from a
List<ShipmentEvent.Delayed>or aList<ShipmentEvent>and pass them to a handler that acceptsShipmentEvent, without requiring separate method overloads for each type. Java generics are invariant by default. For example, aList<ShipmentEvent.Delayed>is not aList<ShipmentEvent>, even thoughShipmentEvent.DelayedimplementsShipmentEvent.Wildcards provide the flexibility needed to work with related types. The mnemonic PECS (Producer Extends, Consumer Super) helps determine which wildcard to use:
- Producer Extends: Use
? extends Twhen a parameter provides values. - Consumer Super: Use
? super Twhen a parameter receives values.
The
addAllmethod follows the producer side of PECS because the incoming collection provides events to the batch without receiving any values in return.processAllworks in the opposite direction fromaddAll. The batch acts as the producer, passing stored events to aHandler<T>, which acts as the consumer.A handler that accepts any
ShipmentEventshould also be able to process a batch ofShipmentEvent.Delayedevents. This follows the Consumer Super principle of PECS: using? super Tallows a handler to accept events of typeTor its supertypes. Wildcards handle single collections, but combining several batches at once needs varargs and generic varargs is where the compiler normally warns you about heap pollution, because an array of a non-reifiable generic type can be exposed to unsafe assignments.@SafeVarargsexists for situations where the method implementation safely handles the varargs array: it lets you tell the compiler 'I've checked, this method only reads from the array and never stores into it,' which is true here sincemergeonly copies events out of each batch.It's a promise you're making about the method body, not a way to silence a warning you haven't verified.
- Producer Extends: Use
-
Challenge
Step 5: Parse, annotate, and audit the full pipeline
You now have events, a batching API, and pattern-matched dispatcher logic. What you don't have yet is a way to get from a raw scanner line to a
ShipmentEventwithout a straynull, and without a checked exception that lambdas and streams can't handle. That's the job ofEventParser: a small functional pipeline that turns text intoOptional<ShipmentEvent>, so callers can reason about "present or absent" instead of chasingNullPointerExceptions.The underlying
RawLineParserinterface declares aparsemethod that can throw a checkedParseException. Standard stream operations do not accept lambdas that throw this checked exception, soparseLinewraps it as an unchecked exception. Blank input is skipped, but malformed input still fails and preserves the original exception as its cause. With a single-line converter in hand, turning a whole batch of raw scanner output into events is just a stream pipeline: map each line throughparseLine, drop the empties, and unwrap the rest. Writing it this way means the blank-line and error-wrapping logic stays in exactly one place --parseLine-- andparseAllonly has to describe the shape of the transformation. Parsing gets raw lines into the pipeline; auditing makes sure a processing run leaves a trustworthy trail even when something goes wrong while cleaning up.AuditLogholds two channels, a primary and a backup, and implementsAutoCloseableso a try-with-resources block can close the audit log automatically.The failure to guard against occurs when both channels throw exceptions while closing. If the second exception replaces the first, the original failure information is lost.
Java's
Throwable.addSuppressedexists for exactly this: it lets one exception carry the other along as extra context instead of discarding it. Every piece is finally in place:EventParserfor text,EventBatchfor variance-safe collection,DispatcherStatusfor pattern-matched summaries, andAuditLogfor exception-safe cleanup.PipelineRunner.runis where they meet.It's also annotated
@Traced, a marker declared just above it that records a label for a run. Look at its@Retention, though:CLASSretains the annotation in the compiled class file, but reflection cannot retrieve it at runtime. You'll switch it toRUNTIME, then read it back withgetMethod(...).getAnnotation(Traced.class)and feed itsvalue()into the audit log and the run summary. That is what makes the annotation observable instead of decorative.The runner gets its
AuditLogfrom aSupplier<AuditLog>passed to its constructor, so a test can hand it an audit log whose channels fail on close. Opening that log in try-with-resources means a failure inside the run is never hidden by a second failure while closing: Java attaches the close failure to the original exception with the sameaddSuppressedyou just used by hand. -
Challenge
Step 6: Watch the shipment pipeline handle a batch
After completing the tasks and passing their checks, run the pipeline once and watch the pieces you wrote separately handle one batch of scanner lines: sealed events, pattern matching, a generic batch, a null-safe parser and an audit log.
Run the whole pipeline
Open the Terminal tab and run:
bash runApp.shThe program sends four scanner lines through your code, one of them blank, and prints four sections. Look for these:
- Section 1 prints
shipment-pipeline-run: processed 3 events. The label is the@Tracedvalue thatPipelineRunner.runfrom Task 5.4 read through reflection. The blank line never became an event, because yourEventParser.parseLinefrom Task 5.1 returnsOptional.empty()for it, soruncounted only the three real ones. - Section 2 prints one dispatcher line per event, starting
NEW:,DELAY:, andDONE:. That is your exhaustiveswitchinDispatcherStatus.statusForfrom Task 3.2, with one case per sealed variant. The delayed shipment also prints an extraURGENT:line, because it is delayed 6 hours and yourinstanceofguard from Task 3.1 flags anything over 4. - Section 3 feeds in
DELAYED,TRK200,Weather,soonand printsFailed: Failed to parse line: DELAYED,TRK200,Weather,soonfollowed byCaused by: Malformed line: .... The bad line is rejected loudly, and the original checked exception is still attached as the cause. - Section 4 closes an audit log whose two channels both fail. It prints
Thrown: primary channel failedandSuppressed: backup channel failed. YourAuditLog.closefrom Task 5.3 kept the second failure instead of silently replacing the first. ### Change one value and rerun
Open
src/main/java/lab/Main.javaand find this line:"DELAYED,TRK100,Weather,6",Change the
6to3and runbash runApp.shagain.In Section 2, the
DELAY:line now saysdelayed 3 hours, and theURGENT:line is gone because 3 hours is below the 4-hour threshold indescribeIfDelayed. Section 1 still printsshipment-pipeline-run: processed 3 events.Change the
3back to6when you are done.You now have a pipeline that turns raw scanner text into typed, immutable events, summarizes them for dispatchers, and never loses a failure along the way.
To rerun all graded checks, use
bash runTest.sh 'Step*Test'. - Section 1 prints
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.