Spring Cloud Gateway With Service Discovery Using HashiCorp Consul
This article introduces HashiCorp Consul, a service registry and discovery tool that integrates well with Spring Boot and supports reactive programming.
Join the DZone community and get the full member experience.
Join For FreeThis article will explain some basics of the HashiCorp Consul service and its configurations. It is a service networking solution that provides service registry and discovery capabilities, which integrate seamlessly with Spring Boot. You may have heard of Netflix Eureka; here, Consul works similarly but offers many additional features. Notably, it supports the modern reactive programming paradigm. I will walk you through with the help of some applications.
- Spring Boot
- Spring Cloud Gateway
- Spring Cloud Consul
- Spring Boot Actuator
The architecture includes three main components:
- Consul
- Service application
- Gateway
We have to download and install the Consul service in the system from the Hashicorp Consul official website. For development purposes, we have to start it using a command in PowerShell (in Windows).
consul agent -dev
This is the place where we can see all the applications registered with Consul. The default port for accessing the Consul dashboard is 8500. Once it starts successfully, you will see something like below.
The next step is to register the Gateway and Service applications to Consul. Once those are added, they will appear in this same dashboard. When multiple instances of the same service are running, Consul continuously monitors their health using "Actuator." If any of them report an unhealthy status, Consul will automatically deregister them from the registry.

It is a simple service application for exposing the APIs. We added an @EnableDiscoveryClient annotation in the main class to register the service in Consul for service discovery. If you run the application under multiple ports then you can see multiple instances in consul dashboard. Used the Actuator to expose the health status.
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceApp {
public static void main(String[] args) {
SpringApplication.run(ServiceApp.class, args);
}
}
Maven Configuration
<properties>
<java.version>21</java.version>
<spring.cloud.version>2023.0.4</spring.cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Application Property File
# Assigning a unique name for the service
spring.application.name=service-app
# Application will use random ports
server.port=0
spring.webflux.base-path=/userService
logback.log.file.path=./logs/service
# ~~~ Consul Configuration ~~~
# It assigns a unique ID to each instance of the service when running multiple instances,
# allowing them to be registered individually in Consul for service discovery.
spring.cloud.consul.discovery.instance-id=${spring.application.name}-${server.port}-${random.int[1,99]}
# To access centralized configuration data from Consul
spring.cloud.consul.config.enabled=false
# To register the service in Consul using its IP address instead of the hostname.
spring.cloud.consul.discovery.prefer-ip-address=true
# The service will register itself in Consul under this name, which the gateway will use for service discovery while routing requests.
spring.cloud.consul.discovery.service-name=${spring.application.name}
# Ip to communicate with consul server
spring.cloud.consul.host=localhost
# Consul runs on port 8500 by default, unless it is explicitly overridden in the configuration.
spring.cloud.consul.port=8500
# Remapping the Actuator URL in Consul since a base path has been added.
spring.cloud.consul.discovery.health-check-path=${spring.webflux.base-path}/actuator/health
# Time interval to check the health of service.
spring.cloud.consul.discovery.health-check-interval=5s
# Time need to wait for the health check response before considering it as timed out
spring.cloud.consul.discovery.health-check-timeout=5s
# The maximum amount of time a service can remain in an unhealthy state before Consul marks it as critical and removes it from the service catalog.
#spring.cloud.consul.discovery.health-check-critical-timeout=1m
Sample API
@GetMapping(value = "getStatus", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResponseEntity<Object>> healthCheck() {
logger.info("<--- Service to get status request : received --->");
logger.info("<--- Service to get status response : given --->");
return Mono.just(ResponseEntity.ok("Success from : " + portListener.getPort()));
}
It is developed with the help of Spring Cloud Gateway. And it consists of the same libraries as the Service application. Consul is used for registering and service discovery of the application. Used the Actuator to expose the health status.
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class, args);
}
}
Maven Configuration
<properties>
<java.version>21</java.version>
<spring.cloud.version>2023.0.4</spring.cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
Application Property File
# Assigning a unique name for the service
spring.application.name=gateway-app
server.port=3000
logback.log.file.path=./logs/gateway
# ~~~ Consul Configuration ~~~
# It is used in Spring Cloud Gateway to handle automatic route discovery from a service registry
# When we are configuring as false, we have to explicitly configure routing of each API requests.
spring.cloud.gateway.discovery.locator.enabled=false
spring.cloud.consul.discovery.instance-id=${spring.application.name}-${server.port}-${random.int[1,99]}
spring.cloud.consul.config.enabled=false
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.discovery.service-name=${spring.application.name}
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
Since we have set spring.cloud.gateway.discovery.locator.enabled to false, we need to explicitly configure the routing for each API request as shown below. For the routing destination URL, instead of specifying the actual URL of the service application, we map it to the load-balanced (lb) URL provided by Consul using the service name.
- In normal gateway
spring.cloud.gateway.routes[0].uri=http://192.168.1.10:5000 - In service discovery enabled gateway
spring.cloud.gateway.routes[0].uri=lb://service-app
#~~~ Example for a url routing ~~~
spring.cloud.gateway.routes[0].id=0
# Instead of configuring the actual url of service application, we are mapping in to the lb url of "consul" with service name.
spring.cloud.gateway.routes[0].uri=lb://service-app
# Rest of the configuration will keep as same as spring cloud gateway configuration
spring.cloud.gateway.routes[0].predicates[0]=Path=/userService/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/userService/(?<segment>.*), /userService/${segment}
spring.cloud.gateway.routes[0].filters[1]=PreserveHostHeader
Final Consul Dashboard

Here, we can see one instance of gateway-app and two instances of service-app, as I am running two instances of the service app under different ports.
Let's test it by calling a sample API through the gateway to verify that it's working.
Upon the first API call:

Upon the second API call:
We can see that each time the API returns a response from a different instance.
GitHub
Please check here to get the full project. Thanks for reading!
Opinions expressed by DZone contributors are their own.
Comments