Build a Lightweight Circuit Breaker in Spring Boot 3 with Custom Annotations

This article demonstrates how to implement a lightweight circuit‑breaker mechanism in Spring Boot 3 by defining a custom @Breaker annotation, a failure‑tracking component using LongAdder, and an AOP aspect that handles opening, probing, and fallback logic without external libraries.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build a Lightweight Circuit Breaker in Spring Boot 3 with Custom Annotations

In micro‑service architectures, calling other services or third‑party APIs can fail repeatedly, wasting CPU, memory, and bandwidth. A circuit‑breaker stops further attempts after several failures, protecting resources.

While libraries such as Resilience4j or Sentinel are common, this guide shows a lightweight solution that works directly at the controller layer using a custom annotation and AOP.

1. Define the circuit‑breaker annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Breaker {
  /** Unique identifier; generated from method if not set */
  String id() default "";
  /** Failure threshold to open the circuit */
  int threshold() default 3;
  /** Fallback method name */
  String fallback() default "";
}

2. Failure counter using LongAdder

public class FailureTracker {
  private final ConcurrentHashMap<String, LongAdder> failures = new ConcurrentHashMap<>();
  public void recordFailure(String id) { failures.computeIfAbsent(id, k -> new LongAdder()).increment(); }
  public long getFailures(String id) { return failures.getOrDefault(id, new LongAdder()).sum(); }
  public void reset(String id) { failures.put(id, new LongAdder()); }
}

3. AOP aspect that controls the circuit

@Aspect
@Component
public class BreakerAspect {
  private static final Logger logger = LoggerFactory.getLogger(BreakerAspect.class);
  private static final long TIMEOUT_MS = 10_000;
  private static final int PROBE_LIMIT = 3;
  private final FailureTracker tracker = new FailureTracker();
  private final ConcurrentHashMap<String, Long> openedAt = new ConcurrentHashMap<>();
  private final ConcurrentHashMap<String, Integer> probeCount = new ConcurrentHashMap<>();
  private final DefaultSpelResolver spelResolver;

  public BreakerAspect(DefaultSpelResolver spelResolver) { this.spelResolver = spelResolver; }

  @Around("@annotation(breaker)")
  public Object wrap(ProceedingJoinPoint pjp, Breaker breaker) throws Throwable {
    Method method = ((MethodSignature) pjp.getSignature()).getMethod();
    String id = spelResolver.resolve(method, pjp.getArgs(), breaker.id());
    logger.info("CircuitBreaker id: {}", id);
    long now = System.currentTimeMillis();
    Long opened = openedAt.get(id);
    if (opened != null) {
      if (now - opened < TIMEOUT_MS) {
        return fallback(pjp, breaker, id, new CircuitBreakerException("Circuit open for " + id));
      }
      int used = probeCount.merge(id, 1, Integer::sum);
      if (used > PROBE_LIMIT) {
        openedAt.put(id, now);
        return fallback(pjp, breaker, id, new CircuitBreakerException("Circuit open for " + id));
      }
    }
    try {
      Object result = pjp.proceed();
      tracker.reset(id);
      openedAt.remove(id);
      probeCount.remove(id);
      return result;
    } catch (Exception ex) {
      tracker.recordFailure(id);
      if (tracker.getFailures(id) >= breaker.threshold()) {
        openedAt.put(id, now);
        probeCount.remove(id);
      }
      return fallback(pjp, breaker, id, ex);
    }
  }

  private Object fallback(ProceedingJoinPoint pjp, Breaker breaker, String id, Throwable th) throws Throwable {
    String fallback = breaker.fallback();
    if (!StringUtils.hasLength(fallback)) {
      throw new CircuitBreakerException("Circuit open for " + id, th);
    }
    Method fallbackMethod = fallbackMethodCache.computeIfAbsent(id, key -> {
      Method m = ((MethodSignature) pjp.getSignature()).getMethod();
      Object target = pjp.getTarget();
      Class<?>[] paramTypes = m.getParameterTypes();
      try {
        return target.getClass().getDeclaredMethod(fallback, paramTypes);
      } catch (Exception e) {
        // try with Throwable as last parameter
        Class<?>[] extended = Arrays.copyOf(paramTypes, paramTypes.length + 1);
        extended[extended.length - 1] = Throwable.class;
        try {
          return target.getClass().getDeclaredMethod(fallback, extended);
        } catch (Exception ex) {
          throw new RuntimeException(fallback + " method not found", ex);
        }
      }
    });
    fallbackMethod.setAccessible(true);
    Object[] args = pjp.getArgs();
    if (fallbackMethod.getParameterCount() > pjp.getArgs().length) {
      args = Arrays.copyOf(pjp.getArgs(), fallbackMethod.getParameterCount());
      args[args.length - 1] = th;
    }
    return fallbackMethod.invoke(pjp.getTarget(), args);
  }
}

4. Supporting exception class

public class CircuitBreakerException extends RuntimeException {
  public CircuitBreakerException(String message, Throwable cause) { super(message, cause); }
  public CircuitBreakerException(String message) { super(message); }
}

5. SpEL resolver used by the aspect

@Component
public class DefaultSpelResolver implements EmbeddedValueResolverAware {
  private static final String PLACEHOLDER_SPEL_REGEX = "^[$#]\\{.+}$";
  private static final String METHOD_SPEL_REGEX = "^#.+$";
  private static final String BEAN_SPEL_REGEX = "^@.+";
  private final SpelExpressionParser expressionParser = new SpelExpressionParser();
  private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
  private final BeanFactory beanFactory;
  private StringValueResolver stringValueResolver;
  public DefaultSpelResolver(BeanFactory beanFactory) { this.beanFactory = beanFactory; }
  public String resolve(Method method, Object[] args, String expr) {
    if (!StringUtils.hasLength(expr)) return expr;
    if (expr.matches(PLACEHOLDER_SPEL_REGEX) && stringValueResolver != null) {
      return stringValueResolver.resolveStringValue(expr);
    }
    if (expr.matches(METHOD_SPEL_REGEX)) {
      SpelRootObject root = new SpelRootObject(method, args);
      MethodBasedEvaluationContext ctx = new MethodBasedEvaluationContext(root, method, args, parameterNameDiscoverer);
      return (String) expressionParser.parseExpression(expr).getValue(ctx);
    }
    if (expr.matches(BEAN_SPEL_REGEX)) {
      SpelRootObject root = new SpelRootObject(method, args);
      MethodBasedEvaluationContext ctx = new MethodBasedEvaluationContext(root, method, args, parameterNameDiscoverer);
      ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
      return (String) expressionParser.parseExpression(expr).getValue(ctx);
    }
    return expr;
  }
  @Override
  public void setEmbeddedValueResolver(StringValueResolver resolver) { this.stringValueResolver = resolver; }
}

6. Example usage

@RestController
@RequestMapping("/users")
public class UserController {
  @GetMapping("/query")
  @Breaker(id = "#name + 'userQuery'", threshold = 2, fallback = "queryFallback")
  public ResponseEntity<String> query(String name) {
    if (Math.random() < 0.5) {
      throw new RuntimeException("查询参数错误");
    }
    return ResponseEntity.ok("success");
  }

  public ResponseEntity<?> queryFallbackNoException(String name) {
    return ResponseEntity.ok("查询失败");
  }

  public ResponseEntity<?> queryFallback(String name, Throwable e) {
    return ResponseEntity.ok("查询失败, " + e.getMessage());
  }
}

The test defines two fallback methods, one of which accepts the exception object, demonstrating how the aspect routes failed calls to the appropriate fallback.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAOPSpring BootCustom Annotationcircuit breaker
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.