Tools
Tools: Defer vs Immediate Reactive Flow Creation (and why your Circuit Breaker can “decide too early”)
2026-02-04
0 views
admin
The mental model ## The circuit breaker timing problem ## When does the breaker check happen? ## Example setup (new code) ## External call (simulated) ## Circuit breaker instance ## ❌ Version 1: “Looks reactive” but can decide too early ## Why can this be a problem? ## ✅ Version 2: defer forces real-time evaluation per subscriber ## What you get with this: ## A quick “multiple subscribers” demo ## Rule of thumb ## Final thought 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. ✅ “Create this pipeline only when someone subscribes.”
✅ “And recreate it for every subscription.” A circuit breaker basically does: So the real question is: If you reuse a Mono/Flux, delay subscription, or have multiple subscribers, “too early” becomes a real bug. Let’s imagine a PaymentClient that hits an external service, and we want to protect it with Resilience4j + Reactor. This is the pattern that bites people: you build a Mono once, store it, reuse it. Because flow is just an object you’re returning. In real systems, it might be: 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. Here we guarantee a fresh pipeline per subscribe: ✅ Circuit breaker permission check happens when someone subscribes
✅ Every subscriber gets a fresh decision
✅ If you call this from: …each one is evaluated at the right time. This small snippet shows why “fresh per subscribe” matters: 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. You can skip defer when: 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”. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK:
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); }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
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); }
} COMMAND_BLOCK:
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); }
} CODE_BLOCK:
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); }
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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); }
} CODE_BLOCK:
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); }
} COMMAND_BLOCK:
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; }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
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; }
} COMMAND_BLOCK:
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; }
} COMMAND_BLOCK:
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)) ); }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
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)) ); }
} COMMAND_BLOCK:
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)) ); }
} CODE_BLOCK:
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(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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(); CODE_BLOCK:
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(); - 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. - CLOSED → allow the call
- OPEN → block the call (throw / short-circuit / fallback) - At pipeline creation time? (too early)
- Or at subscription time? (real time) - stored in a cache layer
- reused by multiple subscribers
- subscribed later due to pipeline composition
- passed around before finally being consumed - an HTTP request
- a background reprocess
- a second consumer - 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 - the flow is always subscribed immediately
- you intentionally want to evaluate once and reuse the exact same pipeline instance
how-totutorialguidedev.toaigitgithub