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.
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.
DiscoveryClient for instances of inventory-service instead of calling a fixed IP.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.| Approach | How it works | Best for | Trade-off |
|---|---|---|---|
| Static config | Hard-coded host and port | Small apps | Breaks when IPs change |
| DNS | Use service name from DNS | Simple clusters | Less metadata and health detail |
| Client-side discovery | Client queries registry | Spring Cloud apps | More client logic |
| Server-side discovery | Gateway queries registry | Central routing | Extra hop |
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.
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:
Common Mistakes:
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:
Practice Tasks:
@LoadBalanced RestTemplate or WebClient to call a service by name.