Week 17: Spring Cloud Microservices — Config Server, Eureka & Gateway

Weeks 9–10 had two services calling each other directly, with a hardcoded URL and environment variables passed in by hand. That's fine for two services; it stops scaling past four or five, once every service needs to know every other service's address and every environment needs its own copy of every config file. This week introduces the shared infrastructure Spring Cloud provides for exactly that problem: centralized configuration, service discovery, and a single gateway in front of everything.

Module 14 of 22 Week 17 of 26 ~4–5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Centralize configuration across services with Spring Cloud Config Server
  • Register and discover services dynamically with Eureka
  • Route external traffic through a single Spring Cloud Gateway

1. Centralized Config with Spring Cloud Config Server

With two services, each keeping its own application.properties per environment is manageable. With ten services and three environments, that's thirty separate config files to keep consistent — and a shared value (a database URL, a feature flag) that changes means editing every copy individually and hoping none were missed. Config Server centralizes this: one Git repository holds every service's configuration, and each service fetches its own config from the server at startup.

config-server's own application.yml
server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/acme/config-repo
          default-label: main
config-repo/task-service.yml — one file per service, in the Git repo
spring:
  datasource:
    url: jdbc:postgresql://prod-db:5432/tasks
resilience4j:
  circuitbreaker:
    instances:
      inventoryService:
        failureRateThreshold: 50
task-service's bootstrap.yml — pulling config on startup
spring:
  application:
    name: task-service       # matches config-repo/task-service.yml
  config:
    import: "configserver:http://localhost:8888"

spring.application.name is the join key — Config Server serves task-service.yml specifically to the service that identifies itself as task-service, and a different file to every other registered service name. Because the backing store is a Git repository, every configuration change has full history, blame, and review through an ordinary pull request — the same audit trail Week 17's DevOps counterpart gets from Terraform's version control.

Config Server is a config store, not a secrets manager

A Git-backed config repo is a poor home for a real database password or API key — it's plaintext, versioned, and often more widely readable than the secrets themselves should be. Reference an actual secrets manager (or Kubernetes Secrets) for genuine credentials, and use Config Server for everything else: connection pool sizes, feature flags, circuit breaker thresholds, timeouts.

2. Service Discovery with Eureka

Week 9's RestClient calls used a hardcoded base URL for the downstream service. That breaks down once a service runs as multiple, dynamically-scaled instances with changing addresses — exactly the world Week 12's Kubernetes Deployments already live in, solved there by a Kubernetes Service. Eureka solves the same problem for Spring Cloud microservices directly: a registry where every service instance registers itself on startup and periodically confirms it's still alive.

eureka-server's application.yml
server:
  port: 8761
eureka:
  client:
    register-with-eureka: false   # the server doesn't register with itself
    fetch-registry: false
task-service registering itself as a client
spring:
  application:
    name: task-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

With both sides configured, a caller no longer needs a hardcoded URL at all — it asks Eureka for an instance of task-service by name, and Eureka returns a currently-healthy address:

calling a service by name via a discovery-aware RestClient
@Bean
@LoadBalanced   // resolves "http://task-service" via Eureka, not DNS
RestClient.Builder loadBalancedRestClientBuilder() {
    return RestClient.builder();
}

// elsewhere
RestClient client = loadBalancedRestClientBuilder.baseUrl("http://task-service").build();
TaskResponse task = client.get().uri("/api/tasks/{id}", id)
    .retrieve().body(TaskResponse.class);

@LoadBalanced is what makes http://task-service resolve at all — without it, that's not a real hostname and the call fails outright. With it, every request is transparently routed to one of the currently-registered, healthy task-service instances, load-balanced across however many are running — the calling code never needs to change as instances scale up, scale down, or move.

3. Spring Cloud Gateway

With several services running, exposing each one's port directly to the outside world is both a security problem and an unpleasant API surface for external clients to navigate. Spring Cloud Gateway sits in front of every service as a single entry point, routing incoming requests to the right backend based on the request path — the same role Week 15's Kubernetes Ingress plays, implemented in the application layer instead of the cluster's networking layer.

application.yml — routing rules
spring:
  cloud:
    gateway:
      routes:
        - id: task-service
          uri: lb://task-service          # "lb" = load-balanced via Eureka
          predicates:
            - Path=/api/tasks/**
          filters:
            - StripPrefix=0
        - id: inventory-service
          uri: lb://inventory-service
          predicates:
            - Path=/api/inventory/**

lb://task-service combines both this section and the last: the lb scheme tells the Gateway to resolve task-service through Eureka and load-balance across its instances, exactly like the @LoadBalanced RestClient from Section 2 — routing and discovery composing together rather than being two unrelated concerns.

A Gateway is also the natural place to apply cross-cutting concerns once, instead of duplicating them in every service — Week 16's rate limiting, request logging, and authentication token validation can all live as Gateway filters that apply uniformly to every route, rather than being reimplemented per service.

All of this infrastructure is a cost — only take it on when it earns out

Config Server, Eureka, and Gateway are each one more moving part to run, monitor, and keep available — and each is a single point of failure if not itself made resilient. For the two or three services this course has built, running them directly with environment-variable config and hardcoded (or Week 12 Kubernetes-Service-resolved) addresses is often genuinely simpler and no less correct. This stack earns its cost once a system has genuinely many services, in genuinely many environments, changing configuration and topology often enough that manual coordination becomes the actual bottleneck.

4. Hands-on Exercise

Hands-on

Wire two services together through Config Server, Eureka and a Gateway

Take the two services from Weeks 9–10 and put real Spring Cloud infrastructure in front of them.

Requirements:

  1. Stand up a Config Server backed by a local Git repository, with a config file per service, and confirm each service pulls its config successfully at startup.
  2. Stand up a Eureka server and register both services with it; confirm both appear as healthy instances in the Eureka dashboard.
  3. Replace one hardcoded service-to-service URL with a @LoadBalanced RestClient resolving the target by service name through Eureka.
  4. Stand up a Spring Cloud Gateway routing /api/tasks/** and /api/inventory/** to the two services by name, and confirm both are reachable only through the Gateway's single port.
  5. Scale one service to two instances locally (different ports, same spring.application.name) and confirm the Gateway load-balances requests across both.
Hint

The Eureka dashboard at http://localhost:8761 shows every registered instance and its status in real time — it's the fastest way to confirm registration actually worked before debugging a routing issue further downstream.

5. Knowledge Check

Three quick questions. Expand each to check your answer.

Q1

Why is a Git-backed Config Server a poor place to store an actual database password?

Config Server's backing Git repository stores values in plaintext with full version history, typically readable by a broader set of people (anyone with repo access) than should be able to see a live production credential. A dedicated secrets manager or Kubernetes Secret restricts access more tightly and supports rotation without leaving old values in Git history forever.

Q2

Why does http://task-service only work as a target URL when the RestClient is @LoadBalanced?

task-service isn't a real DNS hostname — it's a logical service name registered in Eureka. The @LoadBalanced annotation adds an interceptor that resolves that logical name against Eureka's registry into a real host and port before the request is actually sent, and picks which of possibly several healthy instances to send it to. Without it, the client would try to resolve task-service as a literal hostname and fail.

Q3

What's a concrete reason a small, two-service system might reasonably skip Config Server, Eureka, and Gateway entirely?

Each of these components is additional infrastructure to run, monitor, and keep available, and each becomes a new potential point of failure. For a system with only a couple of services and stable, infrequently-changing configuration and topology, the coordination problem this stack solves barely exists yet — environment variables and a Kubernetes Service (or even hardcoded addresses) are simpler and carry less operational risk until the system's actual scale justifies the added infrastructure.