Spring Cloud - Service Discovery with Netflix Eureka & Spring Boot 3.x.x

Spring Cloud - Service Discovery with Netflix Eureka & Spring Boot 3.x.x

Spring Cloud Service Discovery with Spring Boot 3.x, Java 17 & Spring Netflix Eureka

In this article, we are going to talk about one of the best service discovery tools out in the market which is Eureka from the Netflix OSS set of tools. This brings us to our first question.

What is Service Discovery and why should we use it?

To sum it up in brief, as the name suggests, it is a service that runs with the purpose of registering all the services in a microservice-based application and providing their whereabouts as and when needed. This might be a bit confusing if you're reading this for the first time. Let's try to understand the scenario a bit better.

Let's say you have 2 microservices in your food delivery application.

  • Location Service - Handles all the location related data/functionalities

  • Restaurant Service - Handles all the food/restaurant related data/functionalities.

When you get an order, you need to perform certain tasks. One of which might be to show the location of a restaurant on a map. For this, you might need to fetch some details from Location Service.

Now both of these services are using cutting-edge tech with containers, virtualization, and whatnot. This means at some point these cells/pods/servers/instances (whatever suits your desired tech stack) are going to change their location. Maybe the cells died and respawned with a new address. Or a weekly restart.

Since all the services are independent, how are you going to keep a track of the latest location of a service given that we need an entire address with ports for each application?

This is where Service Registry comes in.

With a Service Registry in place, all you need to do is communicate with Service Registry. Ask them the whereabouts of a particular service by a simple identifier that acts as a key for that particular service in the Registry and you never have to make provisions to store the paths of a service.

This entire process of discovering the details of the location of a service is called Service Discovery.


Hands-on with Service Discovery

Let's quickly first list down the stack that we're going to use for this article.

  • Spring Boot 3.x.x

  • Java 17

  • Gradle 7.6

  • Spring Cloud - 2022.0.2

This is important to note that we should use the versions in accordance with the official Spring documentation as you might see random errors if versions are not compatible and you'd spend an eternity solving the wrong thing.

Let's proceed step by step with the setup.


Creating dependent services

First, we're going to create two services as discussed above. Location & Restaurant Service. For this, I'm just going to give the little snippets where we have important info. Rest all is basic Spring Boot Rest Service stuff.

Location Service

Here we create a controller which returns an address by restaurant id.

@RestController
@RequestMapping("/location")
public class LocationController {

    @GetMapping("/restaurant/{id}")
    public String getLocationByRestaurantId(@PathVariable int id) {
        return "221B Undertaker Street";
    }
}

Runs on port: 9090

server:
  port: 9090

Restaurant Service

Here we have a create order call which internally calls location service to get the restaurant's address by its id.

@RestController
@RequestMapping("/restaurant")
@Slf4j
public class RestaurantController {

    @PostMapping("/{restaurantId}/order")
    public String createOrderForRestaurant(@PathVariable int restaurantId) {
        RestTemplate rt = new RestTemplate();
        String location = rt.getForObject(String.format("http://localhost:9090/location/restaurant/%d", restaurantId), String.class);
        log.info("Restaurant Address : {}", location);
        return location;
    }
}

Runs on port: 9091

server:
  port: 9091

Now, we run both services and test them by creating an order.

curl --location --request POST 'http://localhost:9091/restaurant/1/order'

So, at this point, we have 2 services up and running. But we see the problem that we discussed earlier.

Restaurant service is calling a hard-coded URL for Location Service which is : localhost:9090

What if at some point port is changed? Or in a distributed env, domain/IP address itself is changed?

This brings us to our next section where we register our services in Eureka Service Registry and fetch information from there.


Eureka Service Registry

Eureka Service Registry is just another service that acts as a connection point for both/all services. Let's set up one for our application.

For a Eureka Server, we need to add Spring Cloud dependencies to our project which makes our build.gradle look like this :

ext {
    set('springCloudVersion', "2022.0.2")
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
    implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.2'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

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

This is where all the magic happens.

NOTES :

  1. This isn't the complete build.gradle. I've specifically provided the snippet that's responsible for Eureka Server.

  2. JAXB is specially added since it has been removed from java packaging since Java 11 and it's required by Eureka.

Next, we update our properties with the desired port and some Eureka-specific values.

server:
  port: 9999

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    serviceUrl:
      defaultZone: http://localhost:9999/eureka/

register-with-eureka: Tells eureka to not register this service as a client since this is the server itself.

fetch-registry : No need to fetch registry if don't need it.

ServiceUrl > DefaultZone : Tells Eureka to look for service at this location instead of the default 8761 port.

The final step to configure Eureka Server is to enable the server itself via Spring Boot Configuration annotation.

@SpringBootApplication
@EnableEurekaServer // This annotation starts the Eureka Server
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

If everything goes fine, you should be able to see a page like this on http://localhost:9999/

Great! Now we have our Service Registry setup complete which is showing no instances are registered as of now. Next is to start using this for our application/services.


Registering Location Service with Eureka

To register Location Service with Eureka Server, once again we'll have to do a similar drill as we did to create Eureka Server. The only difference this time would be that we'll be pulling Eureka Client instead of Server.

ext {
    set('springCloudVersion', "2022.0.2")
}

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

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.2'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

To enable discovery for this service, we have to inform the application regarding our Eureka Server. This can be done via some properties.

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:9999/eureka/

spring:
  application:
    name: Location Service

If you don't specify a name for your Spring application, Eureka will show the name as UNKNOWN for that service.

Once this is all done, we're all set to enable Discovery Client for our Location Service. This is achieved via annotation like we enabled the server earlier.

@SpringBootApplication
@EnableDiscoveryClient // This is where we tell the service to enable the client
public class LocationServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(LocationServiceApplication.class, args);
    }

}

Now make sure the Eureka server is up and running. Start Location Service. Refresh the Eureka page and voila!! You should be able to see your service listed on Eureka.

Now the only thing left is to use this Eureka Server to discover our Location service in our Restaurant Service instead of explicitly providing the URL.


Discovering Location Service via Eureka Service Registry

For our Restaurant Service, again we need to setup client discovery. The steps are the same as the Location service.

Properties config to inform where Eureka is.

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:9999/eureka/

spring:
  application:
    name: Restaurant Service

Enabling Discovery:

@SpringBootApplication
@EnableDiscoveryClient
public class RestaurantServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestaurantServiceApplication.class, args);
    }

}

Now comes the final and the best part of the article. For which we made all the efforts. Let's see the code first.

@Autowired
    private DiscoveryClient discoveryClient;
    @PostMapping("/{restaurantId}/order")
    public String createOrderForRestaurant(@PathVariable int restaurantId) {
        String baseUrl = discoveryClient.getInstances("Location Service")
                .stream()
                .findFirst()
                .map(ServiceInstance::getUri)
                .orElse(null).toString();
        RestTemplate rt = new RestTemplate();
        String location = rt.getForObject(String.format("%s/location/restaurant/%d", baseUrl, restaurantId), String.class);
        log.info("Restaurant Address : {}", location);
        return location;
    }

If you compare it with the initial code, you'll be able to see the differences. To get the base URL of Location Server, we're now using Eureka Service Discovery instead of hard-coding the URL.

To achieve this, we autowired the bean of Discovery Client and fetched the list of instances for Location Service by NAME. How convenient is that!! Love it! :D

Next, we just iterated over it and fetched the first instance, and hit the API with that. Of course, we can add further logic to it and all the distributed-system-fu. But we'll keep it for later.


With this, we saw how we're able to create a Service Registry and enable service discovery via Spring Cloud and Netflix OSS stack. That's all for today.

See you next time. Thanks for reading.

Feedback is always welcome. :)

Ciao.