If you’ve ever looked at a circuit breaker log and thought:
“Wait… why did it allow/block this call?”
…there’s a good chance the issue isn’t the circuit breaker.
It’s when your reactive pipeline is being created vs when it’s being executed.
Reactive streams are lazy (execution happens on subscribe()), but not every decision in your code is guaranteed to be delayed unless you explicitly make it so.
That’s where defer becomes a lifesaver.
The mental model
- You can build a reactive flow now.
- But the work inside it (HTTP call, DB query, etc.) should happen later, on subscribe.
- The tricky part: some code can accidentally run (or be evaluated) during assembly, not during subscription.
defer forces:
✅ “Create this pipeline only when someone subscribes.”
✅ “And recreate it for every subscription.”
The circuit breaker timing problem
A circuit breaker basically does:
- CLOSED → allow the call
- OPEN → block the call (throw / short-circuit / fallback)
So the real question is:
When does the breaker check happen?
- At pipeline creation time? (too early)
- Or at subscription time? (real time)
If you reuse a Mono/Flux, delay subscription, or have multiple subscribers, “too early” becomes a real bug.
Example setup (new code)
Let’s imagine a PaymentClient that hits an external service, and we want to protect it with Resilience4j + Reactor.
External call (simulated)
import reactor.core.publisher.Mono;
import java.time.Duration;
class PaymentClient {
Mono<String> charge(String userId) {
// Imagine this is an HTTP call
return Mono.delay(Duration.ofMillis(100))
.thenReturn("charged:" + userId);
}
}
Circuit breaker instance
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import java.time.Duration;
class Breakers {
static CircuitBreaker paymentsBreaker() {
var config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slidingWindowSize(10)
.waitDurationInOpenState(Duration.ofSeconds(5))
.build();
return CircuitBreaker.of("payments", config);
}
}
❌ Version 1: “Looks reactive” but can decide too early
This is the pattern that bites people: you build a Mono once, store it, reuse it.
import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator;
import reactor.core.publisher.Mono;
class PaymentServiceBad {
private final PaymentClient client = new PaymentClient();
private final CircuitBreaker cb = Breakers.paymentsBreaker();
// Imagine someone caches this Mono or reuses it across requests
Mono<String> buildChargeFlow(String userId) {
Mono<String> flow = client.charge(userId)
.transformDeferred(CircuitBreakerOperator.of(cb)); // breaker is applied here
return flow;
}
}
Why can this be a problem?
Because flow is just an object you’re returning. In real systems, it might be:
- stored in a cache layer
- reused by multiple subscribers
- subscribed later due to pipeline composition
- passed around before finally being consumed
If the circuit breaker state changes between “build” and “subscribe”, you can get behavior that feels inconsistent.
Also: some integrations/operators may capture context at assembly time. You want the breaker decision and any dynamic values to be evaluated at subscribe time.
✅ Version 2: defer forces real-time evaluation per subscriber
Here we guarantee a fresh pipeline per subscribe:
import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator;
import reactor.core.publisher.Mono;
class PaymentServiceGood {
private final PaymentClient client = new PaymentClient();
private final CircuitBreaker cb = Breakers.paymentsBreaker();
Mono<String> charge(String userId) {
return Mono.defer(() ->
client.charge(userId)
.transformDeferred(CircuitBreakerOperator.of(cb))
);
}
}
What you get with this:
✅ Circuit breaker permission check happens when someone subscribes
✅ Every subscriber gets a fresh decision
✅ If you call this from:
- an HTTP request
- a retry
- a background reprocess
- a second consumer
…each one is evaluated at the right time.
A quick “multiple subscribers” demo
This small snippet shows why “fresh per subscribe” matters:
var service = new PaymentServiceGood();
// Subscriber A
service.charge("user-1")
.doOnNext(System.out::println)
.subscribe();
// Subscriber B (later)
service.charge("user-1")
.doOnNext(System.out::println)
.subscribe();
If the circuit flips OPEN between A and B, B will be blocked (as expected).
Without defer (or if you reused the same Mono instance), you may accidentally carry timing/state you didn’t mean to.
Rule of thumb
Use defer when:
- the flow might be subscribed more than once
- the flow might be created now and subscribed later
- you want circuit breaker decisions aligned with the actual call time
- you care about accurate metrics/logs per attempt
You can skip defer when:
- the flow is always subscribed immediately
- you intentionally want to evaluate once and reuse the exact same pipeline instance
Final thought
In reactive programming, timing is part of the logic.
defer is basically you saying:
“I want this decision to be made when it actually matters.”
And when circuit breakers are involved, that timing is often the difference between “works fine” and “why is this thing gaslighting me”.
Top comments (0)