DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Day 19: Service discovery with Eureka — finding services by name

Yesterday order-service started calling inventory-service over HTTP with OpenFeign. It worked, but it leaned on one line of config: inventory.service.url: http://localhost:8081. A hardcoded address. That value is a promise the network can't keep — restart the service on a new host and it's wrong; scale it to three replicas and the single URL only ever hits one of them; deploy somewhere with dynamic IPs and it's stale before the app boots. Today I replaced "where is it" with "who is it": order-service now finds inventory-service by NAME through a service registry.

The registry idea

A service registry is a live phone book. Every instance, on startup, registers itself — "I am inventory-service, at 10.0.0.5:8081" — and then sends a heartbeat to renew a short-lived lease. Miss enough heartbeats (crash, kill, network cut) and the registry evicts the entry. A caller stops storing addresses and instead asks the registry for a name, getting back whatever is alive right now.

Netflix Eureka is the registry, and Spring Cloud makes it a one-annotation app.

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] a){ SpringApplication.run(EurekaServerApplication.class, a); }
}
Enter fullscreen mode Exit fullscreen mode
server: { port: 8761 }
eureka:
  client:
    register-with-eureka: false   # standalone node: don't register with a peer
    fetch-registry: false         # this node IS the source of truth
Enter fullscreen mode Exit fullscreen mode

A Eureka server is itself a Eureka client by default — in production you run a cluster of peers that replicate to each other for high availability. I run a single standalone node, so those two flags switch off the peer chatter. It joins the monorepo as a third module in the reactor, started before the services.

Registering the services

Adding the client starter is all it takes. On boot each service registers under its spring.application.name and begins renewing its lease. No @EnableDiscoveryClient needed — the starter's presence is enough.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
spring: { application: { name: inventory-service } }   # <-- the SERVICE ID
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
Enter fullscreen mode Exit fullscreen mode

That application name is the identity other services resolve against, so it has to match exactly.

Dropping the URL

Here's the whole point, in one line. On Day 18 the Feign client pinned a URL. Today I drop it and keep only the name — Feign then treats the name as a service id, resolves it through discovery, and hands the candidates to Spring Cloud LoadBalancer to pick one.

// Day 18
@FeignClient(name = "inventory-service", url = "${inventory.service.url:http://localhost:8081}")

// Day 19 — empty default -> resolve by NAME via Eureka + LoadBalancer
@FeignClient(name = "inventory-service", url = "${inventory.service.url:}")
Enter fullscreen mode Exit fullscreen mode

The call site — inventory.reserve(sku, req) — doesn't change at all. Only where the request is aimed does. I kept the empty-default placeholder purely so a test can point Feign at a mock server and bypass discovery; a non-empty url short-circuits it, an empty one enables it.

Load balancing is client-side: the chooser lives inside order-service and round-robins across the instances discovery returns. No central proxy to run or become a bottleneck — each caller balances its own traffic, and a dead instance just drops out of its rotation on the next registry refresh.

I proved it end-to-end: started the registry, ran two things, and watched a placed order reserve stock via a request aimed at http://inventory-service/... — the service name as the host, no address anywhere.

The gotcha that cost me an hour

The eureka client transitively pulls in Apache HttpClient 5 — and it genuinely needs it for its own REST transport, so I couldn't exclude it. But Spring Boot auto-detects HttpClient 5 and makes it the default HTTP client, which the auto-configured TestRestTemplate in my integration tests then picked up. Its connection pool silently retries a request on a stale connection — and my rate-limit test asserts the 6th call is throttled with a 429. The retried POST landed after a token had refilled, so the client saw a 201 and the assertion blew up on a mismatched response body. The rate limiter was fine; the test client had quietly changed behaviour underneath it. Fix: pin the test template back to the JDK factory so it stays deterministic. Order-service uses Feign in production, never RestTemplate, so it's a test-only concern — but a sharp reminder that a transitive dependency can change how unrelated code behaves.

Why not just DNS?

DNS also maps names to addresses, but it's cached with TTLs measured in minutes — far too slow for instances that churn every few seconds. A registry updates within a heartbeat, knows health and metadata, and hands the caller the full instance list so it can load-balance and fail over itself. (In Kubernetes the platform does this for you, which is why k8s deployments often skip Eureka.)

The tradeoff is honest: the registry is a new moving part on the critical path, so in production it must be highly available, and discovery is eventually consistent — which is exactly why the Day 14/15 retry and circuit breaker stay. Day 20 puts an API gateway in front, and it'll use this same registry to route.

Full 3-tier walkthrough (with a live registry sim): https://dev48v.infy.uk/orderhub.php
Code: https://github.com/dev48v/order-hub-from-zero

Top comments (0)