HomeSpring BootService Discovery in Microservices: Eureka, Client-Side vs Server-Side, Kubernetes
Spring Boot

Service Discovery in Microservices: Eureka, Client-Side vs Server-Side, Kubernetes

Learn how service discovery works in Spring Boot microservices: client-side vs server-side discovery, Eureka server and client setup, health checks and heartbeats, self-preservation, load balancing, Kubernetes, and troubleshooting.

What Is Service Discovery?

In a monolith, a method call is resolved at compile time. In microservices, a call goes over the network, and the target's IP and port change whenever a container restarts, moves or scales. **Service discovery** replaces static addresses with a runtime registry of live instances, so a client can resolve a logical name such as `user-service` to a real address. A discovery system does four jobs: 1. **Register:** instances announce themselves on startup. 2. **Heartbeat:** instances prove they are alive. 3. **Look up:** clients ask for the instances of a service. 4. **Remove:** dead instances are dropped from the registry. Examples use Spring Boot 3.x with Spring Cloud. Use the Spring Cloud release train that matches your Boot version.

Do You Need Eureka? Options at a Glance

Eureka is one option, not a requirement. The platform you deploy on often decides. | Option | Pattern | Use when | |---|---|---| | **Kubernetes Service and DNS** | Server-side | You deploy on Kubernetes. Usually the right default, with no registry code | | **Eureka** (Spring Cloud Netflix) | Client-side | VMs, Docker Compose or mixed environments, and you want Spring-native discovery | | **Consul** (Spring Cloud Consul) | Client-side | You want a registry with built-in health checks and a key-value store | | **Spring Cloud Kubernetes Discovery** | Client-side on Kubernetes | You need discovery inside Spring code, such as metadata or instance lists | | **Cloud load balancer (AWS ALB, etc.)** | Server-side | Instances sit behind a managed load balancer | | **Plain DNS** | Server-side | Simple setups with few services | Check the Spring Cloud documentation for the current maintenance status of each integration before you choose one for a new project.

The Problem With Hard-Coded Addresses

A configuration such as `user-service.url=http://192.168.1.101:8081` works until the container restarts and gets a new IP, or you scale to three instances and must choose which one to call. A registry removes that problem. The flow: 1. `UserService` starts and registers its address with the registry. 2. `OrderService` asks the registry where `user-service` is. 3. The registry returns the list of healthy instances. 4. `OrderService` picks one and calls it directly.
Text
UserService (172.18.0.5:8081) --register--> Service Registry
UserService (172.18.0.9:8081) --register--> user-service: [172.18.0.5:8081, 172.18.0.9:8081]

OrderService --"where is user-service?"--> Service Registry
OrderService <--[172.18.0.5, 172.18.0.9]-- Service Registry

OrderService --GET http://172.18.0.5:8081/api/users/1--> UserService

Client-Side vs Server-Side Discovery

| | Client-side discovery | Server-side discovery | |---|---|---| | Who looks up instances | The calling service | A load balancer or proxy | | Examples | Eureka, Consul with Spring Cloud LoadBalancer | Kubernetes Service, AWS ALB | | Extra network hop | No | Yes | | Client complexity | Needs a registry client and load-balancing logic | Plain HTTP to a stable name | | Load-balancing control | Full, per client | Limited to what the platform offers | | Language support | Needs a client library per language | Works for any language | Spring Cloud with Eureka uses **client-side** discovery. Kubernetes uses **server-side** discovery.

Setting Up a Eureka Server

A Eureka server is a Spring Boot application with one annotation. A standalone server should not register with itself or fetch its own registry, so switch both off. The default port is 8761. Run more than one Eureka server in production, each peered with the others, because the registry is a single point of failure.
XML
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Eureka Server Class and Configuration

The dashboard is served at the server's root URL (`http://localhost:8761`).
Java
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServerApplication.class, args);
    }
}

# application.yml
server:
  port: 8761

eureka:
  client:
    register-with-eureka: false     # a standalone server doesn't register with itself
    fetch-registry: false

Registering a Service (Eureka Client)

Add the client starter, give the service a name, and point it at the server. With the starter on the classpath, registration is automatic, and `@EnableDiscoveryClient` is optional. The `spring.application.name` becomes the logical service name other services use. Eureka shows it in upper case, but lookups are case-insensitive.
yaml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

# application.yml
spring:
  application:
    name: user-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

Calling a Service by Name

A URL like `http://user-service/api/users/1` resolves only through a **load-balanced** HTTP client. Mark the client builder with `@LoadBalanced`, and Spring Cloud LoadBalancer replaces the service name with a real instance address. Without `@LoadBalanced`, the call fails with an unknown host error. - `RestClient` and `WebClient` builders can be load-balanced. Check your Spring Cloud version for `RestClient` support, which is newer than the others. - `RestTemplate` also works with `@LoadBalanced`, but it is in maintenance mode, so prefer `RestClient` for new code. - **OpenFeign** resolves service names for you with no extra annotation.
Java
@Configuration
public class HttpClientConfig {

    @Bean
    @LoadBalanced
    RestClient.Builder loadBalancedRestClientBuilder() {
        return RestClient.builder();
    }
}

@Service
public class UserClient {

    private final RestClient restClient;

    public UserClient(RestClient.Builder loadBalancedBuilder) {
        this.restClient = loadBalancedBuilder.baseUrl("http://user-service").build();
    }

    public UserResponse findById(Long id) {
        return restClient.get()
            .uri("/api/users/{id}", id)
            .retrieve()
            .body(UserResponse.class);
    }
}

Health Checks and Heartbeats

A registry is only useful if it reflects live instances. Each client sends a heartbeat to Eureka every 30 seconds by default. If the server stops receiving heartbeats from an instance for the lease duration (90 seconds by default), it becomes eligible for eviction. **Important:** by default, a heartbeat only says "this process is running". Eureka does **not** consult your Actuator health endpoint, so an instance whose database is down still reports `UP`. To make Eureka report the real status, enable health check propagation. Instances whose health is not `UP` are then marked `DOWN` in the registry, and clients stop routing to them. Put this setting in `application.yml`, not in a bootstrap file.
yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: when-authorized

eureka:
  client:
    healthcheck:
      enabled: true               # send Actuator health status to Eureka
  instance:
    lease-renewal-interval-in-seconds: 30        # heartbeat frequency (default 30)
    lease-expiration-duration-in-seconds: 90     # evict after this long without a heartbeat (default 90)

Custom Health Indicator

Add a `HealthIndicator` for dependencies that decide whether the instance can serve traffic. Be careful about making health depend on **other services**: if every service marks itself `DOWN` when a neighbour is down, one failure can ripple through the whole system. Check only what this instance truly needs, such as its own database.
Java
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient client;

    public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
        this.client = client;
    }

    @Override
    public Health health() {
        try {
            client.ping();
            return Health.up().withDetail("payment-gateway", "reachable").build();
        } catch (Exception ex) {
            return Health.down().withDetail("payment-gateway", "unreachable").build();
        }
    }
}

Why Clients Still Call Dead Instances (Timing)

Eureka is **eventually consistent** and favours availability. Several caches and timers sit between "instance crashed" and "clients stop calling it": | Step | Default | Effect | |---|---|---| | Instance heartbeat | 30 s | Detection starts after missed heartbeats | | Lease expiration | 90 s | Instance becomes eligible for eviction | | Server eviction timer | 60 s | Eviction runs on a timer, not instantly | | Server response cache | about 30 s | Clients may be served a cached registry | | Client registry fetch | 30 s | Client refreshes its local copy periodically | | Load balancer instance cache | about 35 s | Spring Cloud LoadBalancer caches instance lists | These add up, so a crashed instance can keep receiving traffic for **a few minutes**. The numbers are defaults and depend on your versions, so verify them in the reference documentation. Don't just shrink every timer, which increases load and risks false evictions. Design for it instead: - **Retry** failed calls on another instance. - Use a **circuit breaker** and timeouts so a dead instance fails fast. - Make operations **idempotent** where retries are possible. - **Deregister gracefully** on shutdown so planned stops are immediate.
yaml
# Faster feedback for local development only
eureka:
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 5000
  client:
    registry-fetch-interval-seconds: 5
  instance:
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 10

Self-Preservation Mode

Self-preservation protects the registry from wrongly emptying itself during a network problem. If Eureka suddenly receives far fewer heartbeats than expected, it assumes a **network partition** rather than mass failure, and **stops evicting** instances. Stale entries stay until heartbeats return. - It activates when renewals fall below the threshold (85% of expected by default). - The dashboard then shows an "EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT" banner. During a partition that is expected, not a bug. - **Keep it on in production.** Turn it off only in local development, where stale entries are annoying. - It can also appear on a **small** setup, for example with one or two instances and a modest expected heartbeat count, because the threshold is computed from the registered instances. A single instance restarting can trip it. - With self-preservation on, clients may receive dead instances, which is another reason to use retries and circuit breakers.
yaml
# Eureka server application.yml
eureka:
  server:
    enable-self-preservation: true          # default; keep on in production
    renewal-percent-threshold: 0.85         # default
    eviction-interval-timer-in-ms: 60000    # default

Customizing Load Balancing

Spring Cloud LoadBalancer uses round-robin by default. To change the algorithm for one service, register a configuration with `@LoadBalancerClient`. **Do not annotate the configuration class with `@Configuration`**, and keep it outside component-scanned packages. If it is scanned, it applies to **every** load-balanced client instead of just the one you named.
Java
// Not annotated with @Configuration, so component scanning doesn't pick it up globally
public class RandomLoadBalancerConfig {

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
            Environment env, LoadBalancerClientFactory factory) {
        String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(
            factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

@Configuration
@LoadBalancerClient(name = "user-service", configuration = RandomLoadBalancerConfig.class)
public class LoadBalancerClientsConfig { }

Service Discovery on Kubernetes

Kubernetes provides **server-side** discovery built in. A `Service` gets a stable virtual IP and a DNS name, and `kube-proxy` forwards traffic to healthy Pods. Readiness probes decide which Pods receive traffic, so you rarely need Eureka. - Calls within the **same namespace** can use the short name `http://user-service`. From another namespace, use `user-service.<namespace>` or the full `user-service.<namespace>.svc.cluster.local`. - Point your readiness probe at `/actuator/health/readiness` (see the Actuator page). - If you do want discovery inside Spring code on Kubernetes, use Spring Cloud Kubernetes instead of running Eureka. - Don't run Eureka **and** rely on Kubernetes Services for the same traffic. Pick one.
yaml
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 8081

# Another service in the same namespace calls:
#   http://user-service/api/users/1

Docker and Container Settings

By default, an instance registers using its host name, which inside a container is often a random ID that other containers can't resolve. Register the IP instead, or set an explicit host name. If the container's IP isn't reachable from other services (for example behind port mapping), set the address explicitly.
yaml
eureka:
  instance:
    prefer-ip-address: true
    # hostname: user-service          # alternative: an explicit resolvable name
    # ip-address: 10.0.0.5            # when the auto-detected IP isn't reachable
  client:
    service-url:
      defaultZone: http://eureka-server:8761/eureka/    # the Compose service name

Securing Eureka

An open Eureka server lets anyone register a fake service or read your topology. In production: - Protect the server with Spring Security and HTTP Basic or mTLS. - Put credentials in the client URL, supplied from the environment. - Keep the server on an internal network only. - Disable CSRF for the `/eureka/**` paths, because registration uses non-browser POST and DELETE requests.
yaml
# Client
eureka:
  client:
    service-url:
      defaultZone: http://${EUREKA_USER}:${EUREKA_PASSWORD}@eureka-server:8761/eureka/

// Server
@Configuration
@EnableWebSecurity
public class EurekaSecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.ignoringRequestMatchers("/eureka/**"))
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults())
            .build();
    }
}

Testing Services That Use Discovery

Don't start a real registry in unit and slice tests. Switch discovery off so tests start fast and don't try to connect to Eureka. For tests that call another service, use a fixed URL (for example a WireMock server) instead of a service name.
yaml
# src/test/resources/application-test.yml
eureka:
  client:
    enabled: false
spring:
  cloud:
    discovery:
      enabled: false

// or on a single test class
@SpringBootTest(properties = "eureka.client.enabled=false")
class UserClientTest { }

Troubleshooting Service Discovery

| Symptom | Usual cause | Fix | |---|---|---| | `UnknownHostException: user-service` | HTTP client isn't load-balanced | Add `@LoadBalanced` to the builder, or use OpenFeign | | `No instances available for user-service` | Service not registered yet, name mismatch, or the registry hasn't propagated | Check the Eureka dashboard; match `spring.application.name`; wait for the fetch interval | | Service registers with an unreachable host name | Container host name used | Set `prefer-ip-address: true` or an explicit `hostname` | | Instance shows `UP` while its database is down | Health check propagation off | Set `eureka.client.healthcheck.enabled: true` | | Calls go to instances that are already down | Registry and load balancer caches, or self-preservation | Use retries, circuit breakers and timeouts; check the self-preservation banner | | "EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING..." banner | Self-preservation active | Expected during a partition; check network and instance count | | Eureka server logs errors about itself or its peers | Standalone server trying to register or fetch | Set `register-with-eureka` and `fetch-registry` to `false` | | Same custom load balancer applies to all services | Configuration class annotated `@Configuration` or component-scanned | Remove the annotation; register it with `@LoadBalancerClient` | | Tests fail connecting to Eureka | Discovery enabled in tests | Set `eureka.client.enabled=false` | | 401 or 403 when registering | Eureka secured, client has no credentials or CSRF blocks it | Add credentials to `defaultZone`; ignore CSRF on `/eureka/**` | | Using Eureka on Kubernetes causes confusion | Two discovery systems for the same traffic | Use Kubernetes Services, or Spring Cloud Kubernetes |

Common Mistakes

- Calling `http://service-name` with a client that is not load-balanced. - Assuming Eureka uses your Actuator health without enabling `healthcheck.enabled`. - Running a single Eureka server in production. - Turning self-preservation off in production to "fix" the dashboard warning. - Annotating a custom load balancer configuration with `@Configuration`. - Expecting dead instances to disappear instantly, and skipping retries and circuit breakers. - Making every service's health depend on its neighbours, which spreads failures. - Running Eureka on Kubernetes alongside Kubernetes Services without a reason. - Leaving the registry unsecured. - Hard-coding addresses "just for now" and never removing them.

Which Discovery Approach Should You Choose?

- **Deploying on Kubernetes:** use Services and DNS. Add Spring Cloud Kubernetes only if code needs instance details. - **VMs, Docker Compose or mixed hosting with Spring services:** Eureka, run as two or more peers, secured. - **Need health checks and a key-value store in one tool:** consider Consul. - **Few services and simple needs:** DNS or a managed load balancer may be enough. - **Whatever you choose:** add timeouts, retries and a circuit breaker, because no registry is instant.