00:00

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.

Hard-coded address vs discovery
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.

Registration, lookup and communication
                   [ 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:

  1. Queries the service registry (usually from a local, regularly refreshed cache).
  2. Selects an available instance using built-in load balancing.
  3. 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:

  1. Queries the service registry.
  2. Selects an available instance.
  3. 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 discoveryServer-side discovery
Who looks up instances?The calling serviceA load balancer, router or gateway
Network hopsDirect call: one hopExtra hop through the load balancer
Client complexityNeeds a discovery client librarySimple: calls one stable address
Language supportNeeds a library for each languageWorks with any language
Load-balancing controlFine-grained, in the clientConfigured centrally
Typical toolsEureka + Spring Cloud LoadBalancer, ConsulKubernetes 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

ToolTypeHighlights
Netflix EurekaClient-side registryEasy Spring Cloud integration; favors availability; popular in Java systems
HashiCorp ConsulRegistry + DNS + health checksWorks with any language; also offers key-value configuration and service mesh features
Apache ZooKeeperCoordination serviceStrongly consistent; often used by older or Hadoop/Kafka-related systems
etcdKey-value storeStrongly consistent; the data store behind Kubernetes
Kubernetes Services + DNSBuilt-in, server-sideNo extra tool needed when running on Kubernetes
Cloud servicesManagedAWS 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.

pom.xml (discovery-server)
<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.

DiscoveryServerApplication.java
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);
    }
}
application.yml (discovery-server)
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:

pom.xml (payment-service)
<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>
application.yml (payment-service)
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:

ClientConfig.java (order-service)
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();
    }
}
PaymentClient.java (order-service)
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:

InstanceController.java
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();
    }
}
GET /admin/services/payment-service
[
  "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:

SettingDefaultMeaning
eureka.instance.lease-renewal-interval-in-seconds30How often an instance sends a heartbeat
eureka.instance.lease-expiration-duration-in-seconds90How long without heartbeats before the instance is removed
eureka.client.registry-fetch-interval-seconds30How often clients refresh their local copy of the registry
eureka.client.healthcheck.enabledfalseWhen true, the instance reports its Actuator health status to Eureka
application.yml
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.

Self-preservation mode: If Eureka suddenly stops receiving many heartbeats at once, it assumes a network problem rather than many crashed services, and stops removing instances for a while. This protects the registry from wiping itself out during a network glitch, but it also means stale entries can remain, another reason for client-side timeouts and retries.

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”:

application.yml (gateway)
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:

payment-service.yaml
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.

PaymentClient.java (on Kubernetes)
// 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:

  1. Each service registers with the registry when it starts.
  2. The order service needs the payment service, but it does not know where it is running. It asks for payment-service by name.
  3. The registry returns the current instances, and the load balancer picks one.
  4. 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.
  5. 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

BenefitsChallenges
Dynamic scaling: instances can be added or removed without configuration changesThe registry is a critical component and must be highly available
High availability: failed instances are removed automaticallyShort delays mean callers can briefly see stale instances
Loose coupling: services depend on names, not addressesAnother component to run and monitor (unless you use Kubernetes)
Built-in load balancing across instancesClient-side discovery needs a library for every language
Easier maintenance of large, changing systemsNetwork problems can split the registry’s view of the system

16. Best Practices

  1. Use your platform’s discovery first: on Kubernetes, rely on Services and DNS before adding another tool.
  2. Run the registry as a cluster of at least two or three nodes.
  3. Give services clear, stable names, such as payment-service, and keep them consistent across environments.
  4. Connect health checks to discovery, so unhealthy instances stop receiving traffic.
  5. Always set timeouts and retries on calls, because registries can be briefly out of date.
  6. Deregister cleanly on shutdown using graceful shutdown, so traffic stops before the instance disappears.
  7. Hide internal discovery from external clients behind an API gateway.
  8. Monitor the registry: number of instances per service, failed heartbeats and registry availability.

17. Common Mistakes to Avoid

MistakeWhat goes wrongBetter approach
Hard-coding IP addressesCalls break whenever instances move or scaleCall services by name through discovery
A single registry nodeIf it fails, new instances cannot be foundRun a registry cluster
No timeouts or retriesCalls to a just-crashed instance hang or failSet timeouts and retry on another instance
Health checks not connectedBroken but running instances still get trafficEnable health checks in the registry or readiness probes
Running Eureka on Kubernetes without needTwo discovery systems that can disagreeUse Kubernetes Services, or Spring Cloud Kubernetes
Inconsistent service namesLookups fail in some environmentsAgree on naming rules and keep them stable
Exposing every service publiclyLarger attack surface; clients depend on internalsExpose 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.