Service Discovery & Registration
Client-side versus server-side discovery, running Eureka properly including self-preservation, Consul as an alternative, and why Kubernetes usually makes a separate registry unnecessary.
On this page
Service discovery answers one question: given a logical service name, which network addresses are currently healthy? Everything else — load balancing, failover, rolling deploys — depends on that answer being fresh.
Key Takeaways
- Client-side discovery: the caller queries a registry and balances itself. Server-side: a load balancer does it.
- Eureka is AP, not CP — it prefers returning slightly stale data to returning nothing.
- Self-preservation stops mass eviction during a network partition. Leave it on in production.
- On Kubernetes, a
Serviceplus DNS replaces the registry entirely. - Whichever you use, the caller still needs timeouts and a circuit breaker.
The two models
Client-side is what Spring Cloud offers natively. The trade-off is that every service needs a discovery client, which is fine in a homogeneous Java estate and becomes a burden the moment a Python or Go service joins.
Registration itself comes in two flavours worth distinguishing, because they fail differently. Self-registration has the instance announce itself and send heartbeats, which is what Eureka does — simple, and it couples every service to the registry's client library and its availability at startup. Third-party registration has the platform register instances on the service's behalf, which is how Kubernetes works: the kubelet reports readiness and the endpoint list updates without the application knowing a registry exists. The second scales better across languages precisely because the application contributes nothing but a health endpoint.
Eureka
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}server:
port: 8761
eureka:
instance:
hostname: eureka-1
client:
# A server should not register with itself, but SHOULD replicate to peers.
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: 'http://eureka-2:8761/eureka/,http://eureka-3:8761/eureka/'
server:
# Stops mass eviction when the network, not the instances, is the problem.
enable-self-preservation: true
renewal-percent-threshold: 0.85
eviction-interval-timer-in-ms: 15000spring:
application:
name: payment-service # this becomes the discovery name
eureka:
client:
service-url:
defaultZone: 'http://eureka-1:8761/eureka/'
registry-fetch-interval-seconds: 10
instance:
prefer-ip-address: true # essential in containers, where hostnames rarely resolve
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30
# Point Eureka at the readiness endpoint so it reflects real health.
health-check-url-path: /actuator/health/readinessThen call by name and let spring-cloud-loadbalancer resolve it:
@Bean
@LoadBalanced
RestClient.Builder loadBalancedRestClient() {
return RestClient.builder();
}
@Service
public class PaymentClient {
private final RestClient client;
public PaymentClient(@LoadBalanced RestClient.Builder builder) {
// The hostname is the registered application name, not a real host.
this.client = builder.baseUrl("http://payment-service").build();
}
public Receipt charge(ChargeRequest request) {
return client.post().uri("/v2/charges").body(request).retrieve().body(Receipt.class);
}
}Eventual consistency, and what it costs
Eureka chooses availability over consistency, which has practical consequences worth planning for. Registration takes up to 30 seconds to propagate through server caches and client fetch intervals, so a freshly started instance receives no traffic for a while. Worse, a stopped instance stays in the registry for a similar window, so callers will try dead addresses.
There is no configuration that eliminates this — it is inherent to a heartbeat-and-cache design. The mitigation is on the caller: short connection timeouts, a retry that tries the next instance, and a circuit breaker so a persistently dead instance stops being selected. Discovery tells you where a service probably is; resilience handles the case where it is not.
Deregister explicitly on shutdown so the window is as short as possible. Spring does this
automatically on a graceful shutdown, which is another reason SIGTERM handling matters.
Consul
Consul adds richer health checking — HTTP, TCP, script and TTL checks — plus a key/value store and multi-datacenter support. It is CP rather than AP, so it will refuse to answer rather than answer staleley during a partition.
spring:
cloud:
consul:
host: consul.internal
port: 8500
discovery:
prefer-ip-address: true
health-check-path: /actuator/health/readiness
health-check-interval: 10s
# Consul removes an instance failing checks for this long.
health-check-critical-timeout: 60s
tags:
- 'version=${app.version}'
- 'zone=${app.zone}'Tags are genuinely useful: they let a caller filter to a version or an availability zone, which is how you do canary routing or zone-aware balancing without a service mesh.
Kubernetes
On Kubernetes, discovery is already solved. A Service gives a stable DNS name and a virtual IP;
kube-proxy load balances across the pod endpoints, and readiness probes control membership.
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
selector: { app: payment-service }
ports: [{ port: 80, targetPort: 8080 }]// No discovery client, no registry, no annotations. Just DNS.
RestClient.builder().baseUrl("http://payment-service.default.svc.cluster.local").build();Running Eureka inside Kubernetes gives you two registries that can disagree, two sets of health semantics, and a failure mode where Kubernetes has removed a pod while Eureka still advertises it. Unless you are mid-migration, use the platform's discovery.
spring-cloud-kubernetes is worth knowing about for the middle ground: it implements the Spring
DiscoveryClient interface backed by the Kubernetes API, so existing @LoadBalanced code keeps
working while the source of truth becomes the cluster.
Zone awareness
In a multi-zone deployment, cross-zone traffic costs money and adds latency. Both Eureka and
Kubernetes support preferring same-zone instances — Eureka through
prefer-same-zone-eureka, Kubernetes through topology-aware routing. Enable it once you span zones,
and make sure the fallback to other zones still works, or a single-zone outage becomes a total one.
What to take away
Use the platform's discovery when you have one — on Kubernetes that means Services and DNS. Reach for Eureka or Consul when you do not. Whichever you choose, remember the registry's view is always slightly stale, and put the real reliability in timeouts, retries against the next instance, and a circuit breaker.
Frequently Asked Questions
Do I still need Eureka on Kubernetes?
What is Eureka self-preservation and should I disable it?
Client-side or server-side discovery?
Related tutorials
- Microservices Decomposition PatternsFinding service boundaries that hold: decomposing by business capability and subdomain, context mapping patterns, the anti-corruption layer, and the strangler fig migration.
- API Gateway with Spring Cloud GatewayBuilding an edge gateway: route predicates, the filter catalogue, custom global filters for auth and correlation, Redis rate limiting, and circuit breakers at the edge.
- Inter-Service Communication PatternsChoosing how services talk: synchronous REST and gRPC versus asynchronous messaging, the coupling each creates, correlation propagation, and graceful degradation.
- Circuit Breakers & Resilience4jResilience4j in production: how the circuit breaker state machine works, tuning the sliding window, combining retry and bulkhead correctly, and the decorator order that matters.