The engine

IdempotencyEngine.execute without Spring - building a context, the two overloads, and CompletionFailurePolicy.

IdempotencyEngine.execute is the entire API. It acquires the lease, runs the action with a heartbeat, encodes and records the result, releases the lease if the action throws, and fires the lifecycle callbacks around all of it. What comes back is a sealed Outcome you switch on.

The engine has no framework or transport types in it, so you can drive it directly. Everything else in the library is an adapter over this.

Building a context

The engine needs a store - any IdempotencyStore - and a ScheduledExecutorService it runs heartbeats on. Share one scheduler across the application rather than creating one per call.

ScheduledExecutorService scheduler =
        Executors.newSingleThreadScheduledExecutor();
IdempotencyEngine engine = new IdempotencyEngine(store, scheduler);

IdempotencyContext context = IdempotencyContext
        .builder("ShipmentListener.onOrderShipped", event.id())
        .ttl(Duration.ofHours(24))
        .leaseDuration(Duration.ofSeconds(30))
        .waitTimeout(Duration.ZERO)   // decline instead of parking the consumer thread
        .build();
Java

The builder takes the scope and key pair. Here event is the message being handled, so its id is the key. Outside Spring there is no method name to derive a scope from, so you name it - and naming it after the handler keeps it stable when the method moves.

Add .fingerprint(sha256Hex) when the payload is worth guarding against key reuse. See fingerprints.

The runnable overload

A caller with nothing for a duplicate to replay uses the runnable overload. Below, handler.handle(event) is the work being guarded, and consumer.nack stands for however your broker client asks for a redelivery:

switch (engine.execute(context, () -> handler.handle(event))) {
    case Outcome.Executed<Void> ignored -> { /* ran for the first time */ }
    case Outcome.Replayed<Void> ignored -> { /* already handled under this key */ }
    case Outcome.InFlight<Void> inFlight ->
            consumer.nack(inFlight.retryAfter());   // someone else has it; redeliver later
}
Java

The codec overload

When a duplicate should get a real result back, pass a PayloadCodec<T> for whatever the action returns. Here handler.handle(event) returns a Handled of your own, and codec is a PayloadCodec<Handled>:

Outcome<Handled> outcome =
        engine.execute(context, () -> handler.handle(event), codec);
Java

Outcome.Replayed carries the decoded value from the original execution, so the same switch handles a first run and a duplicate without the caller knowing which it got. Writing the codec is covered in payloads and codecs.

When the store refuses the completion

The action ran and its side effects are durable, so CompletionFailurePolicy decides what happens next.

  • PROPAGATE - the engine’s own default. Rethrows the storage failure.
  • LOG_AND_RETURN - logs it and returns Executed with the value anyway, which is what an HTTP adapter wants: the response the handler produced should still reach the client.

The lease is not released either way; the record stays in progress until its lease expires, and a retry after that re-executes.

Joined completion is the exception: its completion failures always propagate, whatever the policy says, because returning normally would let the transaction commit the business writes without the record. When that transaction rolls back, the engine releases the lease, so the key is retryable at once instead of after the lease expires.

IdempotencyEngine engine = new IdempotencyEngine(
        store,
        scheduler,
        List.of(),   // lifecycle listeners, none here
        IdempotencyConfig.builder()
                .completionFailurePolicy(CompletionFailurePolicy.LOG_AND_RETURN)
                .build());
Java

Listeners outside Spring

Pass lifecycle listeners as the third argument. Here auditListener is an IdempotencyLifecycleListener of your own:

IdempotencyEngine engine = new IdempotencyEngine(store, scheduler, List.of(auditListener));
Java