Back to Service Discovery
Q2Question 2 of 3

Explain service discovery in depth

Key Concepts
  • Service discovery turns a stable logical name like payment-service into a live, healthy, reachable address at runtime.
  • The registry holds a live catalog of healthy instances keyed by service name, pruned by heartbeats and health checks.
  • Client-side discovery means the caller fetches the instance list and picks one; server-side means a load balancer or Kubernetes Service resolves on the caller's behalf.
  • Kubernetes provides built-in server-side discovery via Service DNS names and kube-proxy routing to healthy Pods.
  • A service mesh like Istio pushes discovery, mTLS, retries, and load balancing into a sidecar proxy transparently.
  • Registry poisoning is a real threat: registration must be authenticated and the registry access-controlled.
  • Multi-region setups run a registry per region with federation and prefer same-region instances for latency.

Definition

Service discovery turns a stable logical name like payment-service into a live, healthy, reachable address, and the registry is the always-updated phone book that makes it possible.
  • Service discovery – The mechanism by which a microservice finds the network location of another service at runtime, without addresses being hardcoded.
  • Service registry – A live catalog of healthy instances keyed by service name, maintained by instance registration, heartbeats, and health checks.
  • Client-side discovery – The calling service queries the registry, gets the instance list, and picks one using a load-balancing rule.
  • Server-side discovery – A load balancer, gateway, or Kubernetes Service resolves the name on the caller's behalf.
  • Health checks – Liveness and readiness probes that keep the registry accurate by pruning dead or unready instances.
  • Zero Trust – A security posture where no request is trusted merely because it came from inside the network; mTLS proves both caller and callee identity.

Imagine Rajesh walks into the National Bank of India and wants to speak to the loans department. He does not memorize which desk or which floor because those change every day as staff come and go. Instead there is a central reception register that always knows which loan officers are present and healthy right now. Rajesh asks reception, reception checks its live register, and points him to an available officer. If an officer goes home sick, reception removes them from the register so nobody is sent to an empty desk.

Understanding the Concepts

What is the service registry?

A service registry is a database of currently running instances keyed by service name. Each instance, when it starts, registers itself with its host, port, and metadata, then sends periodic heartbeats to prove it is alive. Netflix Eureka, HashiCorp Consul, and etcd are common registries.

JAVA
package com.bank.payment;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentServiceApplication.class, args);
        System.out.println("Payment service registered with Eureka as payment-service");
    }
}
TEXT
Expected output:
Payment service registered with Eureka as payment-service

How does client-side discovery work?

In client-side discovery the caller fetches the full list of healthy instances from the registry and picks one itself using a load-balancing rule like round-robin. Spring Cloud LoadBalancer does exactly this when the caller uses the lb:// URI scheme.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import java.math.BigDecimal;

@Service
public class TransferService {

    private final WebClient.Builder webClientBuilder;

    public TransferService(WebClient.Builder webClientBuilder) {
        this.webClientBuilder = webClientBuilder;
    }

    public String initiateTransfer(String fromAccount, String toAccount, BigDecimal amount) {
        // lb:// triggers client-side discovery and load balancing
        return webClientBuilder.build()
                .post()
                .uri("lb://payment-service/api/v1/transfers")
                .bodyValue(new TransferRequest(fromAccount, toAccount, amount))
                .retrieve()
                .bodyToMono(String.class)
                .block();
    }

    record TransferRequest(String fromAccount, String toAccount, BigDecimal amount) {}
}
TEXT
Expected output:
Transfer TXN-20240115-001 initiated from SAV2001 to SAV2002

How does server-side discovery work?

In server-side discovery the caller calls a stable virtual address, such as a Kubernetes Service or a cloud load balancer, and that layer consults the registry and forwards the request. The caller carries no discovery logic at all.

YAML
# Kubernetes Service definition for payment-service
apiVersion: v1
kind: Service
metadata:
  name: payment-service
  namespace: banking-prod
spec:
  selector:
    app: payment-service
  ports:
    - port: 8080
      targetPort: 8080
  type: ClusterIP
JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import java.math.BigDecimal;

@Service
public class TransferServiceK8s {

    // In Kubernetes, the DNS name resolves to the Service's ClusterIP
    private static final String PAYMENT_URL = "http://payment-service.banking-prod.svc.cluster.local:8080";

    private final WebClient webClient;

    public TransferServiceK8s(WebClient.Builder builder) {
        this.webClient = builder.baseUrl(PAYMENT_URL).build();
    }

    public String settlePayment(String accountId, BigDecimal amount) {
        return webClient.post()
                .uri("/api/v1/settle")
                .bodyValue(new SettleRequest(accountId, amount))
                .retrieve()
                .bodyToMono(String.class)
                .block();
    }

    record SettleRequest(String accountId, BigDecimal amount) {}
}
TEXT
Expected output:
Settlement confirmed for SAV2001, amount: 50000.00

How do health checks and load balancing work together?

A liveness check answers whether the process is alive. A readiness check answers whether it can accept traffic right now. A service that is alive but still warming its connection pool should report not-ready so no requests are routed to it.

JAVA
package com.bank.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;

@Component
public class CoreBankingHealthIndicator implements HealthIndicator {

    private final DataSource dataSource;

    public CoreBankingHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            if (conn.isValid(2)) {
                return Health.up()
                        .withDetail("database", "NBI core banking DB reachable")
                        .build();
            }
        } catch (Exception e) {
            // Fall through to down
        }
        return Health.down().withDetail("database", "NBI core banking DB unreachable").build();
    }
}
TEXT
Expected output (healthy):
{"status":"UP","details":{"database":"NBI core banking DB reachable"}}

How does a service mesh handle discovery?

A service mesh like Istio or Linkerd injects a sidecar proxy next to each Pod. The sidecar handles discovery, load balancing, retries, and mutual TLS transparently, so the application code stays unaware. The mesh control plane maintains a registry of endpoints and pushes configuration to every sidecar.

YAML
# Istio VirtualService routing for payment-service
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: payment-service
spec:
  hosts:
    - payment-service
  http:
    - route:
        - destination:
            host: payment-service
            port:
              number: 8080
      retries:
        attempts: 3
        perTryTimeout: 2s
TEXT
Istio sidecar intercepts call to payment-service
  -> resolves healthy endpoints from control plane
  -> load-balances to 10.0.3.14:8080
  -> encrypts with mTLS automatically
  -> retries on failure up to 3 times

What are the security concerns?

The registry is a trust anchor. If an attacker registers a fake instance (registry poisoning), they can hijack traffic meant for the real service. Registration must be authenticated and the registry access-controlled. Zero Trust with mutual TLS ensures both caller and callee prove identity with certificates.

JAVA
package com.bank.security;

// WRONG: No authentication on registration
// Any pod can register as "payment-service" and steal traffic
// eurekaClient.register("payment-service", "10.0.99.1:8080");

// CORRECT: mTLS ensures only pods with valid certificates can register
// and downstream calls verify the callee's certificate
// Istio PeerAuthentication enforces mTLS cluster-wide:
YAML
# Istio PeerAuthentication: enforce mTLS for all services
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: banking-prod
spec:
  mtls:
    mode: STRICT
TEXT
With STRICT mTLS:
  - Only services with valid mesh certificates can register
  - Every call is encrypted and both sides verified
  - Registry poisoning is blocked at the transport layer

What about performance and DNS TTL?

DNS-based discovery is subject to TTL caching. A short TTL means faster failover but more DNS lookups. Proxy hops in a mesh add small per-call latency but buy consistent policy. The registry itself must scale under thousands of instances.

JAVA
package com.bank.config;

import java.net.InetAddress;
import java.security.Security;

public class DnsTtlConfig {
    public static void configureDnsTtl() {
        // JVM caches DNS forever by default with a security manager.
        // Set a short TTL so failover is fast.
        Security.setProperty("networkaddress.cache.ttl", "10");
        Security.setProperty("networkaddress.cache.negative.ttl", "5");
        System.out.println("DNS TTL set to 10 seconds for fast failover");
    }
}
TEXT
Expected output:
DNS TTL set to 10 seconds for fast failover

How do multi-region setups work?

Multi-region setups run a registry per region with federation. Services prefer same-region instances to keep latency low and fail over cross-region only when a whole region's instances are unavailable. Eureka supports zone-aware routing where instances in the same zone are preferred.

YAML
# Eureka zone-aware configuration
eureka:
  instance:
    metadata-map:
      zone: ap-south-1a
  client:
    prefer-same-zone-eureka: true
    service-url:
      ap-south-1a: http://eureka-mumbai:8761/eureka/
      ap-south-1b: http://eureka-mumbai-b:8761/eureka/
      eu-west-1a: http://eureka-london:8761/eureka/
TEXT
Routing preference:
  1. Same zone (ap-south-1a) -> lowest latency
  2. Same region, different zone (ap-south-1b) -> moderate latency
  3. Cross-region (eu-west-1a) -> last resort, high latency

Visual Overview

DIAGRAM
  Client Request
       │
       ▼
┌──────────────┐     ┌─────────────────────────────────┐
│  API Gateway │────▶│      Service Registry (Eureka)   │
│  (validates  │     │  ┌─────────────────────────────┐ │
│   JWT, rate  │     │  │ payment-service              │ │
│   limits)    │     │  │   inst-1: 10.0.3.14 HEALTHY  │ │
└──────┬───────┘     │  │   inst-2: 10.0.3.15 HEALTHY  │ │
       │             │  │   inst-3: 10.0.3.16 DOWN     │ │
       │             │  └─────────────────────────────┘ │
       │             └─────────────────────────────────┘
       │
       │  lb://payment-service
       │  (resolves to inst-1 or inst-2)
       ▼
┌──────────────┐  heartbeat  ┌──────────────┐
│ payment-svc  │◀───────────▶│   Registry   │
│ instance-1   │  every 10s  │  (evicts if  │
│ 10.0.3.14    │             │  3 missed)   │
└──────────────┘             └──────────────┘

Key Takeaway

Service discovery is the runtime mechanism that lets one microservice find another without hardcoding network addresses. At its heart is a service registry, a live catalog of every running instance keyed by logical service name. When an instance starts it registers itself with its host and port, sends periodic heartbeats, and the registry evicts it if heartbeats stop. Discovery is the act of resolving a logical name like payment-service into a real, healthy address at the moment a call is made.

At National Bank of India, Pankaj designed the account service to use Spring Cloud LoadBalancer for client-side discovery, where the caller queries Eureka and picks a healthy payment instance. When they moved to Kubernetes, server-side discovery via Service DNS names meant the application just called payment-service.banking-prod.svc.cluster.local and kube-proxy handled the rest. Adding Istio pushed discovery, mTLS, retries, and load balancing into a sidecar so application code stayed completely unaware. For multi-region, they ran a registry per region with zone-aware routing, preferring Mumbai instances for latency and failing over to London only when the entire Mumbai region was unavailable.

The security and performance nuances matter in production. The registry is a trust anchor, so registration must be authenticated to prevent poisoning, and Zero Trust with mTLS ensures both sides prove identity. DNS TTLs trade failover speed against lookup volume, mesh proxy hops add small latency for consistent policy, and the registry itself must scale under thousands of instances, which is why Eureka favors availability and may serve slightly stale data combined with client retries.

Remember one sentence: the registry is the always-updated source of truth about instances, and discovery is the glue that turns a logical service name into a live, healthy endpoint, making a microservices system elastic and self-healing.

---

Share
Use ← → arrow keys to navigate