Wednesday 02:17 UTC. The loyalty partner API returns 503 on 94% of calls — a certificate rotation gone wrong on their side. Your checkout service still retries every failure three times because maxAttempts: 3 shipped in application-prod.yml eighteen months ago. Outbound RPS to the partner climbs to 12,000 while inbound checkout traffic is only 1,400. The partner SRE pings your bridge: "You're DDoSing us with retries."
The checkout lead scrolls Resilience4j config:
resilience4j.retry:
instances:
loyaltyPartner:
maxAttempts: 3
waitDuration: 200ms
Cutting retries for loyalty only — while tax and shipping stay at three — needs a deploy. The incident commander says what the room already knows:
"Retry budget is operational. Why is one failing dependency taking the whole fleet with it?"
Kiponos.io holds per-dependency max_retries in a live tree: local reads before every outbound call, afterValueChanged to rebuild Resilience4j retry policies without pod recycle.
The problem — max_retries baked into static config
Checkout calls three partners on the payment path. Each uses @Retry with frozen YAML:
@Retry(name = "loyaltyPartner")
public LoyaltyPoints accrueLoyalty(CheckoutRequest req) {
return loyaltyClient.accrue(req);
}
resilience4j.retry:
instances:
loyaltyPartner:
maxAttempts: 3
taxService:
maxAttempts: 3
shippingQuotes:
maxAttempts: 2
During a partner outage, you need loyaltyPartner.max_retries: 0 now — not after a merge queue and rolling restart. Global retry disable is worse: tax and shipping are healthy. Static YAML forces an all-or-nothing deploy.
What teams believe vs production reality
| Belief | Production reality |
|---|---|
| "Retries are resilience — more is safer" | Retries multiply load on a failing dependency |
| "Resilience4j YAML is good enough" | YAML is per-deploy, not per-incident per-dependency |
| "Circuit breaker will stop retries" | Breaker opens after retry storm already landed |
| "We will scale pods to absorb retries" | Partner sees N × pods × maxAttempts — not your problem alone |
| "Idempotent POSTs mean retry freely" | Partner outage + retry storm = extended mutual pain |
The Aha
max_retries is operational config — it changes per dependency during outages, peaks, and brownouts. It belongs in a live tree checkout reads with getInt() before every @Retry invocation, not in Resilience4j YAML frozen at boot.
What Kiponos.io is for retry budgets
Profile ['checkout']['prod']['retry'] hydrates per-dependency retry limits into every checkout pod. Dashboard edit on retry/loyalty_partner/max_retries sends a delta; the next accrueLoyalty() call reads the new budget locally.
kiponos.path("retry", "loyalty_partner").getInt("max_retries") is a local memory read on the outbound hot path — no HTTP to a config service between checkout and partner.
afterValueChanged rebuilds the Resilience4j RetryRegistry entry when ops changes any retry/* key — same pattern as live circuit breaker tuning.
Honest boundary: Kiponos does not replace idempotency key design, partner SLA contracts, or service mesh retry policies. It owns per-dependency retry floats your Java service enforces on every call.
Architecture
Config tree
retry/
loyalty_partner/
max_retries: 3
wait_duration_ms: 200
enabled: true
tax_service/
max_retries: 3
wait_duration_ms: 150
enabled: true
shipping_quotes/
max_retries: 2
wait_duration_ms: 300
enabled: true
incident/
partner_outage_mode: false
default_max_retries_during_outage: 0
ops/
owner: checkout-oncall
notes: "Cut loyalty retries first — partner cert rotations are frequent"
Integration (Spring Boot 3 + Resilience4j)
@Configuration
public class KiponosConfig {
@Bean
public Kiponos kiponos(
@Value("${kiponos.team-id}") String teamId,
@Value("${kiponos.access-key}") String accessKey,
@Value("${kiponos.profile-path}") String profilePath) {
return Kiponos.builder()
.teamId(teamId)
.accessKey(accessKey)
.profilePath(profilePath)
.build();
}
}
@Service
public class CheckoutPartnerGateway {
private final Kiponos kiponos;
private final LoyaltyClient loyaltyClient;
private final RetryRegistry retryRegistry;
private volatile Retry loyaltyRetry;
public CheckoutPartnerGateway(Kiponos kiponos, LoyaltyClient loyaltyClient,
RetryRegistry retryRegistry) {
this.kiponos = kiponos;
this.loyaltyClient = loyaltyClient;
this.retryRegistry = retryRegistry;
kiponos.afterValueChanged(this::onRetryConfigChange);
loyaltyRetry = rebuildLoyaltyRetry();
}
public LoyaltyPoints accrueLoyalty(CheckoutRequest req) {
var cfg = kiponos.path("retry", "loyalty_partner");
if (!cfg.getBool("enabled", true)) {
return loyaltyClient.accrue(req);
}
int maxRetries = resolveMaxRetries("loyalty_partner", cfg);
return loyaltyRetry.executeSupplier(() -> loyaltyClient.accrue(req));
}
private void onRetryConfigChange(ValueChange change) {
if (change.path().startsWith("retry/")) {
loyaltyRetry = rebuildLoyaltyRetry();
log.warn("Loyalty retry policy rebuilt: max_retries={}",
resolveMaxRetries("loyalty_partner",
kiponos.path("retry", "loyalty_partner")));
}
}
private Retry rebuildLoyaltyRetry() {
var cfg = kiponos.path("retry", "loyalty_partner");
int maxAttempts = resolveMaxRetries("loyalty_partner", cfg) + 1;
return retryRegistry.retry("loyaltyPartner", RetryConfig.custom()
.maxAttempts(maxAttempts)
.waitDuration(Duration.ofMillis(cfg.getInt("wait_duration_ms", 200)))
.build());
}
private int resolveMaxRetries(String dependency, ConfigPath cfg) {
if (kiponos.path("retry", "incident").getBool("partner_outage_mode", false)) {
return kiponos.path("retry", "incident")
.getInt("default_max_retries_during_outage", 0);
}
return cfg.getInt("max_retries", 3);
}
}
Ops enables partner_outage_mode or sets loyalty_partner/max_retries: 0. Outbound RPS to the failing partner drops within seconds — tax and shipping retries untouched.
Real scenarios
| Event | Without Kiponos | With Kiponos |
|---|---|---|
| Partner outage — cut retries to failing dependency only | PR + rolling restart all checkout pods |
loyalty_partner/max_retries: 0 live |
| Partial degradation — partner at 50% success | Retry storm keeps failure rate high | Lower to max_retries: 1 mid-incident |
| Recovery — partner green again | Second deploy to restore defaults | Disable partner_outage_mode in one edit |
| Load test — validate zero-retry path | Feature branch per retry value | Hub profile staging/retry
|
| Postmortem audit | Git blame on shared resilience YAML | Hub log: who set loyalty to 0 at 02:19 |
Performance on the outbound hot path
-
getInt()beforeexecuteSupplier— microseconds vs partner HTTP RTT - Retry registry rebuild on change only — not per checkout request
- One WebSocket per checkout pod — not Redis coordination per retry decision
-
Delta patch —
max_retries3 → 0 sends one integer across the fleet - Per-dependency trees — cut loyalty without touching tax/shipping YAML sprawl
Compare to alternatives
| Approach | Cut retries for one partner | Hot-path read cost |
|---|---|---|
| Resilience4j YAML | PR + rolling restart | Zero (frozen) |
@RefreshScope + actuator |
Context refresh | Bean recycle under load |
| Global circuit breaker only | Opens after storm | N/A |
| Redis-shared retry counter | Yes | RTT per outbound call |
| Kiponos SDK | Dashboard, seconds | Memory read |
When not to use Kiponos
| Case | Use instead |
|---|---|
| Idempotency key design and dedup storage | Application schema + docs |
| Service mesh automatic retry policy | Istio/Linkerd VirtualService |
| Partner API contract and SLA enforcement | Legal + integration agreements |
| Which dependencies get retry wrappers at all | Git-reviewed wiring |
| Bootstrap Resilience4j instance names | Git-reviewed YAML is fine |
Getting started (15 minutes)
- Sign up at kiponos.io (TeamPro).
- Create profile path
['checkout']['prod']['retry']. - Add
io.kiponos:sdk-boot-3and Resilience4j to checkout service. - Set
KIPONOS_ID,KIPONOS_ACCESS, and-Dkiponos="['checkout']['prod']['retry']". - Move per-dependency
max_retriesout ofapplication-prod.ymlinto the hub tree. - Wire
CheckoutPartnerGatewaywithafterValueChangedretry rebuild. - Staging game day: simulate partner 503s, set
max_retries: 0, confirm outbound RPS drops without pod restart.
Further reading
- Developer Quickstart
- Product tour
- GETTING-STARTED.md
- Rate limits and circuit breakers
- github.com/kiponos-io/kiponos-io
max_retries belongs in the live ops tree — not in YAML that retries your partner into a bigger outage.

Top comments (0)