Circuit Breaker Upgrade Plan: Hystrix → Resilience4j¶
Current: Netflix Hystrix (deprecated 2018)
Target: Resilience4j 2.2.0
Effort: 1-2 days
Risk: Medium
Prerequisites: Spring Boot 3.2.x completed
Executive Summary¶
Migration from deprecated Hystrix to Resilience4j - the modern, actively maintained fault tolerance library recommended by Netflix.
Why Resilience4j? - ✅ Actively maintained - ✅ Spring Boot 3.x compatible - ✅ Lightweight, no dependencies - ✅ Functional programming API - ✅ Better metrics (Micrometer) - ✅ More patterns: Circuit Breaker, Retry, Rate Limiter, Bulkhead
Files to Update:
1. build.gradle - Dependencies
2. Bootstrap.java - Remove @EnableCircuitBreaker
3. CircuitBreakerHystrixConfig.java - Delete/replace
4. ControllerLoggingAspect.java - Rewrite with Resilience4j
5. application.properties - New configuration
Phase 0: Preparation (1 hour)¶
0.1 Backup¶
git checkout -b backup/before-resilience4j
git commit -am "Backup before Resilience4j migration"
git checkout -b feature/migrate-to-resilience4j
0.2 Document Current Config¶
Hystrix → Resilience4j Mapping:
- apiDefaultService (200ms) → timeoutDuration
- hyxCbSleepWindowMS (1000ms) → waitDurationInOpenState
- hyxCbReqVolmThreshold (20) → minimumNumberOfCalls
- hyxCbErrThresholdPercentage (50%) → failureRateThreshold
Phase 1: Dependencies (30 min)¶
1.1 Update build.gradle¶
dependencies {
// ❌ REMOVE
// implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'
// ✅ ADD Resilience4j
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
implementation 'io.github.resilience4j:resilience4j-timelimiter:2.2.0'
implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0'
}
Phase 2: Configuration (1 hour)¶
2.1 Create Resilience4jConfig.java¶
File: src/main/java/com/anr/config/Resilience4jConfig.java
package com.anr.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class Resilience4jConfig {
@Autowired
private ConfigProperties appProps;
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(appProps.getWaitperiod().getHyxCbErrThresholdPercentage())
.minimumNumberOfCalls(appProps.getWaitperiod().getHyxCbReqVolmThreshold())
.waitDurationInOpenState(Duration.ofMillis(appProps.getWaitperiod().getHyxCbSleepWindowMS()))
.permittedNumberOfCallsInHalfOpenState(10)
.slidingWindowSize(100)
.build();
return CircuitBreakerRegistry.of(config);
}
@Bean
public TimeLimiterRegistry timeLimiterRegistry() {
TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(appProps.getWaitperiod().getApiDefaultService()))
.cancelRunningFuture(true)
.build();
return TimeLimiterRegistry.of(config);
}
}
2.2 Add application.properties¶
# Resilience4j Circuit Breaker
resilience4j.circuitbreaker.configs.default.registerHealthIndicator=true
resilience4j.circuitbreaker.configs.default.slidingWindowSize=100
resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=20
resilience4j.circuitbreaker.configs.default.waitDurationInOpenState=1000ms
resilience4j.circuitbreaker.configs.default.failureRateThreshold=50
resilience4j.circuitbreaker.instances.defaultApi.baseConfig=default
resilience4j.circuitbreaker.instances.secondApi.baseConfig=default
# Time Limiter
resilience4j.timelimiter.instances.defaultApi.timeoutDuration=200ms
resilience4j.timelimiter.instances.secondApi.timeoutDuration=3000ms
# Bulkhead (replaces thread pools)
resilience4j.bulkhead.instances.defaultApi.maxConcurrentCalls=40
# Actuator
management.endpoints.web.exposure.include=health,circuitbreakers
management.health.circuitbreakers.enabled=true
Phase 3: Update Bootstrap (15 min)¶
File: src/main/java/com/anr/Bootstrap.java
@Configuration
@Slf4j
@EnableAsync
@EnableAspectJAutoProxy
// ❌ REMOVE: @EnableCircuitBreaker
@SpringBootApplication(exclude = MongoAutoConfiguration.class)
public class Bootstrap extends SpringBootServletInitializer {
// ... rest unchanged
}
Phase 4: Migrate Aspect (2 hours)¶
File: src/main/java/com/anr/logging/ControllerLoggingAspect.java
package com.anr.logging;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import io.vavr.control.Try;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
@Aspect
@Component
public class ControllerLoggingAspect {
@Autowired private ConfigProperties appProps;
@Autowired private SBUtil sbutil;
@Autowired private Gson gson;
@Autowired private ControllerFailureResponses failures;
@Autowired private CircuitBreakerRegistry circuitBreakerRegistry;
@Autowired private TimeLimiterRegistry timeLimiterRegistry;
@Around("execution(* com.anr.controller.MainSBController.getSampleResponse(..))")
public SBResponseModel logSampleResponse(ProceedingJoinPoint jointpoint,
String transactionID, String sourceChannel, String locale,
String field1, String field2) {
long startTime = System.currentTimeMillis();
SplunkEventBuilder bldr = new SplunkEventBuilder("Default-Api", "localhost",
sourceChannel, transactionID);
bldr.transactionType(TransactionType.Request);
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("defaultApi");
TimeLimiter tl = timeLimiterRegistry.timeLimiter("defaultApi");
SBResponseModel response;
try {
Supplier<CompletableFuture<SBResponseModel>> futureSupplier = () ->
CompletableFuture.supplyAsync(() -> {
try {
return (SBResponseModel) jointpoint.proceed();
} catch (Throwable t) {
throw new RuntimeException(t);
}
});
Callable<SBResponseModel> decorated = TimeLimiter
.decorateFutureSupplier(tl, futureSupplier);
decorated = CircuitBreaker.decorateCallable(cb, decorated);
response = Try.ofCallable(decorated)
.recover(t -> handleFailure(t, jointpoint, transactionID,
sourceChannel, locale, field1, field2))
.get();
} catch (Exception e) {
response = handleFailure(e, jointpoint, transactionID,
sourceChannel, locale, field1, field2);
}
logResponse(response, bldr, transactionID, startTime);
return response;
}
private SBResponseModel handleFailure(Throwable t, ProceedingJoinPoint jp,
String txId, String channel, String locale, String f1, String f2) {
sbutil.logError(txId, "Circuit breaker failure: " + t.getMessage());
return failures.getSampleFailureResponse(txId, channel, locale, f1, f2, t);
}
private void logResponse(SBResponseModel response, SplunkEventBuilder bldr,
String txId, long startTime) {
String msg = response == null ? "null" : gson.toJson(response);
msg += "; time=" + (System.currentTimeMillis() - startTime) + "ms";
sbutil.logInfo(txId, msg);
sbutil.logToSplunkOrSimilar(bldr.build(), startTime);
}
}
Phase 5: Cleanup (30 min)¶
5.1 Remove Old Files¶
5.2 Remove Hystrix Properties¶
Comment out in application.properties:
# ❌ Removed Hystrix config
# sbsvc.waitperiod.hyxCbSleepWindowMS=1000
# sbsvc.waitperiod.hyxThrdPoolCoreSize=30
Phase 6: Testing (2 hours)¶
6.1 Create Tests¶
File: src/test/java/com/anr/controller/ControllerCircuitBreakerTest.java
@SpringBootTest(webEnvironment = RANDOM_PORT)
class ControllerCircuitBreakerTest {
@Autowired
private CircuitBreakerRegistry registry;
@Test
void testCircuitBreakerExists() {
CircuitBreaker cb = registry.circuitBreaker("defaultApi");
assertNotNull(cb);
assertEquals(CircuitBreaker.State.CLOSED, cb.getState());
}
@Test
void testCircuitOpensOnFailures() {
CircuitBreaker cb = registry.circuitBreaker("defaultApi");
for (int i = 0; i < 25; i++) {
try {
cb.executeSupplier(() -> { throw new RuntimeException(); });
} catch (Exception e) {}
}
assertEquals(CircuitBreaker.State.OPEN, cb.getState());
}
}
6.2 Run Tests¶
Phase 7: Verification (1 hour)¶
7.1 Test Endpoints¶
# Health check with circuit breaker status
curl http://localhost:8080/actuator/health
# Circuit breaker metrics
curl http://localhost:8080/actuator/circuitbreakers
# Test API
curl -u user:password http://localhost:8080/api/v1/default
7.2 Monitor Metrics¶
Access Actuator endpoints:
- /actuator/circuitbreakers - CB status
- /actuator/circuitbreakerevents - CB events
- /actuator/metrics/resilience4j.circuitbreaker.calls - Metrics
Comparison: Hystrix vs Resilience4j¶
| Feature | Hystrix | Resilience4j |
|---|---|---|
| Status | Deprecated | Active |
| Spring Boot 3 | ❌ No | ✅ Yes |
| Dependencies | Heavy | Lightweight |
| API Style | Imperative | Functional |
| Metrics | Custom | Micrometer |
| Thread Pools | Built-in | Use Bulkhead |
| Patterns | CB only | CB, Retry, Rate Limit, Bulkhead |
Rollback Plan¶
Success Criteria¶
- ✅ Application builds
- ✅ All tests pass
- ✅ Circuit breaker opens on failures
- ✅ Timeouts work correctly
- ✅ Metrics available in Actuator
- ✅ No Hystrix dependencies
Timeline¶
| Phase | Duration |
|---|---|
| Preparation | 1 hour |
| Dependencies | 30 min |
| Configuration | 1 hour |
| Bootstrap | 15 min |
| Aspect Migration | 2 hours |
| Cleanup | 30 min |
| Testing | 2 hours |
| Verification | 1 hour |
| Total | 8-10 hours (1-2 days) |
Next: Proceed to Upgrade_H2_Plan.md for MongoDB → H2 migration.