Back to API Gateway
Q11Question 11 of 16

Dynamically routing requests to different backends at runtime in Spring Cloud Gateway

Key Concepts
  • A RouteLocator is the gateway's route table; static routes are read from YAML at startup and cannot change at runtime.
  • Programmatic RouteLocator using RouteLocatorBuilder builds routes in Java code but is still fixed at startup.
  • True dynamic routing uses RouteDefinitionRepository to add, change, or remove routes at runtime without redeployment.
  • After changing a route definition, publish a RefreshRoutesEvent to rebuild the cached route table.
  • Weighted routing splits traffic between backends (e.g., 90% blue, 10% green) for canary and blue-green deployments.
  • With multiple gateway replicas, store route definitions in a shared store like Redis and broadcast refresh via Spring Cloud Bus.
  • Route changes must be authenticated, validated, and audited because they control where money-moving requests go.

Definition

A RouteLocator is the gateway's route table, and backing it with a refreshable RouteDefinitionRepository lets you add, change, or shift routes at runtime without a redeploy.
  • RouteLocator – produces a set of routes where each route has a predicate, a URI, and optional filters.
  • Static routes – defined in application.yml, read at startup, and fixed for the gateway's lifetime.
  • Programmatic routes – defined using RouteLocatorBuilder in a @Bean method, still fixed at startup.
  • Dynamic routing – uses RouteDefinitionRepository to add, change, or remove routes at runtime through an API or datastore.
  • RefreshRoutesEvent – rebuilds the cached route table after a definition change.
  • Weighted routing – splits traffic between backends by percentage for canary and blue-green deployments.

Think of the gateway as the information desk at National Bank of India that tells each visitor which counter to go to. A static route is a printed sign on the wall; to change it you must take down the sign and hang a new one, which means closing the desk. Dynamic routing is a digital board that the manager updates instantly from his office. When the loans team moves to a new counter, the manager updates the board live and visitors are redirected immediately without closing the desk.

Understanding the Concepts

What are static routes?

Static routes are defined in YAML and read at startup. They are simple and stable but cannot change while the gateway runs.

YAML
# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: account-route
          uri: lb://account-service
          predicates:
            - Path=/api/v1/accounts/**
        - id: payment-route
          uri: lb://payment-service
          predicates:
            - Path=/api/v1/payments/**
DIAGRAM
// These routes are fixed at startup
// To change: edit YAML → restart gateway → downtime

What are programmatic routes?

Programmatic routes use RouteLocatorBuilder in a @Bean method. They are more flexible than YAML but still fixed at startup.

JAVA
package com.bank.gateway;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ProgrammaticRoutes {

    @Bean
    public RouteLocator bankRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("accounts", r -> r
                        .path("/api/v1/accounts/**")
                        .uri("lb://account-service"))
                .route("payments", r -> r
                        .path("/api/v1/payments/**")
                        .filters(f -> f.circuitBreaker(cb -> cb
                                .setName("paymentCB")
                                .setFallbackUri("forward:/fallback/payments")))
                        .uri("lb://payment-service"))
                .build();
    }
}
TEXT
// More expressive than YAML (can add filters, conditions)
// But still compiled at startup, not changeable at runtime

How do you achieve true dynamic routing?

True runtime dynamic routing uses RouteDefinitionRepository. The built-in InMemoryRouteDefinitionRepository supports adding and removing routes through the actuator API.

JAVA
package com.bank.gateway;

import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/admin/routes")
public class DynamicRouteController {

    private final RouteDefinitionWriter routeWriter;
    private final ApplicationEventPublisher publisher;

    public DynamicRouteController(RouteDefinitionWriter routeWriter,
                                    ApplicationEventPublisher publisher) {
        this.routeWriter = routeWriter;
        this.publisher = publisher;
    }

    @PostMapping
    public Mono<String> addRoute(@RequestBody RouteDefinition definition) {
        return routeWriter.save(Mono.just(definition))
                .then(Mono.fromRunnable(() ->
                        publisher.publishEvent(new RefreshRoutesEvent(this))))
                .then(Mono.just("Route added: " + definition.getId()));
    }

    @DeleteMapping("/{id}")
    public Mono<String> removeRoute(@PathVariable String id) {
        return routeWriter.delete(Mono.just(id))
                .then(Mono.fromRunnable(() ->
                        publisher.publishEvent(new RefreshRoutesEvent(this))))
                .then(Mono.just("Route removed: " + id));
    }
}
DIAGRAM
// POST /admin/routes with a new RouteDefinition JSON
// → Route added, RefreshRoutesEvent published
// → New requests match the updated route table
// No restart, no redeploy needed

Why is RefreshRoutesEvent needed?

The gateway caches compiled routes for performance. After changing a definition, you must publish RefreshRoutesEvent to rebuild the cache. Without it, new requests continue matching the old table.

JAVA
// After saving a new route definition:
publisher.publishEvent(new RefreshRoutesEvent(this));
// Gateway rebuilds the route table from all RouteDefinitionLocators
// New requests now match the updated routes

// Without this event:
// Route definition saved but gateway still uses the old cached table
// Requests go to the old backend until gateway restarts

How does weighted routing enable canary deployments?

Weighted routing splits traffic between two backends by percentage. This is the foundation for canary and blue-green deployments.

YAML
# Canary: 90% to stable, 10% to new version
spring:
  cloud:
    gateway:
      routes:
        - id: payment-stable
          uri: lb://payment-service-v1
          predicates:
            - Path=/api/v1/payments/**
            - Weight=payment-group, 90
        - id: payment-canary
          uri: lb://payment-service-v2
          predicates:
            - Path=/api/v1/payments/**
            - Weight=payment-group, 10
DIAGRAM
// 90% of payment requests → payment-service-v1 (stable)
// 10% of payment requests → payment-service-v2 (canary)
// Monitor canary metrics, then gradually shift: 75/25, 50/50, 0/100
// If canary shows errors → shift back to 100/0 instantly

How do you handle dynamic routing with multiple replicas?

With several gateway replicas, an in-memory repository is not enough because each replica has its own memory. Use a shared store like Redis and broadcast refresh via Spring Cloud Bus.

JAVA
// Problem: 3 gateway replicas
// Add route to Replica 1 → only Replica 1 sees the new route
// Replicas 2 and 3 still have the old route table

// Solution: shared store + broadcast
// 1. Store route definitions in Redis (shared across replicas)
// 2. After saving, publish RefreshRoutesEvent via Spring Cloud Bus
// 3. All replicas receive the event and rebuild their route tables

// Spring Cloud Bus broadcasts via RabbitMQ or Kafka:
// POST /admin/routes (on Replica 1)
// → saves to Redis
// → publishes to Spring Cloud Bus
// → Replicas 2 and 3 receive event → refresh routes

What are common mistakes? (wrong vs correct)

The most dangerous mistake is allowing unauthenticated or unvalidated route changes, which could send payment traffic to the wrong service.

JAVA
// WRONG: anyone can change routes
@PostMapping("/admin/routes")
public Mono<String> addRoute(@RequestBody RouteDefinition def) {
    // No authentication, no validation, no audit
    return routeWriter.save(Mono.just(def)).then(Mono.just("OK"));
}

// CORRECT: secured, validated, audited
@PostMapping("/admin/routes")
@PreAuthorize("hasRole('GATEWAY_ADMIN')")
public Mono<String> addRoute(@RequestBody @Valid RouteDefinition def) {
    auditLogger.log("Route change", def, SecurityContextHolder.getContext());
    return routeWriter.save(Mono.just(def))
            .then(Mono.fromRunnable(() -> publisher.publishEvent(
                    new RefreshRoutesEvent(this))))
            .then(Mono.just("Route added: " + def.getId()));
}
// Route changes are authenticated, validated, and auditable

What should a senior engineer know?

A senior engineer knows that the lb:// URI itself is dynamic because service discovery resolves it per request. They stage route changes through an automated pipeline and use weighted routing for safe rollouts. Routes that control money-moving requests must be treated with the same rigor as code deployments.

TEXT
// Dynamic routing types:
// 1. lb:// URI: service discovery resolves per request (instances scale)
// 2. Weight predicate: splits traffic for canary/blue-green
// 3. RouteDefinitionRepository: add/remove routes at runtime
// 4. All three combine: new route + lb:// + weight = safe live migration

Visual Overview

DIAGRAM
  Static vs Dynamic Routing
  ─────────────────────────
  Static (YAML):
  application.yml ──▶ RouteLocator ──▶ fixed route table
  Change? Edit YAML → restart → downtime

  Dynamic (RouteDefinitionRepository):
  Admin API ──▶ RouteDefinitionWriter ──▶ save to store
       │
       └──▶ RefreshRoutesEvent ──▶ rebuild route table
       │
       └──▶ Spring Cloud Bus ──▶ all replicas refresh
  Change? API call → instant → no downtime


  Weighted Routing for Canary
  ───────────────────────────
  Incoming Traffic
       │
       ├── 90% ──▶ payment-service-v1 (stable)
       │
       └── 10% ──▶ payment-service-v2 (canary)
       │
  Monitor metrics → shift weights → full cutover

Key Takeaway

In Spring Cloud Gateway, routing is driven by a RouteLocator that produces routes, each with a predicate, a URI, and optional filters. Static routes in YAML are read at startup and cannot change, so for runtime changes you use a RouteDefinitionRepository that lets you add, change, or remove routes through an API while the gateway is running. The built-in in-memory repository is wired to the actuator endpoints, so a POST creates a new route and the change becomes live after publishing a RefreshRoutesEvent to rebuild the cached table.

At National Bank of India, this is how Pankaj migrated the Payment Service. He added a weighted route that sent 10% of payment traffic to the rewritten backend as a canary, refreshed the routes, and watched the metrics. Because the weight was dynamic, the cutover was gradual with no downtime during salary week. When the canary showed a latency spike, he shifted the weight back to 100% on the stable backend in seconds.

With multiple gateway replicas, route definitions must be stored in a shared store like Redis and the refresh event broadcast to all replicas via Spring Cloud Bus, so they all rebuild their tables together. Route changes must be authenticated, validated, and audited because they control where money-moving requests go.

Remember one sentence: back your gateway routes with a RouteDefinitionRepository and a RefreshRoutesEvent so you can add, change, or shift routes at runtime without a redeploy.

---

Share
Use ← → arrow keys to navigate