Skip to content

Retry and cache

Each step reference in a use case can carry settings. Settings apply only to that use case, so the same step can be retried in one use case and not in another.

steps:
  - step: saveOrder
    settings:
      retry:
        max-retries: 3
        delay: 100ms
      cache:
        name: orders
        key: "#input.id"

When both are set, the cache is checked first. A cache hit skips the step and its retries.

flowchart LR
    input([input]) --> hit{cache hit?}
    hit -- yes --> out([cached output])
    hit -- no --> step[run step]
    step -- fails --> retry{retries left?}
    retry -- yes --> step
    retry -- no --> err([exception])
    step -- succeeds --> store[store in cache] --> result([output])

Retry

Retry uses Spring Framework's core retry support (RetryTemplate).

Property Default Description
max-retries 3 Retries after the first attempt
delay 1s Delay before the first retry
multiplier 1.0 Factor applied to the delay after each retry
max-delay unlimited Upper bound for the delay
jitter 0 Random jitter added to each delay
timeout 0 (none) Maximum total time spent retrying
includes all Exception classes that trigger a retry
excludes none Exception classes that never trigger a retry

When all retries fail, the last exception thrown by the step reaches the caller as it is.

Retry can also be declared on the step itself. YAML settings take precedence:

@Step(retry = Retryable(maxRetries = 2, delay = 100))
fun saveOrder(request: OrderRequest): OrderEntity { /* ... */ }
@Step(retry = @Retryable(maxRetries = 2, delay = 100))
public OrderEntity saveOrder(OrderRequest request) { /* ... */ }

Cache

Cache uses Spring's cache abstraction, so any CacheManager works (Caffeine, Redis, ...).

Property Required Description
name yes Cache name in the application's CacheManager
key no SpEL expression over the step's inputs. Defaults to the inputs themselves (which then need proper equals/hashCode).

Each input is readable by its parameter name, and #input names the only one of a step taking a single parameter:

- step: saveOrder
  settings:
    cache: { name: orders, key: "#input.id" } # (1)!

- step: quoteShipping
  in: { order: $saveOrder, country: $input }
  settings:
    cache: { name: quotes, key: "#order.weight + '-' + #country" } # (2)!
  1. saveOrder(request: OrderRequest) takes one parameter, so #input is that request.
  2. quoteShipping(order: OrderEntity, country: String) takes two, so each is named. #input does not exist here.

Without a key, a step taking one input is keyed by it, and a step taking several by all of its arguments together.

Cache can also be declared on the step itself. YAML settings take precedence:

@Step(cache = Cached(name = "orders"))
fun findOrder(id: UUID): OrderEntity { /* ... */ }
@Step(cache = @Cached(name = "orders"))
public OrderEntity findOrder(UUID id) { /* ... */ }

Warning

Don't combine this with Spring's @Cacheable on the same method. Both would cache the result, each with its own key.

The application needs a CacheManager bean. With Spring Boot, add spring-boot-starter-cache and @EnableCaching. If a step uses a cache and no CacheManager exists, or the cache name is unknown, startup fails.

Note

Enact reads from and writes to the cache but never evicts entries. Configure expiry in your cache provider, or evict entries yourself through the CacheManager when the underlying data changes. The demo's cancelOrder step does this:

@Step
fun cancelOrder(id: UUID) {
    requireNotNull(orders.remove(id)) { "Order $id not found." }
    cacheManager.getCache("orders")?.evict(id)
}