# Resilience

Make an action survive slow or failing dependencies with retries, a timeout and a fallback.

An action gets resilience from three places. `@Resilience` declares retry and timeout. `@Fallback` names the method that runs when the action still fails. `ResilienceExecutor` wraps any operation of your own, outside an action.

## What `@Resilience` applies

`@Resilience` goes on an action method or on the role class. A method-level annotation replaces the class-level one as a whole. Fields are not merged.

| Field | Applied on an action | Notes |
|---|---|---|
| `retry` | Yes | Runs when `retry.maxAttempts` is above 0. |
| `timeout` | Yes | Milliseconds. 0 means no timeout. |
| `circuitBreaker` | No | Parsed only. Every action gets the default breaker below. |
| `rateLimit` | No | Parsed only. |
| `bulkhead`, `maxConcurrent` | No | Parsed only. |
| `fallback` | No | Parsed only. Use `@Fallback` instead. |

If neither retry nor timeout is set, the action runs unwrapped.

When retry or timeout is set, the action also runs behind a circuit breaker keyed by the action name. Its settings are fixed:

- It opens at a 50% failure rate.
- It counts the last 20 calls, and waits for at least 10.
- It stays open for 15 seconds.

## Retry an action and fall back

`fetchQuote` retries up to three times on `IOException`, with each attempt bounded by a 5 second timeout. If every attempt fails, `cachedQuote` returns a stand-in value.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ResilientQuoteRole.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.Fallback;
import com.tnsai.annotations.Resilience;
import com.tnsai.enums.ActionType;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

import java.io.IOException;

public final class ResilientQuoteRole extends Role {

    @Override
    public RoleIdentity getIdentity() {
        return new RoleIdentity(
                "quotes",
                "Fetch prices from an upstream quote service",
                "market-data");
    }

    @ActionSpec(
            type = ActionType.LOCAL,
            description = "Fetch the latest price for a ticker symbol")
    @Resilience(
            retry = @Resilience.Retry(
                    maxAttempts = 3,
                    backoffMs = 500,
                    retryOn = IOException.class),
            timeout = 5000)
    public String fetchQuote(String symbol) throws IOException {
        return callUpstream(symbol);
    }

    @Fallback(forAction = "fetchQuote")
    public String cachedQuote(String symbol, Exception error) {
        return symbol + ": price unavailable";
    }

    private String callUpstream(String symbol) throws IOException {
        throw new IOException("quote service unreachable");
    }
}
```

`@Resilience.Retry` has these fields:

| Field | Default | Meaning |
|---|---|---|
| `maxAttempts` | `0` | Total attempts. 0 turns retry off. |
| `backoffMs` | `1000` | Delay before the second attempt. |
| `multiplier` | `2.0` | Growth factor for each later delay. |
| `maxBackoffMs` | `30000` | Upper bound on one delay. |
| `retryOn` | all | Exception types that trigger a retry. `Error` types are ignored. |
| `noRetryOn` | none | Exception types that never retry. `Error` types are ignored. |
| `jitter` | `true` | Adds a random 0 to 25% to each delay. |

## Choose the fallback method

`@Fallback` marks a method on the same role. The framework calls it after the action and its retries have failed.

- `@Fallback(forAction = "fetchQuote")` handles one action.
- `@Fallback` with no `forAction` handles every action on the role that has no fallback of its own.
- The method takes the action's parameters in declaration order, then an `Exception` or `Throwable`.
- `maxRetries` reruns the action that many times, with no delay, before the fallback runs. The default is 0.

## Set resilience on a programmatic role

A role built with `RoleBuilder` has no annotated methods. Give it `RoleBuilder.resilience(ResilienceConfig)` instead:

<!-- java-contract: src/main/java/com/example/tnsai/docs/ResilienceRoleBuilderExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.resilience.ResilienceConfig;
import com.tnsai.resilience.RetryPolicy;
import com.tnsai.roles.Role;
import com.tnsai.roles.RoleBuilder;

public final class ResilienceRoleBuilderExample {
    private ResilienceRoleBuilderExample() {}

    public static Role quotesRole() {
        return RoleBuilder.create()
                .name("quotes")
                .resilience(ResilienceConfig.builder()
                        .retry(RetryPolicy.builder().maxAttempts(3).build())
                        .timeoutMs(5000)
                        .build())
                .build();
    }
}
```

The config carries retry and timeout only. It applies when no `@Resilience` annotation does.

## Wrap your own operation

`ResilienceExecutor` runs any `Callable` with the full set of layers. Use it for calls you make outside an action.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ResilienceExecutorExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.resilience.ResilienceExecutor;
import com.tnsai.resilience.ResilienceRequest;
import com.tnsai.resilience.RetryPolicy;
import com.tnsai.resilience.dlq.DeadLetterEntry;
import com.tnsai.resilience.dlq.InMemoryDeadLetterQueue;

import java.time.Duration;

public final class ResilienceExecutorExample {
    private ResilienceExecutorExample() {}

    public static String fetchWithLimits(String symbol) throws Exception {
        InMemoryDeadLetterQueue deadLetters = new InMemoryDeadLetterQueue();
        ResilienceExecutor executor = new ResilienceExecutor(deadLetters);

        ResilienceRequest<String> request = ResilienceRequest.<String>builder()
                .operationId("fetch-quote")
                .operation(() -> callUpstream(symbol))
                .retryPolicy(RetryPolicy.builder()
                        .maxAttempts(3)
                        .initialBackoff(Duration.ofMillis(500))
                        .build())
                .fallbackStrategy(error -> symbol + ": price unavailable")
                .rateLimit(20, 1000)
                .bulkhead(5)
                .timeout(5000)
                .build();

        String result = executor.execute(request);

        for (DeadLetterEntry entry : deadLetters.getEntries("fetch-quote")) {
            System.out.println(entry.getExceptionType() + ": " + entry.getExceptionMessage());
        }
        return result;
    }

    private static String callUpstream(String symbol) {
        throw new IllegalStateException("quote service unreachable");
    }
}
```

The layers wrap each other from outside in:

1. Timeout
2. Rate limit
3. Bulkhead
4. Circuit breaker
5. Retry
6. The operation

Every failure goes to the dead-letter queue, including a failure that the fallback then recovers. The `fallbackStrategy` handles ordinary exceptions only. These rejections are thrown to the caller unchanged:

| Rejection | Exception |
|---|---|
| Timeout | `com.tnsai.exceptions.TimeoutException` |
| Circuit breaker open | `io.github.resilience4j.circuitbreaker.CallNotPermittedException` |
| Bulkhead full | `io.github.resilience4j.bulkhead.BulkheadFullException` |
| Rate limit reached | `io.github.resilience4j.ratelimiter.RequestNotPermitted` |

Each `DeadLetterEntry` has `getOperationId()`, `getExceptionType()`, `getExceptionMessage()`, `getTimestamp()` and `getSpec()`. `getSpec()` returns the request metadata plus `operationId` and `failureType`.

## Related

- [Actions](actions.md): where resilience runs in the action pipeline.
- [Exceptions](exceptions.md): the error categories an action can raise.
- [Long runs](long-runs.md): progress timeouts and abort reasons for long work.
- [Trace, meter and fall back](../tutorials/trace-meter-fallback.md): a full tutorial with a fallback.
