skip to content

How does the DiscoveryClient SPI abstract different registries (Eureka, Consul, Zookeeper), and how does Spring Cloud combine multiple discovery sources?

level: seniorimportance: should knowfreq 45%

answer

  1. One interface, per-backend impls via starters
  2. Eureka/Consul/Zookeeper/K8s/Simple
  3. CompositeDiscoveryClient aggregates delegates
  4. Reactive twin: ReactiveDiscoveryClient
  5. Abstraction leaks: metadata/health/CAP differ

basics

~20 s

DiscoveryClient is a common interface; each backend (Eureka, Consul, Zookeeper) ships its own implementation via a starter. Your code depends only on the interface, so switching registries means swapping the starter. Spring Cloud can also merge several implementations behind one CompositeDiscoveryClient.

solid answer

~40 s

DiscoveryClient is an SPI: one stable interface (getServices, getInstances) with backend-specific implementations — EurekaDiscoveryClient, ConsulDiscoveryClient, ZookeeperDiscoveryClient, KubernetesDiscoveryClient — each provided by its own spring-cloud-starter and auto-configured when on the classpath. Application code depends only on the interface, so migrating registries is mostly a dependency and config swap, not a code change. When more than one implementation is present, Spring Cloud wires a CompositeDiscoveryClient that aggregates them and answers getInstances by consulting each delegate in order, which is how hybrid setups (e.g. Eureka plus SimpleDiscoveryClient static entries) coexist. The reactive world mirrors this with ReactiveDiscoveryClient and ReactiveCompositeDiscoveryClient. The abstraction's limit: metadata keys, health semantics, and registration behavior differ per backend, so truly portable code sticks to the interface contract and avoids backend-specific assumptions.

code

yaml · 14 lines
yaml
# Static instances via SimpleDiscoveryClient - no real registry needed (dev/test)
spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            order-service:
              - uri: http://localhost:8081
                metadata: { track: stable }
              - uri: http://localhost:8082
                metadata: { track: canary }
# If a real Eureka starter is also present, CompositeDiscoveryClient
# merges both sources behind the single injected DiscoveryClient bean.

go deeper

for a junior

Should grasp 'same interface, different implementation per registry via starters'.

for a middle

Should name a couple of implementations and know code depends on the interface, not the impl.

for a senior

Should explain CompositeDiscoveryClient aggregation and the reactive parallel.

for a principal

Should discuss where the abstraction leaks (CAP model, health semantics, metadata) and design consumers to stay portable.

## SPI = one interface, many providers An **SPI (Service Provider Interface)** is a contract that Spring Cloud defines and third parties implement. Here the contract is `DiscoveryClient` (and its reactive twin `ReactiveDiscoveryClient`). The concrete providers each live in their own starter: - **Eureka** — `EurekaDiscoveryClient` (`spring-cloud-starter-netflix-eureka-client`). - **Consul** — `ConsulDiscoveryClient` (`spring-cloud-starter-consul-discovery`). - **Zookeeper** — `ZookeeperDiscoveryClient` (`spring-cloud-starter-zookeeper-discovery`). - **Kubernetes** — `KubernetesDiscoveryClient` (Spring Cloud Kubernetes). - **Static/manual** — `SimpleDiscoveryClient`, which serves instances you list in `spring.cloud.discovery.client.simple.instances.*` — handy for local dev/tests with no real registry. Each starter's **auto-configuration** registers its implementation as a bean when the starter is on the classpath. Because your code injects the `DiscoveryClient` interface, swapping backends is largely a **build-dependency + configuration** change, not a source change — the core value proposition of the abstraction. ## CompositeDiscoveryClient When multiple discovery implementations are present, Spring Cloud does not force you to pick one. `CompositeDiscoveryClient` (with a bean of type `DiscoveryClient`) **aggregates all the discovered `DiscoveryClient` beans**, ordered, and delegates: - `getServices()` returns the union across delegates. - `getInstances(serviceId)` walks the ordered delegates and returns the first non-empty result (so a higher-priority source can shadow a lower one). This is what lets, say, a real Eureka client coexist with `SimpleDiscoveryClient` static overrides. Delegate order is controlled via `@Order` / configured order. ## Reactive parallel For reactive stacks (WebFlux), the same design repeats with `ReactiveDiscoveryClient` and `ReactiveCompositeDiscoveryClient`, returning `Flux<ServiceInstance>` instead of a `List`. ## Where the abstraction leaks The interface is portable, but backends differ underneath: - **Metadata**: which keys exist and how they're populated (zone, secure flag, health path) varies; code that reads specific metadata keys is not automatically portable. - **Health semantics**: Eureka status vs Consul health checks vs Zookeeper ephemeral nodes decide differently what counts as 'up' and how fast a dead instance disappears. - **Consistency/heartbeat model**: Eureka favors availability (AP, cached, lease-based); Consul/Zookeeper lean more consistent (CP). This changes staleness behavior even though the API is identical. - **Registration config**: instance-id format, secure ports, and metadata publishing are backend-specific properties. ## When to rely on it Lean on the SPI when you want vendor-neutral consumer code and the option to migrate registries. Don't over-trust it as a full portability guarantee for operational behavior — design/config still differs per backend.

  • If both a Eureka client and SimpleDiscoveryClient are on the classpath, which DiscoveryClient bean gets injected?
    A CompositeDiscoveryClient that wraps both. It delegates getInstances to each underlying client in order and returns the first non-empty result, so both sources contribute behind one interface.
  • Is switching from Eureka to Consul truly zero-code?
    Consumer code that only uses the DiscoveryClient interface is usually unchanged; you swap the starter and config. But metadata keys, health semantics, and CAP behavior differ, so any code or ops assumptions tied to those may need adjustment.

saying these in an interview costs you the question

  • Claiming the abstraction makes all backends behaviorally identical (ignoring CAP/health/metadata differences)
  • Thinking you must choose exactly one discovery implementation
  • Not knowing CompositeDiscoveryClient exists / how it merges results
  • Assuming SimpleDiscoveryClient talks to a real registry

context