Feature request / Enhancement

Background

When configuring maximum retry attempts in Spring Framework (RetryPolicy, RetryTemplate), the maxAttempts parameter is currently documented and implemented as the "total number of executions (including the first attempt)". This is supposed to match the definition in spring-retry, but the way it is explained in spring-retry is more direct and user-friendly. Furthermore, in recent @spring-projects/spring-framework tests, its behavior appears inconsistent with the documentation and with spring-retry itself.

How it works in spring-retry

MaxAttemptsRetryPolicy:

"Includes the initial attempt before the retries begin so, generally, will be >= 1. For example setting this property to 3 means 3 attempts total (initial + 2 retries)."

Javadoc and parameter descriptions for maxAttempts in spring-retry make it clear: setting to 3 means 3 total attempts (1 initial + 2 retries). This is incredibly clear for users ('don't make me think').

How it's defined and tested in spring-framework

Documentation Reference: The docs in Spring Framework 7.0 clearly state that the initial attempt plus maxAttempts - 1 retries is the intended behavior:

framework-docs/modules/ROOT/pages/core/resilience.adoc

Recent tests (see link above) indicate the initial call is not counted towards maxAttempts—meaning that if you set maxAttempts = 3, you may get the initial attempt + 3 more as retries (total of 4). This is not consistent with the spring-retry definition and can lead to off-by-one confusion for users.

Proposal

  • Goal: Unify semantics so that the initial attempt IS INCLUDED in maxAttempts for both modules—so a value of N means N total tries (the initial attempt + N-1 retries).
  • Update docs, javadocs, examples, and definition so that it matches the more user-friendly approach of spring-retry.
  • Review and (if needed) update tests and implementation to avoid confusion and ensure the initial call is counted as one of the maxAttempts.
  • Add code comments and examples highlighting this for future contributors.

References


If possible, please harmonize the documentation, implementation, and user experience between Spring Framework retry and spring-retry. The initial attempt should count towards maxAttempts (as in spring-retry), making it easier for users and developers to reason about behavior.

Comment From: sbrannen

Hi @dol,

Congratulations on submitting your first issue for the Spring Framework. 👍

I realize there can be some confusion between "max attempts" and "max retry attempts", but before we proceed I'd like to ask a few questions.

When configuring maximum retry attempts in Spring Framework (RetryPolicy, RetryTemplate), the maxAttempts parameter is currently documented and implemented as the "total number of executions (including the first attempt)".

I cannot seem to find any text like that.

Can you please provide a link to that documentation?

Furthermore, in recent @spring-projects/spring-framework tests, its behavior appears inconsistent with the documentation

Can you please point to a specific test which demonstrates that?

How it's defined and tested in spring-framework

Documentation Reference: The docs in Spring Framework 7.0 clearly state that the initial attempt plus maxAttempts - 1 retries is the intended behavior:

I also cannot find any documentation which states that.

Can you please expound on what led you to interpret the documentation like that?

Recent tests (see link above) indicate the initial call is not counted towards maxAttempts—meaning that if you set maxAttempts = 3, you may get the initial attempt + 3 more as retries (total of 4).

That is correct.

This is not consistent with the spring-retry definition and can lead to off-by-one confusion for users.

That is also correct.

The behavior in Spring Retry and Core Retry (in the Spring Framework) differ in that regard.

  • Spring Retry: talks about max "attempts"
  • Core Retry: talks about max "retry attempts" (even though the configuration is named maxAttempts)

As far as I know, the documentation in Spring Framework is consistent in that regard. Although, the wording may not be as clear as it could be.

The initial attempt should count towards maxAttempts (as in spring-retry), making it easier for users and developers to reason about behavior.

Again, I understand the confusion between "max attempts" and "max retry attempts", so we'll discuss this again within the team.

In the interim, however, I would appreciate it if you could answer the questions I raised above.

Thanks

Comment From: jhoeller

Note that in terms of semantic alignment, we are not primarily targeting Spring Retry (or Resilience4J for that matter) but rather the MicroProfile @Retry annotation, the Micronaut @Retryable annotation, and Reactor's RetryBackoffSpec (which we directly integrate with for reactive methods annotated with Spring's @Retryable). All of the latter interpret the specified number of attempts as retry attempts in addition to the initial invocation, and the common annotation default is a maximum of 3 such retry attempts.

FWIW, both specifying the number of retry attempts as well as the default of 3 such attempts in addition to the original call is common on other platforms as well, such as on .NET (https://www.pollydocs.org/strategies/retry.html). Arguably this is common industry consensus for similar programming model features that we are aligning with here, or at least common enough in the industry for us to adopt that approach as well.

That said, we could offer both variants side by side, for example as retryAttempts and totalAttempts attributes (with only one of them to be set) on the annotation. Even then, the remaining question is whether the default would be 3 retry attempts or 3 total attempts. In some way, this would make the annotation harder to understand, so I'm not sure this would actually be helpful.

Comment From: sbrannen

Another proposal...

We could rename maxAttempts to maxRetries in both the @Retryable annotation and RetryPolicy builder, and the static factory method for the policy would be RetryPolicy.withMaxRetries().

By doing that, we avoid any confusion regarding which "attempts" we are referring to.