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:
- Eureka Server - Service registry
- Product Service - Manages products
- 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:
- Eureka Server (port 8761)
- Product Service (port 8081)
- 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: