RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#587 min readJul 11, 2026

Service Discovery.

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Service discovery is the phone book of microservices: instead of chasing changing IP addresses, services ask, who is available right now?

Question: What is service discovery in Spring Boot?

Answer: Service discovery lets one service find another by a logical name like inventory-service, not by a hard-coded host and port. In Spring Boot, this is usually done with Spring Cloud plus a registry such as Eureka or Consul. The registry keeps track of healthy instances, so clients can still find them when pods restart, IPs change, or a service scales up and down.

Interview-Ready Answer: I use service discovery so my services do not depend on fixed IPs. Each instance registers itself with a registry, renews a heartbeat, and clients ask for a service name like inventory-service to get a live instance. In modern Spring Boot setups, this is typically Spring Cloud plus a registry such as Eureka or Consul, and the client usually goes through Spring Cloud LoadBalancer to pick one healthy instance.

🧠 Memory Map
Memory map — visual summary of this topic

What it means

Service discovery solves a simple but painful problem: in microservices, the address of a service changes all the time. A container can restart, a pod can move, or auto-scaling can create more copies. Rather than storing raw URLs in code, the app asks a registry for a current list of instances. In Spring Boot, the registry is usually provided by Spring Cloud integration, not by Spring Boot alone.

How it works under the hood

  1. A service starts and registers itself with the registry using a logical name, host, port, and metadata such as version or zone.
  2. It sends a heartbeat, which is a periodic renewal that says, I am still alive. In Eureka, the common default is every 30 seconds.
  3. The registry keeps a lease, which is a time-limited claim on that instance. If heartbeats stop for about 90 seconds by default in Eureka, the instance is treated as expired.
  4. A consumer asks DiscoveryClient for instances of inventory-service instead of calling a fixed IP.
  5. A load balancer, such as Spring Cloud LoadBalancer, chooses one instance. Common choices are round robin or weighted selection.
  6. The consumer makes the real HTTP call to the chosen instance. Discovery is only the lookup step; it is not the network call itself.
  7. If the call fails, retry and circuit breaker logic can kick in, but discovery alone does not guarantee success.

Spring Boot pieces you should name

  • spring-cloud-starter-netflix-eureka-client for Eureka registration and lookup.
  • spring-cloud-starter-loadbalancer for picking one instance from many.
  • @LoadBalanced on RestTemplate or WebClient so logical service names can be resolved.
  • @EnableDiscoveryClient is often optional in modern Spring Cloud starters, but you still see it in older examples.

Client-side vs server-side vs DNS

ApproachHow it worksBest forTrade-off
Static configHard-coded host and portSmall appsBreaks when IPs change
DNSUse service name from DNSSimple clustersLess metadata and health detail
Client-side discoveryClient queries registrySpring Cloud appsMore client logic
Server-side discoveryGateway queries registryCentral routingExtra hop

When and why to use it

  • Use it when services scale up and down frequently.
  • Use it when IPs are not stable, which is common in containers and Kubernetes.
  • Use it when you want to move from hard-coded URLs to resilient logical names.
  • Do not use it as a replacement for health checks, retries, or circuit breakers. It is one part of resilience, not the whole solution.

Performance and defaults

  • Lookup is typically fast because clients cache registry data locally, so the registry is not hit on every request. The selection step is usually O(1) or close to it; the network call dominates the cost.
  • Eureka client heartbeats are commonly every 30 seconds, registry expiration is commonly about 90 seconds, and registry refresh is often every 30 seconds. That means there can be a short stale window.
  • Memory grows with the number of cached instance records. For example, 200 services with 10 instances each means roughly 2,000 entries in client cache.
  • Older Spring Cloud tutorials may show Ribbon. Modern Spring Cloud uses Spring Cloud LoadBalancer instead.

Edge cases and gotchas

  • A service can be discovered but still be dead. Discovery tells you where to try; it does not prove the next request will succeed.
  • Registry lag matters. During deploys, one node may disappear from reality before it disappears from the cache.
  • If the registry is down, existing clients may still use cached data for a short time, but new registration and refresh operations can fail.
  • If multiple instances exist and your client picks only the first one, you may accidentally create hot spots. Use load balancing rather than always choosing index zero.

Real-world story

Imagine an e-commerce checkout service that must call inventory and payment services. The team first hard-coded URLs like http://10.2.4.19:8081. During a rolling deploy, the inventory pod moved to a new node, the old IP stopped answering, and checkout started failing. Users saw carts spin forever, then get a 503 error at the final step.

After moving to service discovery, checkout asked for inventory-service instead of a fixed IP. The registry returned a live instance, so the app survived pod restarts and scaling. One bad day still happened: the registry kept a stale entry for about a minute after a node died, so some requests hit Connection refused. The fix was to keep discovery plus health checks plus a retry limit, not to rely on discovery alone.

What went wrong in the outage: logs showed No instances available for inventory-service on the consumer and Connection refused on the failed downstream call. The customer impact was direct: checkout failures, abandoned carts, and support tickets that sounded like the system was randomly broken.

Spring Boot
package com.example.servicediscovery;

import java.net.URI;
import java.util.List;
import java.util.Map;

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;

@SpringBootApplication
public class ServiceDiscoveryDemoApplication {

    public static void main(String[] args) {
        // Keep the demo self-contained and predictable.
        SpringApplication app = new SpringApplication(ServiceDiscoveryDemoApplication.class);
        app.setDefaultProperties(Map.of("server.port", "8080"));
        app.run(args);
    }

    @Bean
    DiscoveryClient discoveryClient() {
        return new DiscoveryClient() {
            // In a real app this data comes from Eureka, Consul, Kubernetes, or another registry.
            private final Map<String, List<ServiceInstance>> registry = Map.of(
                    "inventory-service",
                    List.of(new SimpleServiceInstance(
                            "inventory-1",
                            "inventory-service",
                            "localhost",
                            8080,
                            false,
                            Map.of("zone", "local"))),
                    // This entry demonstrates the failure path: the service is discoverable,
                    // but the target port is not listening.
                    "shipping-service",
                    List.of(new SimpleServiceInstance(
                            "shipping-1",
                            "shipping-service",
                            "localhost",
                            9999,
                            false,
                            Map.of("zone", "down"))));

            @Override
            public String description() {
                return "In-memory demo registry";
            }

            @Override
            public List<ServiceInstance> getInstances(String serviceId) {
                return registry.getOrDefault(serviceId, List.of());
            }

            @Override
            public List<String> getServices() {
                return List.copyOf(registry.keySet());
            }

            @Override
            public int getOrder() {
                return 0;
            }
        };
    }

    @Bean
    RestTemplate restTemplate() {
        // Plain RestTemplate is enough here because the registry already resolves the URI.
        return new RestTemplate();
    }

    @Bean
    ApplicationRunner startupLog(DiscoveryClient discoveryClient) {
        return args -> System.out.println("Discovered services: " + discoveryClient.getServices());
    }

    @RestController
    static class DemoController {
        private final DiscoveryClient discoveryClient;
        private final RestTemplate restTemplate;

        DemoController(DiscoveryClient discoveryClient, RestTemplate restTemplate) {
            this.discoveryClient = discoveryClient;
            this.restTemplate = restTemplate;
        }

        @GetMapping("/hello")
        String hello() {
            return "inventory-service says hello";
        }

        @GetMapping("/services")
        Map<String, Object> services() {
            return Map.of(
                    "allServices", discoveryClient.getServices(),
                    "inventoryInstances", discoveryClient.getInstances("inventory-service")
                            .stream()
                            .map(ServiceInstance::getUri)
                            .toList(),
                    "shippingInstances", discoveryClient.getInstances("shipping-service")
                            .stream()
                            .map(ServiceInstance::getUri)
                            .toList());
        }

        @GetMapping("/proxy/{serviceId}")
        ResponseEntity<String> proxy(@PathVariable String serviceId) {
            List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);

            // No instance means discovery worked, but the registry has nothing usable right now.
            if (instances.isEmpty()) {
                throw new ResponseStatusException(
                        HttpStatus.SERVICE_UNAVAILABLE,
                        "No instances found for " + serviceId);
            }

            ServiceInstance chosen = instances.get(0);

            // Discovery only gives us a target. The actual HTTP call can still fail if the instance is dead.
            URI target = chosen.getUri().resolve("/hello");
            try {
                String body = restTemplate.getForObject(target, String.class);
                return ResponseEntity.ok("Discovered " + chosen.getUri() + " and got: " + body);
            } catch (RestClientException ex) {
                throw new ResponseStatusException(
                        HttpStatus.BAD_GATEWAY,
                        "Found " + serviceId + " but could not call it: " + ex.getMessage(),
                        ex);
            }
        }
    }

    static final class SimpleServiceInstance implements ServiceInstance {
        private final String instanceId;
        private final String serviceId;
        private final String host;
        private final int port;
        private final boolean secure;
        private final URI uri;
        private final Map<String, String> metadata;

        SimpleServiceInstance(String instanceId,
                              String serviceId,
                              String host,
                              int port,
                              boolean secure,
                              Map<String, String> metadata) {
            this.instanceId = instanceId;
            this.serviceId = serviceId;
            this.host = host;
            this.port = port;
            this.secure = secure;
            this.uri = URI.create((secure ? "https" : "http") + "://" + host + ":" + port);
            this.metadata = metadata;
        }

        @Override
        public String getServiceId() {
            return serviceId;
        }

        @Override
        public String getHost() {
            return host;
        }

        @Override
        public int getPort() {
            return port;
        }

        @Override
        public boolean isSecure() {
            return secure;
        }

        @Override
        public URI getUri() {
            return uri;
        }

        @Override
        public Map<String, String> getMetadata() {
            return metadata;
        }

        @Override
        public String getInstanceId() {
            return instanceId;
        }

        public String getScheme() {
            return secure ? "https" : "http";
        }
    }
}

Follow-up & Tricky Questions:

  • What is the difference between service discovery and load balancing? Discovery finds candidate instances by name; load balancing chooses one of those instances to call. In Spring Cloud, discovery and load balancing are separate pieces that often work together.
  • What is the difference between Eureka and Spring Cloud LoadBalancer? Eureka is a registry that stores service instances. Spring Cloud LoadBalancer is the client-side component that picks one instance from the list returned by discovery.
  • How does a service know it is still alive? It sends heartbeats or renewals to the registry. If renewals stop for long enough, the registry removes or marks the instance unhealthy.
  • What happens if the registry is down? Existing clients may keep using cached data for a while, but new registrations and refreshes can fail. That is why teams often run the registry in a highly available setup.
  • How do you secure service discovery? Use TLS, authentication, and network policies so only trusted services can register or query the registry. The registry is sensitive infrastructure because it exposes your internal topology.
  • How do you do discovery in Kubernetes? Many teams use Kubernetes service DNS instead of Eureka. Spring Boot apps can still use discovery-style lookup, but the registry is often built into the cluster platform.
  • Is @EnableDiscoveryClient required? Usually not in modern Spring Cloud starters. Many projects still add it for readability, but the starter often auto-configures discovery for you.
  • Can service discovery guarantee the call will succeed? No. It only helps you find a live-looking instance. You still need timeouts, retries, health checks, and circuit breakers for real resilience.
  • Tricky: Is service discovery the same as DNS? No. DNS gives you a name-to-address mapping, but service discovery usually adds instance metadata, health awareness, and registry-driven updates.
  • Tricky: If a service is listed in the registry, is it healthy? Not always. A stale cache, delayed heartbeat, or broken dependency can make a discovered instance unreachable even though it still appears in the registry.
  • Tricky: Should I always choose the first instance? No. That creates uneven traffic and can overload one node. Use the load balancer so requests spread across healthy instances.

Common Mistakes:

  • Hard-coding URLs. Correction: call services by logical name and resolve them through a registry or cluster DNS.
  • Confusing discovery with load balancing. Correction: discovery finds the candidates; load balancing chooses the target.
  • Assuming discovery means healthy. Correction: add health checks, timeouts, retries, and circuit breakers.
  • Reading old Ribbon examples as if they were current. Correction: modern Spring Cloud uses Spring Cloud LoadBalancer.

Memory Hook: Think of discovery as a live phone book: the registry stores the numbers, discovery looks up the name, and the load balancer picks which number to dial.

Cheat Sheet:

  • Service discovery lets services find each other by name.
  • The registry stores serviceId, host, port, and metadata.
  • Healthy instances renew heartbeats; stale ones expire after a lease timeout.
  • Modern Spring Cloud uses LoadBalancer, not Ribbon.
  • Discovery reduces hard-coded IPs, but it does not replace resilience tools.

Practice Tasks:

  • Build a controller that lists all discovered services and their URIs.
  • Wire a @LoadBalanced RestTemplate or WebClient to call a service by name.
  • Simulate one dead instance and verify that your app returns a clean 503 or 502 instead of hanging.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

package com.example.servicediscovery; import java.net.URI; import java.util.List; import java.util.Map; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.server.ResponseStatusException; @SpringBootApplication public class ServiceDiscoveryDemoApplication { public static void main(String[] args) { // Keep the demo self-contained and predictable. SpringApplication app = new SpringApplication(ServiceDiscoveryDemoApplication.class); app.setDefaultProperties(Map.of("server.port", "8080")); app.run(args); } @Bean DiscoveryClient discoveryClient() { return new DiscoveryClient() { // In a real app this data comes from Eureka, Consul, Kubernetes, or another registry. private final Map<String, List<ServiceInstance>> registry = Map.of( "inventory-service", List.of(new SimpleServiceInstance( "inventory-1", "inventory-service", "localhost", 8080, false, Map.of("zone", "local"))), // This entry demonstrates the failure path: the service is discoverable, // but the target port is not listening. "shipping-service", List.of(new SimpleServiceInstance( "shipping-1", "shipping-service", "localhost", 9999, false, Map.of("zone", "down")))); @Override public String description() { return "In-memory demo registry"; } @Override public List<ServiceInstance> getInstances(String serviceId) { return registry.getOrDefault(serviceId, List.of()); } @Override public List<String> getServices() { return List.copyOf(registry.keySet()); } @Override public int getOrder() { return 0; } }; } @Bean RestTemplate restTemplate() { // Plain RestTemplate is enough here because the registry already resolves the URI. return new RestTemplate(); } @Bean ApplicationRunner startupLog(DiscoveryClient discoveryClient) { return args -> System.out.println("Discovered services: " + discoveryClient.getServices()); } @RestController static class DemoController { private final DiscoveryClient discoveryClient; private final RestTemplate restTemplate; DemoController(DiscoveryClient discoveryClient, RestTemplate restTemplate) { this.discoveryClient = discoveryClient; this.restTemplate = restTemplate; } @GetMapping("/hello") String hello() { return "inventory-service says hello"; } @GetMapping("/services") Map<String, Object> services() { return Map.of( "allServices", discoveryClient.getServices(), "inventoryInstances", discoveryClient.getInstances("inventory-service") .stream() .map(ServiceInstance::getUri) .toList(), "shippingInstances", discoveryClient.getInstances("shipping-service") .stream() .map(ServiceInstance::getUri) .toList()); } @GetMapping("/proxy/{serviceId}") ResponseEntity<String> proxy(@PathVariable String serviceId) { List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); // No instance means discovery worked, but the registry has nothing usable right now. if (instances.isEmpty()) { throw new ResponseStatusException( HttpStatus.SERVICE_UNAVAILABLE, "No instances found for " + serviceId); } ServiceInstance chosen = instances.get(0); // Discovery only gives us a target. The actual HTTP call can still fail if the instance is dead. URI target = chosen.getUri().resolve("/hello"); try { String body = restTemplate.getForObject(target, String.class); return ResponseEntity.ok("Discovered " + chosen.getUri() + " and got: " + body); } catch (RestClientException ex) { throw new ResponseStatusException( HttpStatus.BAD_GATEWAY, "Found " + serviceId + " but could not call it: " + ex.getMessage(), ex); } } } static final class SimpleServiceInstance implements ServiceInstance { private final String instanceId; private final String serviceId; private final String host; private final int port; private final boolean secure; private final URI uri; private final Map<String, String> metadata; SimpleServiceInstance(String instanceId, String serviceId, String host, int port, boolean secure, Map<String, String> metadata) { this.instanceId = instanceId; this.serviceId = serviceId; this.host = host; this.port = port; this.secure = secure; this.uri = URI.create((secure ? "https" : "http") + "://" + host + ":" + port); this.metadata = metadata; } @Override public String getServiceId() { return serviceId; } @Override public String getHost() { return host; } @Override public int getPort() { return port; } @Override public boolean isSecure() { return secure; } @Override public URI getUri() { return uri; } @Override public Map<String, String> getMetadata() { return metadata; } @Override public String getInstanceId() { return instanceId; } public String getScheme() { return secure ? "https" : "http"; } } }