5 Elegant Spring Boot Gray Release Techniques You Can Implement Immediately

This article walks through five practical ways to implement gray (canary) releases in Spring Boot 3.5.0, covering gateway‑based weight and header routing, programmatic route configuration, a custom reactive load‑balancer, and application‑layer annotations with custom HandlerMapping, each illustrated with concrete code samples.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
5 Elegant Spring Boot Gray Release Techniques You Can Implement Immediately

In fast‑moving applications, frequent system updates increase the risk of full‑scale releases. Gray (canary) releases mitigate this risk by routing a subset of traffic—based on criteria such as region, device type, or user tier—to a new version, allowing early validation and quick rollback.

1. Gateway‑Based Gray Release (Spring Cloud Gateway)

The most common approach in a microservice architecture is to let the API gateway manage gray rules. Traffic is directed by attaching a gray identifier (e.g., version number or user tag) to request headers, and the gateway routes requests to the appropriate service instance using metadata from the service registry.

Weight‑Based Routing – Uses WeightRoutePredicateFactory with group and weight parameters. Example configuration:

spring:
  cloud:
    gateway:
      server:
        webflux:
          discovery:
            locator:
              enabled: true
          default-filters:
            - StripPrefix=1
          routes:
            - id: s_v1
              uri: http://localhost:8081
              predicates:
                - Path=/api/**
                - Weight=s0, 90
            - id: s_v2
              uri: http://localhost:8082
              predicates:
                - Path=/api/**
                - Weight=s0, 10

This defines a group s0 where 90% of traffic goes to 8081 and 10% to 8082.

Header‑Based Routing – Uses HeaderRoutePredicateFactory with a header name and a Java regular expression. Example:

spring:
  cloud:
    gateway:
      server:
        webflux:
          discovery:
            locator:
              enabled: true
          default-filters:
            - StripPrefix=1
          routes:
            - id: s_v1
              uri: http://localhost:8081
              predicates:
                - Path=/api/**
                - Header=X-Gray-Id, [a-zA-Z0-9]+
            - id: s_v2
              uri: http://localhost:8082
              predicates:
                - Path=/api/**

Requests carrying a valid X-Gray-Id header are routed to the gray service s_v1 (8081).

Programmatic Configuration – Uses RouteLocatorBuilder bean for fluent route definition:

@Configuration
public class RouteConfig {
  @Bean
  RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
      .route("new_s", r -> r.path("/api-1/**")
        .and().header("X-Gray-Id", "xxxooo")
        .filters(f -> f.stripPrefix(1))
        .uri("http://localhost:8081"))
      .route("old_s", r -> r.path("/api-1/**")
        .filters(f -> f.stripPrefix(1))
        .uri("http://localhost:8082"))
      .build();
  }
}

These three gateway‑based methods are straightforward because they rely on Spring Cloud Gateway’s built‑in features.

2. Custom Load‑Balancer for More Flexibility

Spring Cloud Gateway’s underlying ReactiveLoadBalancer can be extended to apply custom gray logic based on service metadata (e.g., a gray tag) and request headers.

public class GrayRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
  final AtomicInteger position;
  final String serviceId;
  ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

  public GrayRoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> provider,
                                    String serviceId) {
    this(provider, serviceId, new Random().nextInt(1000));
  }

  public GrayRoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> provider,
                                    String serviceId, int seed) {
    this.serviceId = serviceId;
    this.serviceInstanceListSupplierProvider = provider;
    this.position = new AtomicInteger(seed);
  }

  @Override
  public Mono<Response<ServiceInstance>> choose(Request request) {
    ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
      .getIfAvailable(NoopServiceInstanceListSupplier::new);
    return supplier.get(request).next()
      .map(instances -> processInstanceResponse(supplier, instances, request));
  }

  private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
                                                          List<ServiceInstance> instances,
                                                          Request request) {
    Response<ServiceInstance> response = getInstanceResponse(instances, request);
    if (supplier instanceof SelectedInstanceCallback && response.hasServer()) {
      ((SelectedInstanceCallback) supplier).selectedServiceInstance(response.getServer());
    }
    return response;
  }

  private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
    if (instances.isEmpty()) {
      return new EmptyResponse();
    }
    List<ServiceInstance> filtered = instances.stream().filter(instance -> {
      Map<String, String> metadata = instance.getMetadata();
      Object gray = metadata.get("gray");
      RequestDataContext ctx = (RequestDataContext) request.getContext();
      String value = ctx.getClientRequest().getHeaders().getFirst("X-Gray-Id");
      if (StringUtils.hasText(value)) {
        return gray != null && gray.equals(value);
      }
      return !StringUtils.hasText(value) && gray == null;
    }).collect(Collectors.toList());
    if (filtered.isEmpty()) {
      filtered = instances;
    }
    int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
    ServiceInstance instance = filtered.get(pos % filtered.size());
    return new DefaultResponse(instance);
  }
}

The corresponding configuration class registers the custom balancer:

public class GrayDefaultConfiguration {
  @Bean
  ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
      Environment environment, LoadBalancerClientFactory factory) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    return new GrayRoundRobinLoadBalancer(
        factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
  }
}

Finally, enable it with:

@SpringBootApplication
@LoadBalancerClients(defaultConfiguration = GrayDefaultConfiguration.class)
public class App {}

3. Application‑Layer Gray Logic with Custom Annotations

For tighter coupling of gray rules to business code, define a custom @VersionRequestMapping annotation that extends @RequestMapping and carries supported version strings.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping
public @interface VersionRequestMapping {
  String[] v() default {};
  @AliasFor(annotation = RequestMapping.class)
  RequestMethod[] method() default {};
}

Implement a custom HandlerMapping that reads the X-Api-Version header and matches it against the annotation values:

public class GrayRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
  public static final String GRAY_HEADER_KEY = "X-Api-Version";

  @Override
  protected RequestCondition<?> getCustomMethodCondition(Method method) {
    return new VersionAnnotedElementRequestCondition(method);
  }
}

class VersionAnnotedElementRequestCondition implements RequestCondition<VersionAnnotedElementRequestCondition> {
  private final AnnotatedElement element;
  VersionAnnotedElementRequestCondition(AnnotatedElement element) { this.element = element; }
  @Override
  public VersionAnnotedElementRequestCondition getMatchingCondition(HttpServletRequest request) {
    String version = request.getHeader(GrayRequestMappingHandlerMapping.GRAY_HEADER_KEY);
    VersionRequestMapping vrm = element.getAnnotation(VersionRequestMapping.class);
    if (version == null && vrm == null) return this;
    if (version == null || vrm == null) return null;
    String[] supported = vrm.v();
    return Set.of(supported).contains(version) ? this : null;
  }
  @Override public int compareTo(VersionAnnotedElementRequestCondition other, HttpServletRequest request) { return 0; }
  @Override public VersionAnnotedElementRequestCondition combine(VersionAnnotedElementRequestCondition other) { return this; }
}

Register the custom mapping:

@Component
public class PackWebMvcRegistrations implements WebMvcRegistrations {
  @Override
  public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
    return new GrayRequestMappingHandlerMapping();
  }
}

Example controller demonstrating versioned endpoints:

@RestController
@RequestMapping("/version")
public class VersionController {
  @GetMapping("/v")
  public Object index() { return "api version 1.0.0..."; }

  @VersionRequestMapping(path = "/v", v = {"2.0.0"})
  public Object index_v2() { return "api version 2.0.0..."; }

  @VersionRequestMapping(path = "/v", v = {"3.0.0"})
  public Object index_v3() { return "api version 3.0.0..."; }
}

4. Summary of the Five Gray Release Strategies

The article presents five concrete ways to achieve gray releases in Spring Boot 3.5.0:

Gateway weight‑based routing.

Gateway header‑based routing.

Programmatic route definition via RouteLocatorBuilder.

Custom reactive load balancer that selects instances based on metadata and request headers.

Application‑layer control using a custom @VersionRequestMapping annotation and a bespoke HandlerMapping.

Each method includes full configuration snippets and a minimal test controller, enabling developers to choose the most suitable approach for their traffic‑control requirements.

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.

JavaGray ReleaseSpring BootAnnotationSpring Cloud GatewayCustom Load Balancer
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.