Skip to content

Choose a strategy

Three strategies, and the right one is decided by your posture and your provider — not by this package. Three questions settle it.

The three questions

1. Does the value refresh itself?

An aws.Config from LoadDefaultConfig renews credentials internally. So does an azcore.TokenCredential, and a Google cached token provider. A bare token string — VAULT_TOKEN, a Consul ACL token, a forge API token — does not.

2. Are you willing to hold a credential between operations?

A signing service may deliberately decline to, so that credentials are not resident in memory longer than a mint requires. That is a security posture, and it is yours to choose even when the value would be perfectly safe to hold.

3. Is building it expensive?

A full credential-chain resolution reaching a metadata service is expensive. A struct literal is not.

The answers

Refreshes itself? Willing to hold? Strategy
yes yes Memoised
yes no PerCall
no Invalidatable if building is expensive, PerCall if it is cheap
// The value renews itself and you are happy to hold it.
src := clientlifecycle.Memoised(build)

// You decline to hold a credential between operations.
src := clientlifecycle.PerCall(build)

// The value cannot renew itself, and rebuilding it every time is too expensive.
src := clientlifecycle.Invalidatable(build)

All three satisfy Resolver[T], so switching posture is a one-word change at the construction site and nothing else. Invalidatable additionally satisfies Invalidator[T].

Two mistakes worth naming

Reaching for PerCall because the value goes stale. If the value cannot renew itself, PerCall is correct but may be ruinous: you pay a full resolution on every operation. That is the gap Invalidatable exists to fill — hold and reuse, then re-resolve when told. Only take PerCall for a stale-able value when building it is cheap.

Reaching for Invalidatable because it sounds strictly better. It is not. If your value refreshes itself, Memoised is correct and Invalidatable is worse: it adds an invalidation path nobody should ever call, and if somebody does call it wrongly you get the fan-out described in invalidating a stale credential. Take the weakest strategy that is correct.

Bounding and scoping

Every strategy takes the same options.

// Bound one attempt. Per attempt, not a total budget.
clientlifecycle.Memoised(build, clientlifecycle.WithBuildTimeout(5*time.Second))

// Tie attempts to the life of whatever owns the resolver.
clientlifecycle.Memoised(build, clientlifecycle.WithLifetimeContext(appCtx))

WithLifetimeContext is deliberately not a call's context: a caller's cancellation must release that caller alone, while this is the separate, longer-lived scope that says "this whole source is finished". A resolver whose lifetime context is done refuses to build at all — including after an invalidation, however many times it has been invalidated.

PerCall ignores it, being already scoped to its caller.