Service Discovery in Microservices
In a microservices architecture, an application is divided into many small, independent services. Each service performs a specific business task and talks to other services over the network. Because these services start, stop, scale and move all the time, knowing where each one is running becomes a real challenge. This is where service discovery plays a very important role.
This guide explains what service discovery is, how it works, the client-side and server-side approaches, and how to set it up step by step with Spring Cloud Netflix Eureka, a load-balanced RestClient, the API gateway and Kubernetes.
1. What Is Service Discovery?
Service discovery is a mechanism that lets microservices find and talk to each other by name, without hard-coding network locations such as IP addresses and port numbers.
In simple words, service discovery works like a phone directory for microservices. Instead of remembering phone numbers, services look up a name, such as payment-service, and automatically get the current address.
Without discovery: http://10.0.3.17:8083/payments
(breaks when the IP address changes)
With discovery: http://payment-service/payments
(always finds a running instance)
2. Why Do We Need Service Discovery?
In traditional applications, services usually run on fixed servers with fixed IP addresses. In microservices, this does not work well:
- Services are frequently scaled up or down; the number of instances changes during the day.
- Containers restart or move to other machines.
- IP addresses and ports change at any time, especially in the cloud and in Kubernetes.
- Failed instances must be removed quickly, so no traffic is sent to them.
If services depend on hard-coded addresses, communication fails whenever something changes. Service discovery solves this by managing service locations dynamically.
3. How Service Discovery Works
Service discovery usually involves three main steps:
3.1 Service registration
When a service instance starts, it registers itself with a service registry, providing information such as:
- Service name, for example
payment-service - IP address and port number
- Health status and optional metadata, such as version or zone
While it runs, the instance sends regular heartbeats to say “I am still alive”. When it shuts down cleanly, it deregisters; if it crashes, the registry removes it after the heartbeats stop.
3.2 Service lookup
When one service wants to call another, it does not use a fixed address. It asks the registry: “Where is payment-service?” The registry returns the current list of healthy instances.
3.3 Service communication
The caller picks one instance, usually with load balancing, and sends the request to it. All of this happens automatically and transparently.
[ Service Registry ]
^ ^ |
1. register | | 2. | 3. list of
+ heartbeats | | lookup| instances
| | v
Payment Service x3 Order Service
10.0.3.17:8083 <-------------+
10.0.3.21:8083 4. call one instance
10.0.4.09:8083 (load balanced)
4. Client-Side vs Server-Side Discovery
4.1 Client-side service discovery
The calling service is responsible for discovery. It:
- Queries the service registry (usually from a local, regularly refreshed cache).
- Selects an available instance using built-in load balancing.
- Sends the request directly to that instance.
Example: Netflix Eureka with Spring Cloud LoadBalancer.
4.2 Server-side service discovery
The caller does not talk to the registry. It sends the request to a load balancer, router or gateway, which:
- Queries the service registry.
- Selects an available instance.
- Forwards the request to it.
This keeps the client simple and hides discovery logic. Examples: Kubernetes Services, AWS Elastic Load Balancing, and an API gateway in front of your services.
| Client-side discovery | Server-side discovery | |
|---|---|---|
| Who looks up instances? | The calling service | A load balancer, router or gateway |
| Network hops | Direct call: one hop | Extra hop through the load balancer |
| Client complexity | Needs a discovery client library | Simple: calls one stable address |
| Language support | Needs a library for each language | Works with any language |
| Load-balancing control | Fine-grained, in the client | Configured centrally |
| Typical tools | Eureka + Spring Cloud LoadBalancer, Consul | Kubernetes Services, cloud load balancers, API gateways |
5. The Service Registry
The service registry is the central database that keeps track of all running service instances and their health. Its main responsibilities:
- Registering new instances as they start.
- Removing stopped or unhealthy instances.
- Providing up-to-date location details to callers.
Because every service depends on it, the registry must be highly available, usually run as a cluster of several nodes. Learn more in the Service Registry guide.
6. Popular Service Discovery Tools
| Tool | Type | Highlights |
|---|---|---|
| Netflix Eureka | Client-side registry | Easy Spring Cloud integration; favors availability; popular in Java systems |
| HashiCorp Consul | Registry + DNS + health checks | Works with any language; also offers key-value configuration and service mesh features |
| Apache ZooKeeper | Coordination service | Strongly consistent; often used by older or Hadoop/Kafka-related systems |
| etcd | Key-value store | Strongly consistent; the data store behind Kubernetes |
| Kubernetes Services + DNS | Built-in, server-side | No extra tool needed when running on Kubernetes |
| Cloud services | Managed | AWS Cloud Map, Azure and Google Cloud service directories |
If you run on Kubernetes, its built-in discovery is usually enough. Outside Kubernetes, Eureka is a natural choice for Spring teams and Consul for mixed-language environments.
7. Setting Up a Eureka Server
Let us build service discovery with Spring Cloud Netflix Eureka. First, the registry itself is a small Spring Boot application.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
Import the Spring Cloud BOM (spring-cloud-dependencies) in <dependencyManagement>, so the version matches your Spring Boot version.
package com.example.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
server:
port: 8761
spring:
application:
name: discovery-server
eureka:
client:
register-with-eureka: false # the server does not register with itself
fetch-registry: false
Start it and open http://localhost:8761 to see the Eureka dashboard, which lists every registered service and instance.
8. Registering Services with Eureka
Each microservice adds the Eureka client starter. Spring Boot then registers the service automatically at startup, using spring.application.name as its service ID:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
server:
port: 0 # random free port: run many instances on one machine
spring:
application:
name: payment-service # the name other services use to find it
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
# A unique ID per instance, needed when the port is random
instance-id: ${spring.application.name}:${random.value}
Start the payment service two or three times. Each instance gets its own port and appears on the Eureka dashboard under PAYMENT-SERVICE. No address is written anywhere in the callers’ configuration.
9. Calling Services by Name
The order service calls the payment service by name. Marking a RestClient.Builder with @LoadBalanced tells Spring Cloud LoadBalancer to replace the service name with a real instance address on every call:
package com.example.order.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
public class ClientConfig {
@Bean
@LoadBalanced
public RestClient.Builder loadBalancedRestClientBuilder() {
return RestClient.builder();
}
}
package com.example.order.client;
import java.math.BigDecimal;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class PaymentClient {
private final RestClient restClient;
public PaymentClient(RestClient.Builder loadBalancedBuilder) {
// "payment-service" is a service name, not a host name
this.restClient = loadBalancedBuilder.baseUrl("http://payment-service").build();
}
public PaymentResult charge(String orderId, BigDecimal amount) {
return restClient.post()
.uri("/payments")
.body(new PaymentRequest(orderId, amount))
.retrieve()
.body(PaymentResult.class);
}
public record PaymentRequest(String orderId, BigDecimal amount) {
}
public record PaymentResult(String transactionId, String status) {
}
}
Each call goes to one of the registered payment instances, by default in round-robin order. If an instance disappears from the registry, it simply stops receiving calls. Spring Cloud LoadBalancer works the same way with WebClient and with OpenFeign clients.
10. Using DiscoveryClient Directly
Sometimes you need the list of instances yourself, for example to show them on an admin page or to call every instance. Inject DiscoveryClient:
package com.example.order.admin;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class InstanceController {
private final DiscoveryClient discoveryClient;
public InstanceController(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
@GetMapping("/admin/services")
public List<String> services() {
return discoveryClient.getServices();
}
@GetMapping("/admin/services/{name}")
public List<String> instances(@PathVariable String name) {
return discoveryClient.getInstances(name).stream()
.map(ServiceInstance::getUri)
.map(Object::toString)
.toList();
}
}
[
"http://10.0.3.17:52114",
"http://10.0.3.17:52190",
"http://10.0.4.9:50873"
]
DiscoveryClient is an abstraction: the same code works with Eureka, Consul, ZooKeeper or Kubernetes, depending on which discovery starter is on the classpath.
11. Health Checks and Heartbeats
A registry is only useful if it lists healthy instances. Eureka uses heartbeats and leases:
| Setting | Default | Meaning |
|---|---|---|
eureka.instance.lease-renewal-interval-in-seconds | 30 | How often an instance sends a heartbeat |
eureka.instance.lease-expiration-duration-in-seconds | 90 | How long without heartbeats before the instance is removed |
eureka.client.registry-fetch-interval-seconds | 30 | How often clients refresh their local copy of the registry |
eureka.client.healthcheck.enabled | false | When true, the instance reports its Actuator health status to Eureka |
eureka:
client:
healthcheck:
enabled: true # an instance whose /actuator/health is DOWN is marked DOWN in Eureka
Because of these intervals, a crashed instance can stay in some clients’ caches for a short while. That is why callers should also use timeouts and retries: if one instance does not answer, the next attempt goes to another.
12. Service Discovery with the API Gateway
The API gateway is a typical example of server-side discovery. External clients call the gateway, and the gateway uses the registry to find instances. In Spring Cloud Gateway, the lb:// scheme means “look this name up in service discovery and load-balance”:
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: payments
uri: lb://payment-service
predicates:
- Path=/api/payments/**
filters:
- StripPrefix=1
- id: orders
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
The gateway also needs the Eureka client starter. Clients only ever know the gateway’s address; everything behind it can scale and move freely.
13. Service Discovery in Kubernetes
Kubernetes has built-in, server-side discovery. A Service gives a set of pods one stable name and virtual IP, and Kubernetes DNS resolves that name:
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
selector:
app: payment-service # sends traffic to pods with this label
ports:
- port: 80 # port other services call
targetPort: 8083 # port the container listens on
Other pods simply call http://payment-service (or http://payment-service.shop.svc.cluster.local from another namespace). Kubernetes keeps the list of healthy pods up to date using readiness probes, and spreads requests across them.
// No discovery library and no @LoadBalanced: Kubernetes DNS and Services do the work
this.restClient = RestClient.builder()
.baseUrl("http://payment-service")
.build();
On Kubernetes, most teams therefore do not run Eureka. If you want the Spring DiscoveryClient API on Kubernetes, the Spring Cloud Kubernetes project provides an implementation that reads services from the Kubernetes API.
14. Real-World Example: An Online Shop
Imagine an online shop with an order service, a payment service and an inventory service:
- Each service registers with the registry when it starts.
- The order service needs the payment service, but it does not know where it is running. It asks for
payment-serviceby name. - The registry returns the current instances, and the load balancer picks one.
- During a sale, the payment service scales from 2 to 6 instances. The new instances register themselves, and the order service starts using them within seconds, with no configuration change.
- One payment instance crashes. Its heartbeats stop, it is removed from the registry, and calls go only to the healthy instances.
15. Benefits and Challenges
| Benefits | Challenges |
|---|---|
| Dynamic scaling: instances can be added or removed without configuration changes | The registry is a critical component and must be highly available |
| High availability: failed instances are removed automatically | Short delays mean callers can briefly see stale instances |
| Loose coupling: services depend on names, not addresses | Another component to run and monitor (unless you use Kubernetes) |
| Built-in load balancing across instances | Client-side discovery needs a library for every language |
| Easier maintenance of large, changing systems | Network problems can split the registry’s view of the system |
16. Best Practices
- Use your platform’s discovery first: on Kubernetes, rely on Services and DNS before adding another tool.
- Run the registry as a cluster of at least two or three nodes.
- Give services clear, stable names, such as
payment-service, and keep them consistent across environments. - Connect health checks to discovery, so unhealthy instances stop receiving traffic.
- Always set timeouts and retries on calls, because registries can be briefly out of date.
- Deregister cleanly on shutdown using graceful shutdown, so traffic stops before the instance disappears.
- Hide internal discovery from external clients behind an API gateway.
- Monitor the registry: number of instances per service, failed heartbeats and registry availability.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| Hard-coding IP addresses | Calls break whenever instances move or scale | Call services by name through discovery |
| A single registry node | If it fails, new instances cannot be found | Run a registry cluster |
| No timeouts or retries | Calls to a just-crashed instance hang or fail | Set timeouts and retry on another instance |
| Health checks not connected | Broken but running instances still get traffic | Enable health checks in the registry or readiness probes |
| Running Eureka on Kubernetes without need | Two discovery systems that can disagree | Use Kubernetes Services, or Spring Cloud Kubernetes |
| Inconsistent service names | Lookups fail in some environments | Agree on naming rules and keep them stable |
| Exposing every service publicly | Larger attack surface; clients depend on internals | Expose only the gateway |
18. Summary
Service discovery is a core building block of microservices. It lets services find and talk to each other by name instead of fixed addresses: instances register with a service registry, send heartbeats while they run, and callers look up healthy instances whenever they need them. In client-side discovery, such as Eureka with Spring Cloud LoadBalancer, the caller picks an instance; in server-side discovery, such as Kubernetes Services or an API gateway, a router does it for you. With a few lines of Spring Cloud configuration, your services can scale up, scale down and recover from failures without any configuration changes, making the whole system more scalable, resilient and easier to manage. Continue with the Service Registry guide to learn more about the directory at the heart of discovery.
19. Frequently Asked Questions (FAQ)
Q1. What is service discovery in simple words?
Service discovery lets microservices find each other by name instead of by fixed IP address and port. Each running instance registers itself in a directory, and other services ask that directory where to send their requests.
It works like a phone book that updates itself whenever someone moves or a new person arrives.
Q2. What is the difference between service discovery and a service registry?
- The service registry is the database: the list of services, their instances and their health.
- Service discovery is the whole process: registering, sending heartbeats, looking up instances and choosing one to call.
The registry is one part of service discovery, just as a phone book is one part of finding someone to call.
Q3. Which is better: client-side or server-side discovery?
Neither is better in general:
- Client-side discovery gives direct calls with no extra hop and fine control over load balancing, but every service needs a discovery library.
- Server-side discovery keeps clients simple and language-independent, at the cost of an extra hop through a load balancer.
On Kubernetes, server-side discovery is built in and usually the best choice. In Spring-only environments outside Kubernetes, client-side discovery with Eureka is common.
Q4. Do I need Eureka if I use Kubernetes?
Usually no. Kubernetes already provides service discovery: each Service gets a stable DNS name, and Kubernetes routes traffic to healthy pods.
Running Eureka as well adds a second, separate registry that can disagree with Kubernetes. If you want Spring’s DiscoveryClient API on Kubernetes, use Spring Cloud Kubernetes instead.
Q5. What happens if the service registry goes down?
It depends on the tool. With Eureka, clients keep a local cache of the registry, so existing services can usually keep calling each other for a while. However:
- New instances cannot register.
- Crashed instances are not removed.
- Services that just started cannot find anything.
That is why the registry should always run as a cluster of several nodes.
Q6. How quickly does service discovery notice a crashed instance?
With Eureka’s defaults, an instance is removed after about 90 seconds without heartbeats, and clients refresh their local copy every 30 seconds. So a crashed instance can still receive some calls for up to a couple of minutes.
You can shorten these intervals, but always protect calls with timeouts and retries, so a request that hits a dead instance is quickly retried on a healthy one. Kubernetes readiness probes usually react faster.
Q7. What is Eureka self-preservation mode?
If Eureka suddenly stops receiving a large share of heartbeats, it assumes a network problem rather than many services crashing at once. It then stops removing instances until heartbeats return.
This prevents the registry from deleting healthy services during a network glitch. The downside is that truly dead instances may stay listed for longer, so callers need timeouts and retries.
Q8. What does @LoadBalanced do?
@LoadBalanced marks a RestClient.Builder, WebClient.Builder or RestTemplate bean so that Spring Cloud LoadBalancer intercepts its calls.
When you call http://payment-service/payments, it looks up payment-service in service discovery, picks one instance, and replaces the name with that instance’s real host and port before sending the request.
Q9. Which load-balancing strategy does Spring Cloud LoadBalancer use?
By default it uses round-robin: requests go to each instance in turn. A random strategy is also included, and you can write your own, for example to prefer instances in the same zone.
For most services round-robin is fine. Combine it with health checks, so only healthy instances are in the list, and with retries, so a failed instance is skipped.
Q10. Can services written in different languages use the same discovery?
Yes. Options include:
- Server-side discovery, such as Kubernetes Services or a load balancer, which works with any language.
- Consul, which offers an HTTP API and DNS interface usable from any language.
- Eureka through its REST API, or client libraries available for several languages.
Q11. What is DNS-based service discovery?
Services are found through normal DNS names, such as payment-service.shop.svc.cluster.local. Clients need no special library: any HTTP client that can resolve a host name works.
Kubernetes and Consul both offer DNS-based discovery. A limitation is that DNS answers can be cached, so changes may take a little time to reach every client.
Q12. How does service discovery handle different environments?
Use the same service names in every environment, but separate registries or namespaces:
- One registry or Kubernetes namespace per environment, such as
dev,testandprod. - The registry address comes from configuration, such as an environment variable.
This way the code never changes, and a test service can never accidentally call a production service.
Q13. Is service discovery needed for a few services?
Not always. If you have two or three services with fixed addresses, simple configuration or a reverse proxy can be enough.
Discovery becomes valuable when services scale dynamically, run in containers whose addresses change, or when the number of services grows. On Kubernetes you get it for free, so there is no reason to avoid it there.
Q14. How does service discovery work with an API gateway?
The gateway is a client of service discovery. When it receives a request for /api/payments, it looks up payment-service in the registry, picks an instance and forwards the request.
In Spring Cloud Gateway, you write uri: lb://payment-service in a route. External clients only know the gateway’s address, while services behind it scale and move freely.
Q15. Do I need @EnableDiscoveryClient or @EnableEurekaClient?
Not anymore. In current Spring Cloud versions, adding a discovery starter such as spring-cloud-starter-netflix-eureka-client is enough; the application registers automatically.
@EnableEurekaClient has been removed, and @EnableDiscoveryClient is optional. You can turn registration off with spring.cloud.discovery.enabled=false, which is handy in tests.
Q16. How do I run several instances of a service on one machine?
Give each instance its own port and a unique instance ID:
server:
port: 0 # pick a random free port
eureka:
instance:
instance-id: ${spring.application.name}:${random.value}
Without a unique instance ID, instances on the same host can overwrite each other in the registry.
Q17. How do I make sure traffic stops before an instance shuts down?
- Enable graceful shutdown (
server.shutdown=graceful), so running requests finish. - Let the instance deregister from the registry at the start of shutdown; Spring Cloud does this automatically.
- On Kubernetes, the pod is removed from the Service’s endpoints when it starts terminating; a short pre-stop delay gives load balancers time to notice.
Callers should still retry, in case a request reaches an instance that is just stopping.
Q18. Can service discovery help with zones and regions?
Yes. Instances can register metadata, such as their availability zone. Load balancers can then prefer instances in the same zone as the caller, which reduces latency and cross-zone network costs, and fall back to other zones only when needed.
Eureka supports zones, Spring Cloud LoadBalancer offers a zone-preference option, and Kubernetes provides topology-aware routing.
Q19. How is service discovery related to a service mesh?
A service mesh, such as Istio or Linkerd, places a proxy next to every service. The proxies use the platform’s discovery, usually Kubernetes, to find instances, and then add load balancing, retries, encryption and metrics.
With a mesh, your code just calls a service name, and discovery and load balancing happen entirely in the proxies. It builds on discovery rather than replacing it.
Q20. How do I test code that uses service discovery?
- In unit and slice tests, disable discovery with
spring.cloud.discovery.enabled=falseand point clients to a mock server such as WireMock. - In integration tests, Spring Cloud LoadBalancer supports a simple configured list of instances, so you can test load balancing without a real registry.
- Run a small end-to-end environment, for example with Docker Compose, to test real registration and lookups.