Service Discovery with Eureka

In a microservices environment, services scale up and down dynamically. You can’t hardcode IP addresses — a service running on 10 pods today has 10 different addresses. Service discovery solves this: services register themselves, and clients look up live instances by name.

What Service Discovery Does

Without service discovery:
  Order Service → http://192.168.1.45:8081/api/inventory  ← hardcoded, breaks when IP changes

With service discovery:
  Order Service → "inventory-service" → Eureka → [192.168.1.45:8081, 192.168.1.46:8081]
                                                    ↑ picks one, load-balances

Eureka is Spring Cloud’s service registry. Services register on startup, send heartbeats to stay listed, and deregister on shutdown.

Eureka Server

<!-- pom.xml — eureka-server -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceRegistryApplication.class, args);
    }
}
# application.yml — eureka-server
server:
  port: 8761

spring:
  application:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false   # don't register with itself
    fetch-registry: false
  server:
    wait-time-in-ms-when-sync-empty: 0   # faster startup in dev

Visit http://localhost:8761 — the Eureka dashboard shows registered services.

Service Registration (Eureka Client)

Every microservice includes:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
# application.yml — order-service
spring:
  application:
    name: order-service    # ← this is the service name others use to find it

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30
    health-check-url-path: /actuator/health
    metadata-map:
      version: ${project.version}
      zone: us-east-1a
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
    // No annotation needed — eureka-client auto-configures registration
}

On startup, order-service registers itself with Eureka, sends heartbeats every 10 seconds, and deregisters on graceful shutdown.

Client-Side Load Balancing with Spring Cloud LoadBalancer

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
@Configuration
public class RestClientConfig {

    @Bean
    @LoadBalanced   // ← enables service-name resolution
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}

@Service
@RequiredArgsConstructor
public class InventoryClient {

    private final RestClient.Builder restClientBuilder;

    public boolean checkStock(UUID productId, int quantity) {
        // "inventory-service" is resolved via Eureka to a real IP:port
        return restClientBuilder.build()
            .get()
            .uri("http://inventory-service/api/inventory/{productId}/check?quantity={qty}",
                productId, quantity)
            .retrieve()
            .body(Boolean.class);
    }
}

@LoadBalanced intercepts the RestClient call, resolves inventory-service to a live instance from Eureka, and applies round-robin load balancing across all instances.

Health-Aware Routing

Eureka uses heartbeats to detect unhealthy instances. When an instance stops sending heartbeats (crash, network partition), Eureka removes it from the registry after lease-expiration-duration-in-seconds.

But you can also feed Actuator health status into Eureka’s awareness:

eureka:
  client:
    healthcheck:
      enabled: true   # Eureka reads /actuator/health status

With this enabled, if your service marks itself as DOWN (e.g., database connection lost), Eureka immediately removes it from the live instance list — traffic stops routing to it.

@Component
public class DatabaseHealthContributor implements HealthIndicator {

    @Override
    public Health health() {
        try {
            dataSource.getConnection().isValid(1);
            return Health.up().build();
        } catch (SQLException e) {
            // This DOWN status propagates to Eureka, removes instance from routing
            return Health.down().withException(e).build();
        }
    }
}

Custom Load Balancing

By default, Spring Cloud LoadBalancer uses round-robin. Customize it:

@Configuration
@LoadBalancerClient(name = "inventory-service", configuration = InventoryLoadBalancerConfig.class)
public class LoadBalancerConfig {}

@Configuration
public class InventoryLoadBalancerConfig {

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

Or zone-aware routing — prefer instances in the same availability zone:

@Bean
public ServiceInstanceListSupplier zoneAwareSupplier(
        ConfigurableApplicationContext context) {
    return ServiceInstanceListSupplier.builder()
        .withDiscoveryClient()
        .withZonePreference()   // prefer same-zone instances
        .withHealthChecks()
        .build(context);
}

Multiple Eureka Servers (High Availability)

In production, run Eureka in a cluster:

# eureka-server-1
eureka:
  instance:
    hostname: eureka1.devopsmonk.com
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka2.devopsmonk.com:8761/eureka/

# eureka-server-2
eureka:
  instance:
    hostname: eureka2.devopsmonk.com
  client:
    service-url:
      defaultZone: http://eureka1.devopsmonk.com:8761/eureka/

Each server registers with the other. Clients configure both:

eureka:
  client:
    service-url:
      defaultZone: http://eureka1.devopsmonk.com:8761/eureka/,http://eureka2.devopsmonk.com:8761/eureka/

If one Eureka server fails, clients still discover instances through the other.

Eureka vs Kubernetes Service Discovery

If you’re running on Kubernetes, you don’t need Eureka — Kubernetes has built-in service discovery via DNS and Service objects:

FeatureEurekaKubernetes
Service registryExplicit registrationAutomatic via labels/selectors
DNS-based discoveryNoYes (service-name.namespace.svc.cluster.local)
Health checksHeartbeat + /healthLiveness/readiness probes
Load balancingClient-sidekube-proxy (server-side)
Extra infrastructureYes (Eureka server)No (built-in)

On Kubernetes: use spring-cloud-starter-kubernetes-discoveryclient instead:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
</dependency>

Services are discovered by their Kubernetes Service name — no Eureka server needed.

What You’ve Learned

  • Eureka Server maintains a registry of live service instances
  • Clients register with eureka-client dependency — no annotation required
  • @LoadBalanced on RestClient.Builder resolves service names to real instances via Eureka
  • Actuator health status propagates to Eureka when healthcheck.enabled: true — unhealthy instances are removed from routing
  • Run 2+ Eureka servers in production — clients configure all servers for resilience
  • On Kubernetes, use Kubernetes-native service discovery instead of Eureka

Next: Article 48 — API Gateway with Spring Cloud Gateway — route, filter, and secure all external traffic through one entry point.