Imagine running a matatu business where drivers have to memorize the phone number of every other driver to coordinate routes. Chaotic, right?

That's what microservices without service discovery look like - every service hardcoding the URLs of every other service it needs to talk to. Change a hostname? Update a port? Good luck finding and updating every config file in your 47 microservices.

Netflix Eureka solves this. Services register themselves when they start, discover other services dynamically, and automatically handle instances coming and going. No hardcoded URLs. Pure, beautiful dynamic discovery.

Today we're building a complete microservices system with Spring Boot and Eureka. Start with building REST APIs in Spring Boot + Kotlin, then scale to microservices.

Why Service Discovery?

Without Eureka (the bad old days):

# In Product Service
user.service.url=http://user-service:8081
order.service.url=http://order-service:8082

# What if order-service moves to 8083? Update every config!
# What if you have 5 instances of order-service? How do you load balance?
# What if an instance crashes? Manual intervention!

With Eureka (the enlightened present):

// Just call by service name
val response = restTemplate.getForObject(
    "http://ORDER-SERVICE/api/orders",
    OrderResponse::class.java
)
// Eureka handles:
// - Finding ORDER-SERVICE instances
// - Load balancing across them
// - Removing dead instances

Project Structure

We'll build:

  1. Eureka Server - Service registry
  2. Product Service - Manages products
  3. Order Service - Manages orders, calls Product Service

1. Eureka Server

build.gradle.kts:

dependencies {
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
}

extra["springCloudVersion"] = "2023.0.0"

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

Application Class:

@SpringBootApplication
@EnableEurekaServer
class EurekaServerApplication

fun main(args: Array<String>) {
    runApplication<EurekaServerApplication>(*args)
}

application.yml:

server:
  port: 8761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false

Start it. Visit http://localhost:8761 - you'll see the Eureka dashboard.

2. Product Service

build.gradle.kts:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
}

Application:

@SpringBootApplication
@EnableDiscoveryClient
class ProductServiceApplication

fun main(args: Array<String>) {
    runApplication<ProductServiceApplication>(*args)
}

Product Model & Repository:

data class Product(
    val id: Long,
    val name: String,
    val price: Double,
    val stock: Int
)

@Component
class ProductRepository {
    private val products = mutableMapOf(
        1L to Product(1, "Laptop", 150000.0, 10),
        2L to Product(2, "Phone", 50000.0, 25),
        3L to Product(3, "Tablet", 80000.0, 15)
    )

    fun findById(id: Long): Product? = products[id]
    fun findAll(): List<Product> = products.values.toList()
}

Controller:

@RestController
@RequestMapping("/api/products")
class ProductController(private val repository: ProductRepository) {

    @GetMapping
    fun getAllProducts() = repository.findAll()

    @GetMapping("/{id}")
    fun getProductById(@PathVariable id: Long): ResponseEntity<Product> {
        return repository.findById(id)
            ?.let { ResponseEntity.ok(it) }
            ?: ResponseEntity.notFound().build()
    }
}

application.yml:

server:
  port: 8081

spring:
  application:
    name: PRODUCT-SERVICE

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

3. Order Service (Calls Product Service)

build.gradle.kts:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
    implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
}

Config (Enable Load-Balanced RestTemplate):

@Configuration
class AppConfig {

    @Bean
    @LoadBalanced
    fun restTemplate(): RestTemplate = RestTemplate()
}

Models:

data class Order(
    val id: Long,
    val productId: Long,
    val quantity: Int,
    val totalPrice: Double,
    val productDetails: ProductDetails?
)

data class ProductDetails(
    val id: Long,
    val name: String,
    val price: Double
)

Service:

@Service
class OrderService(
    @LoadBalanced private val restTemplate: RestTemplate
) {
    private val orders = mutableMapOf(
        1L to Order(1, 1, 2, 300000.0, null),
        2L to Order(2, 2, 1, 50000.0, null)
    )

    fun getOrderById(id: Long): Order? {
        val order = orders[id] ?: return null

        // Call Product Service via Eureka
        val productDetails = try {
            restTemplate.getForObject(
                "http://PRODUCT-SERVICE/api/products/${order.productId}",
                ProductDetails::class.java
            )
        } catch (e: Exception) {
            null
        }

        return order.copy(productDetails = productDetails)
    }

    fun getAllOrders(): List<Order> {
        return orders.values.map { order ->
            val productDetails = try {
                restTemplate.getForObject(
                    "http://PRODUCT-SERVICE/api/products/${order.productId}",
                    ProductDetails::class.java
                )
            } catch (e: Exception) {
                null
            }

            order.copy(productDetails = productDetails)
        }
    }
}

Controller:

@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {

    @GetMapping
    fun getAllOrders() = orderService.getAllOrders()

    @GetMapping("/{id}")
    fun getOrderById(@PathVariable id: Long): ResponseEntity<Order> {
        return orderService.getOrderById(id)
            ?.let { ResponseEntity.ok(it) }
            ?: ResponseEntity.notFound().build()
    }
}

application.yml:

server:
  port: 8082

spring:
  application:
    name: ORDER-SERVICE

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

Running the System

Start services in order:

  1. Eureka Server (port 8761)
  2. Product Service (port 8081)
  3. Order Service (port 8082)

Check Eureka Dashboard: Visit http://localhost:8761 - you should see both services registered.

Test:

# Get products
curl http://localhost:8081/api/products

# Get orders (automatically fetches product details via Eureka)
curl http://localhost:8082/api/orders

# Response includes product details fetched from Product Service
{
  "id": 1,
  "productId": 1,
  "quantity": 2,
  "totalPrice": 300000.0,
  "productDetails": {
    "id": 1,
    "name": "Laptop",
    "price": 150000.0
  }
}

Advanced Features

Load Balancing (Run Multiple Instances):

# Start multiple Product Service instances on different ports
java -jar product-service.jar --server.port=8081
java -jar product-service.jar --server.port=8091

# Eureka will load balance requests across both

Health Checks:

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90

Fallback with Circuit Breaker (using Resilience4j):

@Service
class OrderService(
    @LoadBalanced private val restTemplate: RestTemplate,
    private val circuitBreakerFactory: CircuitBreakerFactory<*, *>
) {
    fun getOrderById(id: Long): Order? {
        val order = orders[id] ?: return null

        val circuitBreaker = circuitBreakerFactory.create("productService")
        val productDetails = circuitBreaker.run(
            {
                restTemplate.getForObject(
                    "http://PRODUCT-SERVICE/api/products/${order.productId}",
                    ProductDetails::class.java
                )
            },
            {
                // Fallback when Product Service is down
                ProductDetails(
                    id = order.productId,
                    name = "Product information unavailable",
                    price = 0.0
                )
            }
        )

        return order.copy(productDetails = productDetails)
    }
}

The Bottom Line

Eureka gives you:

  • Dynamic service discovery (no hardcoded URLs)
  • Client-side load balancing
  • Automatic health checking
  • Zero-downtime deployments (new instances register, old ones deregister)

You just built a microservices system that scales horizontally and handles failures gracefully. Secure your services with JWT authentication in Spring Security, deploy on Kubernetes for container orchestration, or use Docker Compose for local development.

Sources: