Where to Start
"Would you tell me, please, which way I ought to go from here?"
"That depends a good deal on where you want to get to," said the Cat.
-- Lewis Carroll, Alice's Adventures in Wonderland
Alice's reply was that she did not much care where, at which point the Cat told her any road would do. This page is built on the opposite assumption: that you do care, that you already have a problem in front of you, and that what you need is the shortest route to the chapter that solves it.
- Which tool fits the problem you have, before you know the library's vocabulary
- How the four tool families combine when one is not enough
- Where to go next once you have picked your direction
The one question
What are you actually trying to do?
Pick the branch that matches. If two match, read both and combine them; the Combining tools section below covers the common overlaps.
- I'm working with values that might fail or be absent
- I'm reading or updating data inside immutable records
- I'm doing concurrent, async, or deferred I/O
- I'm sequencing several effects together
- I'm writing library or polymorphic code that others will compose with
1. Values that might fail or be absent
You have a function that may return nothing, throw, or report a domain error. You want chaining, short-circuiting, and a clear extraction point.
| Your situation | Reach for |
|---|---|
| Value may be missing; the reason does not matter | MaybePath |
Bridging to Java's Optional | OptionalPath |
| Failure carries a typed domain error you control | EitherPath |
| Failure comes from code that throws exceptions | TryPath |
| You want all validation errors, not just the first | ValidationPath |
If you are not sure between EitherPath and TryPath, ask: would I want to
unit-test the error branch? If yes, EitherPath. If the error is genuinely
exceptional (file system blew up), TryPath is honest about it.
Go to: Effect Path Quickstart · Core Paths · full Path decision tree
2. Reading or updating nested data
You have records (often deeply nested), sealed hierarchies, or collections, and you want to read or update one field without rebuilding the surrounding structure by hand.
| Your situation | Reach for |
|---|---|
| Required field on a record, get/set | Lens, via Focus DSL |
| Optional field, or a field that may be absent | Affine / AffinePath |
| One variant of a sealed type | Prism |
| Every element of a collection | Traversal |
| Two equivalent representations | Iso |
| The type isn't yours to annotate | @ImportOptics or an OpticsSpec |
In practice almost everyone starts with the Focus DSL
and annotations like @GenerateLenses, @GenerateFocus, @GeneratePrisms;
hand-written optic composition is rarely needed.
Go to: Optics Quickstart · full Optics decision trees
3. Concurrent, async, or deferred I/O
You are calling APIs, talking to a database, reading files, fanning out work across threads, and you want a sane composition story.
| Your situation | Reach for |
|---|---|
| Defer a side effect; do not run it until you ask | IOPath |
| Concurrent work, virtual threads, structured concurrency | VTaskPath |
| Lazy pull-based streaming, possibly infinite | VStreamPath |
Existing CompletableFuture<T> returns you must integrate with | CompletableFuturePath |
| Memoise a deferred computation | LazyPath |
| Deeply recursive algorithm that would blow the stack | TrampolinePath |
If you are starting a new service from scratch on Java 21+, default to
VTaskPath; it gives you virtual-thread
concurrency without forcing the rest of your code reactive.
Go to: VTask Monad · Structured Concurrency
4. Sequencing several effects
You have more than one of the above to do, in order. Two answers, depending on how regular the pipeline is.
| Your situation | Reach for |
|---|---|
| A linear pipeline: do A, then B with A's result, then C | The Path's own .flatMap / .map chain |
| A pipeline that pulls from several independent sources | ForPath comprehension |
| Independent steps you want to run in parallel | ForPath parallel |
| The same operation across a collection | ForPath traverse |
| A DSL you want to interpret in multiple ways (prod, audit, dry-run) | FreePath and Effect Handlers |
ForPath is the answer most of the time once a pipeline has more than two
steps and one of them needs a name. It reads top-to-bottom and removes the
nesting that flatMap chains accumulate.
Go to: For Comprehension · Composition Patterns
5. Library or polymorphic code
You are not writing application code; you are writing a function that other teams will compose into their effect stack. The Path API fixes the outer effect at the call site, which is wrong for this case.
| Your situation | Reach for |
|---|---|
| You need to read configuration without fixing the caller's effect | MonadReader |
| You need to read and modify state polymorphically | MonadState |
| You need to record an audit log polymorphically | MonadWriter |
| You need to combine several of the above | MTL Combining Capabilities |
Bridging to a third-party type like Mono<Either<E, A>> | EitherT directly |
If you are not in one of these cases, you do not need the transformer chapter. The full reasoning lives in Path or Transformer?.
Go to: MTL Capabilities · Transformers Quickstart
Combining tools
The four families overlap. Here are the five combinations that come up most often in real code, each with the right entry point.
Validation and nested update
You need to update a field inside a record, but the update can fail.
- Tools: Focus DSL (to navigate) +
EitherPathorValidationPath(to carry the error). - API: Call
.focus(lens)on the Path, orlens.toEitherPath()from Focus. - Read: Optics Integration · Capstone: Effects Meet Optics · Tutorial14_FocusEffectBridge
Async and typed errors
You need to call an async API that can return a domain error before the result is materialised.
- Tools:
VTaskPathfor the async,EitherPathfor the error, composed throughForPath. For polymorphic code or third-partyMono<Either<...>>shapes, drop toEitherT<VTaskKind.Witness, E, A>(EitherT). - Read: Composition Patterns · Path or Transformer?
Async and resilience (retry, circuit breaker, bulkhead, saga)
You have async or deferred work and you need to make it survive partial failure.
- Tools:
VTaskPathorIOPathunderneath the resilience combinators. - Read: Resilience Patterns · Retry · Saga
Pure logic and I/O at the edges
You want a functional core (pure, testable, no effects) and an imperative shell that runs the effects.
- Tools: Plain data types in the core,
IOPath/VTaskPathat the boundary,.unsafeRun()only at the entry point (main, controller, test). - Read: the hkj-arch skill, Effect Boundary Integration.
A DSL and multiple interpreters
You want to describe a workflow once and run it differently in production, tests, audits, or dry-runs.
- Tools:
FreePathfor the description,@EffectAlgebrafor the vocabulary, interpreters for each run mode. - Read: Effect Handlers · Free Monad · the hkj-effects skill.
Escape hatches
If none of the above fits, you are in one of these situations:
- A custom monad with no Path. Use
GenericPath<F, A>given aMonad<F>instance. - You are extending the library itself with a new HKT. Read Extending and the 0.3.0 migration guide for the witness arity contract.
- You are stuck on a compiler error and the type doesn't make sense. See Common Compiler Errors for effects, Optics Compiler Errors for optics, or Transformer Errors for transformers.
Quick reference
| If you're doing this... | Start here |
|---|---|
| Absent values | MaybePath |
| Typed domain errors | EitherPath |
| Wrapping throwing code | TryPath |
| Accumulating validation errors | ValidationPath |
| Deferred side effect | IOPath |
| Async on virtual threads | VTaskPath |
| Lazy streaming | VStreamPath |
| Reading or updating one nested field | Focus DSL |
| Sealed-type variant | Prism |
| Every element of a collection | Traversal |
| Linear pipeline | .flatMap on the Path |
| Pipeline pulling from several sources | ForPath |
| Validation + nested update | Optics Integration |
| Async + typed errors | Composition Patterns |
| Resilience around async work | Resilience Patterns |
| Polymorphic library function | MTL Capabilities |
| Third-party outer effect | EitherT |
| DSL with multiple interpreters | FreePath + Effect Handlers |
Start with the Quickstart. Most production workflows use one or two Paths and a Focus DSL, and that combination is enough to ship. Reach for transformers, MTL, or Free monads only when one of the signals on this page actually fires.
Previous: Cheat Sheet Next: Effect Path API