Skip to main content

From Eureka to Consul: Migrating Service Discovery in Spring Cloud

·978 words·5 mins
Michael Antonio Tomaylla
Author
Michael Antonio Tomaylla
From technical complexity to simplicity that creates value

Introduction — Leaving Printed Addresses Behind
#

Hard‑coding IPs in microservices is like using a paper map in the GPS era. For years Netflix Eureka was the natural choice in Spring Cloud projects: easy integration, reliable, familiar. Over time we needed more: efficiency, richer features and an active roadmap.

This guide summarizes a real migration I performed from Eureka to HashiCorp Consul: why we decided to switch, the practical steps, issues faced, and measurable benefits in production—plus clear examples and actionable recommendations.

Migration from Eureka to Consul

What Service Discovery Really Is
#

Service Discovery is the GPS for microservices: a service locates dynamically the IP and port of another service. It avoids static configuration, enables automatic scaling and improves resilience.

Common patterns:

  • Client‑side discovery: the client queries the registry (Eureka/Consul) and selects an instance.
  • Server‑side discovery: a proxy or router (Kubernetes Service, load balancer) resolves on your behalf.

If you still trust fixed IPs, your code needs an urgent conversation… and a coffee.

Why Eureka was standard

Why Eureka Was the Standard (And Why We Liked It)
#

Eureka gained traction because integration with Spring Cloud was simple: a few dependencies, a couple annotations and apps self‑registered. Historical advantages:

  • Fast setup thanks to auto‑configuration.
  • AP philosophy: high availability tolerating stale registrations.
  • Natural integration with the Spring Cloud stack.

Yet limitations began to outweigh convenience.

Limitations That Pushed the Migration
#

1) Stagnant development
Eureka moved into maintenance mode. Building on a project without a roadmap adds technical debt.

2) Resource consumption
Eureka Server is a full JVM: we measured ~300–500 MB RAM per node. In larger clusters that adds up.

3) Limited functionality
Eureka does discovery, but not a KV store, advanced health checks or integrated mesh.

If your goal is a modern platform, relying solely on Eureka today is like owning a Swiss Army knife without the blade.

Why Consul

Why HashiCorp Consul (More Than Discovery)
#

Consul is a distributed networking and configuration platform with clear advantages:

  • Robust Service Discovery.
  • Advanced health checks (HTTP, TCP, scripts, gRPC).
  • Key‑Value store for distributed configuration.
  • Service Mesh with Consul Connect (mTLS and routing).
  • Native multi‑datacenter support.
  • Lightweight implementation (written in Go).

In practice: fewer components, more features, active roadmap.

Quick Comparison
#

  • Language: Eureka = Java, Consul = Go.
  • RAM per node: Eureka ≈ 300–500 MB, Consul ≈ 40–80 MB.
  • Development: Eureka maintenance; Consul active.
  • KV Store: Eureka ❌, Consul ✅.
  • Service Mesh: Eureka ❌, Consul ✅ (Consul Connect).
  • Health Checks: Eureka basic; Consul advanced.
  • UI: Eureka simple; Consul modern & usable.
Migration plan

Migration Plan: Practical Focus
#

Most effort is configuration; application code barely changes. Four steps overview:

  1. Update dependencies (pom.xml)
  2. Adjust configuration (bootstrap.yml / application-*.yml)
  3. Verify / adapt code (minimal changes)
  4. Orchestrate with Docker Compose / CI / Kubernetes

Step 1 — Dependencies (pom.xml)
#

<!-- REMOVE -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- ADD -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

Note: spring-cloud-starter-bootstrap lets Spring contact Consul during the bootstrap phase. Keep it.

Step 2 — Configuration: bootstrap.yml and application-docker.yml
#

Keep bootstrap.yml minimal: only what is essential to reach Consul.

bootstrap.yml
spring:
  application:
    name: ms-customer
  cloud:
    consul:
      host: consul
      port: 8500

In the Docker profile (application-docker.yml) define registration & health checks:

application-docker.yml
spring:
  cloud:
    consul:
      discovery:
        service-name: ${spring.application.name}
        instance-id: ${spring.application.name}:${server.port}
        prefer-ip-address: true
        health-check-path: /actuator/health/liveness
        health-check-interval: 10s

management:
  endpoint:
    health:
      probes:
        enabled: true

Key points:

  • prefer-ip-address: true is critical in Docker to avoid localhost.
  • Use Actuator for real health checks.
  • Keep bootstrap.yml lean; move runtime properties to application-*.yml to avoid bootstrap conflicts.

Step 3 — Java Code: Nearly Drama‑Free
#

Spring Cloud abstractions (@LoadBalanced RestTemplate, FeignClient, etc.) usually require no changes. Typical example:

@SpringBootApplication
public class CustomerApplication {
  // unchanged code
}

@FeignClient(name = "ms-activity")
public interface ActivityClient { /* ... */ }

Note: @EnableDiscoveryClient is optional in Spring Boot 3.x; Spring Cloud auto‑configures discovery.

Step 4 — Orchestration: Minimal Docker Compose
#

Minimal example for local tests:

services:
  consul:
    image: hashicorp/consul:latest
    ports: ["8500:8500"]
    command: agent -server -ui -bootstrap-expect=1 -client=0.0.0.0

  ms-customer:
    build: .
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - consul

Tips:

  • depends_on helps in Compose; in Kubernetes rely on readiness probes.
  • In production configure probes, network policies and security.
Debugging issues

Common Issues & Practical Fixes
#

1) Service does not appear in Consul
Cause: bloated or conflicting bootstrap.yml.
Fix: simplify bootstrap.yml, move runtime properties to application-*.yml.

2) Registers with localhost or wrong IP
Cause: registration using localhost under Docker.
Fix: prefer-ip-address: true and check Docker network config.

3) Health check fails and service deregisters
Cause: wrong path or container networking issue.
Fix: validate the endpoint from the Consul container (curl) and correct path/port.

Debugging: enter the Consul container, inspect catalog & health state. Often it’s a mistyped path or mismatched port.

Results: Metrics & Benefits
#

Memory consumption

  • Eureka: ~400 MB per node
  • Consul: ~60 MB per node
  • Approx. savings: ~85% in discovery service memory

Failure detection time

  • Eureka: ~90 s
  • Consul: ~20 s
  • Detection ~4.5× faster → quicker recovery & less user impact

Qualitative benefits:

  • Friendlier UI for debugging.
  • Integrated KV store → fewer stack pieces.
  • Path toward service mesh with Consul Connect (mTLS & policies).
  • Lower operational cost (less RAM, lighter processes).
Roadmap

Roadmap & Next Steps
#

  1. Security & secrets management
    • Implement Consul ACLs.
    • Integrate HashiCorp Vault for secrets.
  2. Service Mesh
    • Enable Consul Connect for automatic mTLS and policy control.
  3. Runtime optimization
    • Evaluate native image (GraalVM) to reduce memory & startup time.

Vault + Consul Connect opens many possibilities but requires security design & reviews.

Conclusion — Is Migration Worth It?
#

For us it was a strategic investment: modernized the platform, reduced costs and paved the way for service mesh and centralized configuration/secrets. Most effort was configuration; application code stayed almost untouched. Net result: an achievable migration for most teams following good practices.

Want me to prepare an example repository and a step‑by‑step checklist? Tell me if you prefer Docker Compose for local tests or Kubernetes manifests for staging/production. Let’s talk microservices—no paper maps needed!