Concurrency: VTask Journey

What We'll Learn

  • Creating and composing lazy concurrent computations with VTask
  • Executing tasks on virtual threads with proper error handling
  • Using Par combinators for parallel execution
  • Understanding structured concurrency principles
  • Using the fluent VTaskPath API for effect-based workflows
  • Working with VTaskContext for dependency injection

Duration: ~45 minutes | Tutorials: 3 (VTask, VTaskPath, ForPath with VTaskPath) | Exercises: 18 (16 graded + 2 diagnostic)

Requirements: Java 25+ (virtual threads and structured concurrency)

Where This Fits in the Bigger Picture

VTask is the deferred-effect type for virtual-thread-based concurrency; it is what we reach for when the repo.find(id) token in One Line, Six Layers is async, which it almost always is in production. VTaskPath lifts VTask into the Effect Path API, so the same fluent shape works for sync and async code.

This journey adopts the Phase 2 template: every tutorial opens with a Pain → Promise header showing the imperative-Java pain (Executors, futures, thenCompose, exceptionally) it replaces, and ends with a diagnostic exercise drawn from a common stumble.

Journey Overview

This journey introduces VTask and VTaskPath, Higher-Kinded-J's types for virtual thread-based concurrency. We learn to describe computations lazily, compose them functionally, and execute them on lightweight virtual threads.

┌─────────────────────────────────────────────────────────────────┐
│                    VTask Journey                                │
│                                                                 │
│  Tutorial 1: VTask Fundamentals                                 │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐      │
│  │ VTask.of │ → │ map/flat │ → │ Par.zip  │ → │ runSafe  │      │
│  │          │   │   Map    │   │  /map2   │   │          │      │
│  └──────────┘   └──────────┘   └──────────┘   └──────────┘      │
│                                                                 │
│  Tutorial 2: VTaskPath Effect API                               │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐      │
│  │Path.vtask│ → │ via/map  │ → │ timeout/ │ → │   run    │      │
│  │          │   │          │   │handleErr │   │          │      │
│  └──────────┘   └──────────┘   └──────────┘   └──────────┘      │
└─────────────────────────────────────────────────────────────────┘

By the end, you'll understand how to build concurrent applications using functional composition rather than low-level thread management.


Tutorial 1: VTask Fundamentals (~25 minutes)

File: TutorialVTask.java | Exercises: 8

Master virtual thread-based concurrency with functional composition.

Part 1: Creating VTasks

What you'll learn:

  • Creating lazy computations with VTask.of() and VTask.delay()
  • Immediate values with VTask.succeed() and VTask.fail()
  • Understanding that VTask describes effects without executing them

Key insight: A VTask is a recipe, not a meal. Creating a VTask doesn't run anything; it just describes what would happen when you eventually call run().

Exercise 1: Create a VTask that computes the length of a string Exercise 2: Create a succeeding and a failing VTask


Part 2: Transforming VTasks

What you'll learn:

  • Using map to transform results
  • Using flatMap to chain dependent computations
  • Short-circuiting on errors automatically

Key insight: VTask follows the same patterns as other monads in Higher-Kinded-J. If you know Either.flatMap, you know VTask.flatMap.

Exercise 3: Transform a VTask result using map Exercise 4: Chain dependent VTask computations with flatMap


Part 3: Error Handling

What you'll learn:

  • Using runSafe() to capture errors in Try
  • Recovery with recover and recoverWith
  • Transforming errors with mapError

Key insight: runSafe() is the preferred execution method; it captures failures in a Try, letting you handle errors functionally rather than with try-catch.

Exercise 5: Execute a VTask safely and handle the result Exercise 6: Recover from a failed VTask with a default value


Part 4: Parallel Composition

What you'll learn:

  • Using Par.zip to run tasks in parallel
  • Combining parallel results with Par.map2
  • Racing tasks with Par.race
  • Collecting results with Par.all

Key insight: Par combinators use StructuredTaskScope under the hood, ensuring proper cancellation if any task fails.

Exercise 7: Execute two tasks in parallel and combine results Exercise 8: Race multiple tasks and get the first result


Tutorial 2: VTaskPath Effect API (~20 minutes)

File: TutorialVTaskPath.java | Exercises: 8

Learn the fluent Effect Path API for VTask-based workflows.

Part 1: Creating VTaskPaths

What you'll learn:

  • Creating paths with Path.vtask(), Path.vtaskPure(), Path.vtaskFail()
  • Understanding the relationship between VTask and VTaskPath
  • Converting between VTask and VTaskPath

Key insight: VTaskPath wraps VTask to provide a fluent, composable API that integrates with the broader Effect Path ecosystem.

Exercise 1: Create a VTaskPath from a computation Exercise 2: Create pure and failing VTaskPaths


Part 2: Fluent Composition

What you'll learn:

  • Chaining operations with via() (the Effect Path equivalent of flatMap)
  • Transforming values with map()
  • Debugging with peek()
  • Sequencing with then()

Key insight: VTaskPath operations mirror VTask operations but use Effect Path naming conventions (via instead of flatMap) for consistency across all Path types.

Exercise 3: Chain VTaskPath operations with via Exercise 4: Use peek for debugging a computation pipeline


Part 3: Error Handling and Timeouts

What you'll learn:

  • Safe execution with runSafe() returning Try
  • Recovery patterns: handleError(), handleErrorWith()
  • Timeout management with timeout(Duration)
  • Building fallback chains

Key insight: Timeouts integrate naturally into the fluent API. A timeout that expires returns a failure that can be recovered from, just like any other error.

Exercise 5: Add a timeout to a slow operation Exercise 6: Build a fallback chain with multiple recovery attempts


Part 4: Real-World Patterns

What you'll learn:

  • Service aggregation with parallel fetches
  • Dashboard-style data loading
  • Graceful degradation under failure

Key insight: Combining VTaskPath with Par combinators gives you the best of both worlds: fluent composition for individual paths and parallel execution for independent operations.

Exercise 7: Aggregate data from multiple services in parallel Exercise 8: Build a resilient dashboard loader with timeouts and fallbacks


Running the Tutorials

Using Gradle

# Run all VTask exercises
./gradlew :hkj-examples:test --tests "*TutorialVTask*"

# Run VTaskPath exercises
./gradlew :hkj-examples:test --tests "*TutorialVTaskPath*"

# Check your solutions pass
./gradlew :hkj-examples:test --tests "*TutorialVTask*_Solution*"
./gradlew :hkj-examples:test --tests "*TutorialVTaskPath*_Solution*"

Using Your IDE

Navigate to hkj-examples/src/test/java/org/higherkindedj/tutorial/concurrency/ and run the tests.


Common Pitfalls

1. Forgetting VTask is Lazy

Problem: Assuming a VTask runs when created.

// WRONG: This doesn't print anything yet!
VTask<Unit> task = VTask.exec(() -> System.out.println("Hello"));

// RIGHT: You must run it
task.run(); // Now it prints

2. Using run() Instead of runSafe()

Problem: Using run() and letting exceptions propagate.

// RISKY: Throws on failure
Integer result = VTask.of(() -> dangerousOperation()).run();

// BETTER: Captures failure in Try
Try<Integer> result = VTask.of(() -> dangerousOperation()).runSafe();
result.foldFailureFirst(
    error -> handleError(error),
    value -> handleSuccess(value)
);

3. Sequential When Parallel Would Work

Problem: Using flatMap for independent computations.

// SEQUENTIAL: fetchB waits for fetchA to complete
VTask<Pair> result = fetchA.flatMap(a ->
    fetchB.map(b -> new Pair(a, b)));

// PARALLEL: Both execute simultaneously
VTask<Pair> result = Par.map2(fetchA, fetchB, Pair::new);

4. Ignoring Timeouts in Production

Problem: Letting slow operations block indefinitely.

// RISKY: No timeout, could hang forever
VTaskPath<Data> path = Path.vtask(() -> fetchFromSlowService());

// BETTER: Add timeout with fallback
VTaskPath<Data> safe = Path.vtask(() -> fetchFromSlowService())
    .timeout(Duration.ofSeconds(5))
    .handleError(e -> Data.empty());

Key Concepts Summary

ConceptDescription
LazinessVTask/VTaskPath describes computation; nothing runs until run()/runSafe()/runAsync()
Virtual ThreadsExecution happens on lightweight JVM-managed threads
Structured ConcurrencyPar combinators ensure proper task lifecycle management
Error PropagationFailures short-circuit chains; use recover (VTask) or handleError (VTaskPath)
TimeoutsUse timeout(Duration) to prevent runaway operations
Effect Path IntegrationVTaskPath integrates with other Path types and ForPath comprehensions

What's Next?

After completing this journey:

  1. Continue to Scope & Resource: Learn structured concurrency and resource management → Scope & Resource Journey
  2. Read the VTask Documentation: Deep dive into all VTask operations → VTask Guide
  3. Explore VTaskPath Documentation: See the full VTaskPath API → VTaskPath Guide
  4. Learn Effect Contexts: Use VTaskContext for dependency injection → Effect Contexts

Previous: Effect API Next: Scope & Resource

Related: VTask Guide | VTaskPath Guide | Monad Guide | Effect Path Overview