Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Intermediate
Last updated
Nov 04, 2026
Duration
1h 30m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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:00Z and 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.java models the four kinds of scan events as records behind a sealed interface, allowing only the four permitted event types to implement the interface.

    • DispatcherStatus.java matches those variants using pattern-matched instanceof and an exhaustive switch to generate dispatcher text without requiring a default branch.

    • EventBatch.java is a generic container whose wildcard methods follow PECS, allowing it to accept and process events without variance errors.

    • EventParser.java turns raw strings into Optional<ShipmentEvent> while wrapping checked parsing exceptions in unchecked exceptions that callers can handle.

    • AuditLog.java and PipelineRunner.java complete the pipeline with a custom @Traced annotation and an AutoCloseable resource 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 run call.

    Open src/main/java/lab/ShipmentEvent.java and move on to Step 2. ### Running validation checks

    To 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, and Step5Test).

    To run the validation for a specific task, such as Task 5.4, use:

    bash runTest.sh task_5_4
    

    info> Note: Validation checks may fail until you complete the tasks because the starter code contains UnsupportedOperationException placeholders. Successful checks may display no output. To confirm success, run echo $? immediately after validation. An exit code of 0 indicates success. Detailed results are available in target/surefire-reports.

  2. Challenge

    Step 2: Model immutable shipment events with records and sealed types

    Open src/main/java/lab/ShipmentEvent.java. It declares ShipmentEvent as a plain interface with four nested record implementations: Created, Delayed, Delivered, and Lost.

    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 switch without a default branch.

    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 Delivered record is already complete and demonstrates this pattern: validate the arguments, then implement summary() to generate dispatcher-facing text.

    You will apply the same approach to Created, Delayed, and Lost. Delayed carries a reason and a duration instead of a location, but the same discipline applies: the compact constructor can prevent creation of an invalid Delayed before its fields are assigned. Blank-string and non-positive-number checks belong there, not scattered through code that consumes the event later. Lost is the fourth and final variant, completing the domain model used throughout the pipeline. Like Created, it has two required strings, a compact constructor, and a summary() method.

    After completing all four variants, you will seal ShipmentEvent so only those four record types can implement the interface.

  3. Challenge

    Step 3: Match shipment variants safely with instanceof and switch

    With ShipmentEvent now sealed over Created, Delayed, Delivered, and Lost, 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.java does: it turns a raw ShipmentEvent into 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 instanceof combines the type check, cast, and variable declaration into one expression: if (event instanceof Delayed delayed). When the check succeeds, delayed is 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. Use instanceof to identify a specific event type, such as an urgent delay. To generate a status line for every event type, use a switch expression.

    Java's switch expressions support pattern matching on the permitted subtypes of a sealed interface:

    switch (event) {
        case Created c -> ...;
        case Delayed d -> ...;
        // Additional cases
    }
    

    Because ShipmentEvent is sealed, the compiler can verify that the switch handles every permitted subtype without requiring a default branch.

    In this lab, omit the default branch so the compiler checks for exhaustive matching. If a new record type is added to the hierarchy later, the switch must be updated to handle it.

  4. Challenge

    Step 4: Build a variance-safe generic batching API

    Dispatchers rarely work with a single ShipmentEvent at 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 in src/main/java/lab/EventBatch.java. Its methods currently accept Collection<T>, Handler<T>, and EventBatch<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 a List<ShipmentEvent> and pass them to a handler that accepts ShipmentEvent, without requiring separate method overloads for each type. Java generics are invariant by default. For example, a List<ShipmentEvent.Delayed> is not a List<ShipmentEvent>, even though ShipmentEvent.Delayed implements ShipmentEvent.

    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 T when a parameter provides values.
    • Consumer Super: Use ? super T when a parameter receives values.

    The addAll method follows the producer side of PECS because the incoming collection provides events to the batch without receiving any values in return. processAll works in the opposite direction from addAll. The batch acts as the producer, passing stored events to a Handler<T>, which acts as the consumer.

    A handler that accepts any ShipmentEvent should also be able to process a batch of ShipmentEvent.Delayed events. This follows the Consumer Super principle of PECS: using ? super T allows a handler to accept events of type T or 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.

    @SafeVarargs exists 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 since merge only 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.

  5. 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 ShipmentEvent without a stray null, and without a checked exception that lambdas and streams can't handle. That's the job of EventParser: a small functional pipeline that turns text into Optional<ShipmentEvent>, so callers can reason about "present or absent" instead of chasing NullPointerExceptions.

    The underlying RawLineParser interface declares a parse method that can throw a checked ParseException. Standard stream operations do not accept lambdas that throw this checked exception, so parseLine wraps 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 through parseLine, 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 -- and parseAll only 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.

    AuditLog holds two channels, a primary and a backup, and implements AutoCloseable so 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.addSuppressed exists for exactly this: it lets one exception carry the other along as extra context instead of discarding it. Every piece is finally in place: EventParser for text, EventBatch for variance-safe collection, DispatcherStatus for pattern-matched summaries, and AuditLog for exception-safe cleanup. PipelineRunner.run is 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: CLASS retains the annotation in the compiled class file, but reflection cannot retrieve it at runtime. You'll switch it to RUNTIME, then read it back with getMethod(...).getAnnotation(Traced.class) and feed its value() into the audit log and the run summary. That is what makes the annotation observable instead of decorative.

    The runner gets its AuditLog from a Supplier<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 same addSuppressed you just used by hand.

  6. 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.sh
    

    The 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 @Traced value that PipelineRunner.run from Task 5.4 read through reflection. The blank line never became an event, because your EventParser.parseLine from Task 5.1 returns Optional.empty() for it, so run counted only the three real ones.
    • Section 2 prints one dispatcher line per event, starting NEW:, DELAY:, and DONE:. That is your exhaustive switch in DispatcherStatus.statusFor from Task 3.2, with one case per sealed variant. The delayed shipment also prints an extra URGENT: line, because it is delayed 6 hours and your instanceof guard from Task 3.1 flags anything over 4.
    • Section 3 feeds in DELAYED,TRK200,Weather,soon and prints Failed: Failed to parse line: DELAYED,TRK200,Weather,soon followed by Caused 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 failed and Suppressed: backup channel failed. Your AuditLog.close from Task 5.3 kept the second failure instead of silently replacing the first. ### Change one value and rerun

    Open src/main/java/lab/Main.java and find this line:

    "DELAYED,TRK100,Weather,6",
    

    Change the 6 to 3 and run bash runApp.sh again.

    In Section 2, the DELAY: line now says delayed 3 hours, and the URGENT: line is gone because 3 hours is below the 4-hour threshold in describeIfDelayed. Section 1 still prints shipment-pipeline-run: processed 3 events.

    Change the 3 back to 6 when 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'.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

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.

Get started with Pluralsight