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:
| Feature | Eureka | Kubernetes |
|---|---|---|
| Service registry | Explicit registration | Automatic via labels/selectors |
| DNS-based discovery | No | Yes (service-name.namespace.svc.cluster.local) |
| Health checks | Heartbeat + /health | Liveness/readiness probes |
| Load balancing | Client-side | kube-proxy (server-side) |
| Extra infrastructure | Yes (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-clientdependency — no annotation required @LoadBalancedonRestClient.Builderresolves 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.