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.
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, 10This 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
