Spring BootService Registry
Spring Boot

Service Registry

A service registry is a directory of available service instances. Services register on startup and deregister on shutdown. Clients query the registry to discover service locations instead of using hard-coded URLs. Spring Cloud provides Eureka (Netflix) as the default registry, with Consul and Zookeeper as alternatives. This entry covers Eureka Server setup, client registration, health checks, zone-aware routing, and self-preservation.

Eureka Server Setup

Create a dedicated Spring Boot application for the service registry. @EnableEurekaServer activates the Eureka dashboard and registration API. Run two or more Eureka servers in production and have them peer-replicate for high availability.
XML
<!-- Eureka Server pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

// ── Main application ───────────────────────────────────────────────────
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(
            ServiceRegistryApplication.class, args);
    }
}

# ── application.yml (standalone Eureka server) ────────────────────────
server:
  port: 8761

spring:
  application:
    name: service-registry
  security:
    user:
      name: ${EUREKA_USER:eureka}
      password: ${EUREKA_PASSWORD}

eureka:
  instance:
    hostname: localhost
  client:
    # Standalone — do not register with itself
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka/
  server:
    # Wait before starting evictions — grace period for startup
    wait-time-in-ms-when-sync-empty: 0
    # Self-preservation: if > 85% of expected heartbeats arrive,
    # do not evict instances. Adjust for your environment.
    renewal-percent-threshold: 0.85

# ── Peer-aware Eureka (two instances for HA) ──────────────────────────
# application-peer1.yml
eureka:
  instance:
    hostname: eureka-peer1
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka:password@eureka-peer2:8761/eureka/

# application-peer2.yml
eureka:
  instance:
    hostname: eureka-peer2
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka:password@eureka-peer1:8761/eureka/

Eureka Client Registration

Add spring-cloud-starter-netflix-eureka-client to each service. The service registers automatically on startup and sends heartbeats every 30 seconds by default. The Eureka dashboard at http://eureka-server:8761 shows all registered instances with their health status.
XML
<!-- Client service pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

// ── Auto-registration (no annotation needed in Spring Boot 3) ─────────
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(
            OrderServiceApplication.class, args);
    }
}

# ── application.yml (client service) ──────────────────────────────────
spring:
  application:
    name: order-service     # service ID in Eureka — use lowercase

eureka:
  client:
    service-url:
      defaultZone: >
        http://${EUREKA_USER:eureka}:${EUREKA_PASSWORD}@
        eureka-server:8761/eureka/
    # Cache registry locally — reduces load on Eureka
    registry-fetch-interval-seconds: 30
    # Prefer IP over hostname for containerised environments
    instance:
      prefer-ip-address: true

  instance:
    # Unique instance ID (important for multiple instances)
    instance-id: >
      ${spring.application.name}:
      ${spring.cloud.client.ip-address}:
      ${server.port}
    # How often to send heartbeats (default 30s)
    lease-renewal-interval-in-seconds: 15
    # Time before Eureka removes instance without heartbeat
    lease-expiration-duration-in-seconds: 45
    # Health check URL — Eureka uses actuator/health
    health-check-url-path: /actuator/health
    status-page-url-path:  /actuator/info

management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: always

Service Discovery with DiscoveryClient

Use DiscoveryClient to programmatically query the registry for service instances. Spring Cloud LoadBalancer uses the registry to resolve service names to URLs for @LoadBalanced RestClient and WebClient calls. Service names replace hard-coded host:port pairs.
Java
// ── DiscoveryClient — programmatic service lookup ─────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class ServiceDiscoveryDemo {

    private final DiscoveryClient discoveryClient;

    // ── List all registered services ──────────────────────────────────
    public List<String> listServices() {
        return discoveryClient.getServices();
    }

    // ── Get instances of a specific service ───────────────────────────
    public List<ServiceInstance> getInstances(String serviceId) {
        List<ServiceInstance> instances =
            discoveryClient.getInstances(serviceId);

        instances.forEach(instance ->
            log.info("Service: {} at {}:{} meta={}",
                instance.getServiceId(),
                instance.getHost(),
                instance.getPort(),
                instance.getMetadata()));

        return instances;
    }

    // ── Pick one instance (for manual load balancing) ─────────────────
    public Optional<String> resolveUrl(String serviceId) {
        return discoveryClient.getInstances(serviceId)
            .stream()
            .findFirst()
            .map(ServiceInstance::getUri)
            .map(URI::toString);
    }
}

// ── @LoadBalanced RestClient — resolves service name via registry ──────
@Configuration
public class RestClientConfig {

    @Bean
    @LoadBalanced       // Spring Cloud LoadBalancer intercepts calls
    public RestClient.Builder loadBalancedRestClientBuilder() {
        return RestClient.builder();
    }

    @Bean
    public RestClient inventoryClient(
            @LoadBalanced RestClient.Builder builder) {
        return builder
            .baseUrl("http://inventory-service")  // service name, not URL
            .build();
    }
}

// ── Service using load-balanced RestClient ─────────────────────────────
@Service
@RequiredArgsConstructor
public class InventoryClient {

    private final RestClient inventoryClient;

    public StockResponse checkStock(Long productId) {
        // "inventory-service" resolved to actual host:port via Eureka
        return inventoryClient.get()
            .uri("/api/v1/stock/{id}", productId)
            .retrieve()
            .body(StockResponse.class);
    }
}

// ── @LoadBalanced WebClient ────────────────────────────────────────────
@Configuration
public class WebClientConfig {

    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }

    @Bean
    public WebClient paymentClient(
            @LoadBalanced WebClient.Builder builder) {
        return builder
            .baseUrl("http://payment-service")
            .build();
    }
}

Health Checks and Metadata

Eureka marks instances UP or DOWN based on actuator health checks. Custom health indicators contribute to the overall health and control whether the instance receives traffic. Instance metadata carries custom key-value pairs — zone, version, capabilities — that clients can use for routing decisions.
Java
// ── Custom health indicator ───────────────────────────────────────────
@Component
@RequiredArgsConstructor
public class DatabaseHealthIndicator
        implements HealthIndicator {

    private final DataSource dataSource;

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            conn.isValid(3);
            return Health.up()
                .withDetail("database", "PostgreSQL")
                .withDetail("status",   "connected")
                .build();
        } catch (SQLException ex) {
            return Health.down()
                .withDetail("error", ex.getMessage())
                .build();
        }
    }
}

// ── Composite health — all indicators must be UP ──────────────────────
// If any indicator returns DOWN, Eureka marks the instance OUT_OF_SERVICE
// and load balancer stops routing traffic to it.

// ── Instance metadata for zone-aware routing ──────────────────────────
eureka:
  instance:
    metadata-map:
      zone:    us-east-1a      # availability zone
      version: "2.1.0"         # service version
      weight:  "100"           # relative weight for load balancing

// ── Read metadata in a custom load balancer ───────────────────────────
@Component
@RequiredArgsConstructor
@Slf4j
public class ZoneAwareServiceInstanceListSupplier
        implements ServiceInstanceListSupplier {

    private final ServiceInstanceListSupplier delegate;

    @Value("${eureka.instance.metadata-map.zone:default}")
    private String currentZone;

    @Override
    public String getServiceId() {
        return delegate.getServiceId();
    }

    @Override
    public Flux<List<ServiceInstance>> get() {
        return delegate.get().map(instances -> {
            // Prefer instances in the same zone
            List<ServiceInstance> sameZone = instances.stream()
                .filter(i -> currentZone.equals(
                    i.getMetadata().get("zone")))
                .toList();

            return sameZone.isEmpty() ? instances : sameZone;
        });
    }
}

# ── Self-preservation tuning ──────────────────────────────────────────
eureka:
  server:
    # Disable self-preservation in development
    # (instances removed immediately on missed heartbeats)
    enable-self-preservation: false   # dev only
    eviction-interval-timer-in-ms: 5000

    # In production keep self-preservation ON
    # to prevent mass eviction during network partitions
    enable-self-preservation: true    # prod (default)
    renewal-percent-threshold: 0.85

Consul as an Alternative Registry

HashiCorp Consul provides service discovery, health checking, and key-value configuration in a single tool. Spring Cloud Consul integrates with the same @LoadBalanced abstractions as Eureka — switching requires only a dependency swap and configuration change, no code changes.
XML
<!-- Replace Eureka client with Consul client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>

# ── application.yml (Consul client) ──────────────────────────────────
spring:
  application:
    name: order-service

  cloud:
    consul:
      host: ${CONSUL_HOST:localhost}
      port: ${CONSUL_PORT:8500}
      token: ${CONSUL_TOKEN:}       # ACL token for secure Consul

      discovery:
        enabled:              true
        prefer-ip-address:    true
        instance-id: >
          ${spring.application.name}-
          ${spring.cloud.client.ip-address}-
          ${server.port}
        health-check-path:    /actuator/health
        health-check-interval: 15s
        health-check-timeout:  10s
        deregister-critical-services-after: 30s
        tags:
          - version=${app.version:1.0.0}
          - zone=${AVAILABILITY_ZONE:default}

      config:
        enabled: true
        prefix: config
        default-context: application
        profile-separator: ","
        format: YAML
        # Reads from Consul KV at:
        # config/order-service/data
        # config/application/data  (shared)

# ── Consul Docker Compose ─────────────────────────────────────────────
# services:
#   consul:
#     image: consul:1.17
#     command: >
#       agent -server -bootstrap-expect=1
#       -ui -client=0.0.0.0
#     ports:
#       - "8500:8500"
#       - "8600:8600/udp"