This version is still in development and is not considered stable yet.

Error Handling

Pipelite offers three mutually-exclusive error handling modes at the flow level: retry, a custom error channel, and a fully custom exception handler. Exactly one is configured via the DSL at the end of the flow definition - each returns a terminal builder step, so none can be chained with another (a custom ExceptionHandler configured alongside a retry or an error channel is a compile error, not a silently-ignored combination).

Retry

When an exception propagates through the flow, the framework serializes the Exchange into a FlowExecutionDump and re-queues it for a subsequent attempt, resuming at the step that failed rather than replaying the flow from its source.

An exhaustion action is mandatory: the withRetry(…​) lambda only type-checks once it ends in onErrorChannel(…​) or onExceptionHandler(…​) - a retry with no defined outcome once attempts run out cannot be expressed.

import io.pipelite.dsl.definition.builder.Backoff;
import java.time.Duration;

FlowDefinition flow = Pipelite.defineFlow("resilient-flow")
    .fromSource("kafka://my-topic")
    .process("parse", (exchange, contribution) -> {
        // may throw exceptions
    })
    .toSink("slf4j://output")
    .withRetry(retry -> retry
        .maxAttempts(3)                                          (1)
        .backoff(Backoff.exponential(Duration.ofSeconds(2)))     (2)
        .onErrorChannel(err -> err.toDLQ()))                     (3)
    .build();
1 Up to 3 attempts total (the original send plus 2 retries). Optional - with no maxAttempts, only the framework’s own retry-channel polling cadence paces attempts.
2 The delay before each attempt, counted from the previous one. Backoff.linear(…​) grows by a constant step (2s, 4s, 6s, …​); Backoff.exponential(…​) doubles it each time (2s, 4s, 8s, …​). Optional - with no backoff declared, a retry runs as soon as the retry channel next polls.
3 The exhaustion action: here, the built-in dead letter queue (see Custom Error Channel). onExceptionHandler(handler) invokes a supplied ExceptionHandler instead of routing anywhere.

Custom Error Channel

For error handling with no retry attempts - a single hand-off, not a schedule of them - use withErrorChannel, or onErrorChannel(…​) as a retry’s own exhaustion action (above):

FlowDefinition flow = Pipelite.defineFlow("flow-with-error-channel")
    .fromSource("http://api/v1/ingest")
    .process("process", (exchange, contribution) -> { /* ... */ })
    .toSink("slf4j://output")
    .withErrorChannel(err -> err.toChannel("queue://ingest-dead-letter"))   (1)
    .build();
1 The target is a URL, like every destination in the DSL: queue://<queue name> for an internal flow (the queue its fromSource("queue://…​") reads, not the flow’s own name), or a registered channel adapter’s protocol (kafka://…​) for an external system. A bare name is rejected when the flow is defined.

err.toDLQ() targets the framework’s built-in dead letter queue instead, with no flow to write.

Custom Exception Handler

For anything the two options above don’t cover, withExceptionHandler(handler) hands the exception straight to a user-supplied ExceptionHandler, with no dump, no retry, and no routing of any kind - the handler decides everything.

Default handler

If none of the three is configured, the exception is handled by GlobalDefaultExceptionHandler, which logs the error and stops processing without propagating the exception to the caller.

FlowExecutionDump

The FlowExecutionDump is the serialization of the Exchange at the time of failure. It contains the serialized payload, headers, and properties required for the retry.

The default FlowExecutionDumpRepository is already durable - a pending retry is written to disk before the flow moves on, and survives a restart. Swap it via ConfigurablePipeliteContext#setFlowExecutionDumpRepository(…​) for a different backend (e.g. a database or Redis) or to opt back into FlowExecutionDumpInMemoryRepository.