Consul and Micronaut - Microservices service discovery
Use Consul service discovery to expose your Micronaut apps.
Authors: Sergio del Amo
Micronaut Version: 1.1.0.M1
1 Getting Started
In this guide, we are going to create three microservices and register them with Consul Service discovery.
Consul is a distributed service mesh to connect, secure, and configure services across any runtime platform and public or private cloud
You will discover how Micronaut eases Consul integration.
1.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
1.2 Solution
We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/micronaut-guides/micronaut-microservices-services-discover-consul-groovy.git
Then, cd
into the complete
folder which you will find in the root project of the downloaded/cloned project.
2 Writing the App
Lets describe the microservices you are going to build through the tutorial.
-
bookcatalogue
- It returns a list of books. It uses a domain consisting of a book name and isbn. -
bookinventory
- It exposes an endpoint to check whether a book has sufficient stock to fullfil an order. I uses a domain consisting of a stock level and isbn. -
bookrecommendation
- It consumes previous services and exposes an endpoint which recommends book names which are in stock.
Initially we are going to hardcode the addresses where the different services are in the bookcatalogue
service.
As shown in the previous image, the bookcatalogue
hardcodes references to its collaborators.
In the second part of this tutorial we are going to use a discovery service.
o learn about registration patterns:
We will use a self‑registration pattern. Thus, each service instance is responsible for registering and deregistering itself with the service registry. Also, if required, a service instance sends heartbeat requests to prevent its registration from expiring.
Services register when they start up:
We will use client‑side service discovery, clients query the service registry, select an available instance, and make a request.
2.1 Catalogue Microservice
Create the bookcatalogue
microservice:
mn create-app --lang groovy example.micronaut.bookcatalogue.bookcatalogue
The previous command creates a folder named bookcatalogue
and a Micronaut app inside it with
default package: example.micronaut.bookcatalogue
.
Create a BooksController
class to handle incoming HTTP requests into the bookcatalogue
microservice:
package example.micronaut.bookcatalogue
import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
@CompileStatic
@Controller("/books") (1)
class BooksController {
@Get("/") (2)
List<Book> index() {
[
new Book("1491950358", "Building Microservices"),
new Book("1680502395", "Release It!"),
new Book("0321601912", "Continuous Delivery:"),
]
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /books |
2 | The @Get annotation is used to map the index method to /books requests that use an HTTP GET. |
The previous controller responds a List<Book>
. Create the Book
POGO:
package example.micronaut.bookcatalogue
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor
@TupleConstructor
@EqualsAndHashCode
@CompileStatic
class Book {
String isbn
String name
}
Write a test:
package example.micronaut.bookcatalogue
import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class BooksControllerSpec extends Specification {
@Shared
@AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
@Shared
@AutoCleanup
HttpClient client = HttpClient.create(embeddedServer.URL)
void "it is possible to retrieve books"() {
when:
HttpRequest request = HttpRequest.GET("/books") (1)
List books = client.toBlocking().retrieve(request, Argument.of(List, Book)) (2)
then:
books.size() == 3
books.contains(new Book("1491950358", "Building Microservices"))
books.contains(new Book("1680502395", "Release It!"))
}
}
1 | It is easy to create HTTP requests with a fluid API. |
2 | Parse easily JSON into Groovy objects. |
Edit application.yml
micronaut:
application:
name: bookcatalogue (1)
server:
port: 8081 (2)
1 | Configure the application name. The app name will be use by the discovery service. |
2 | Configure the app to listen at port 8081 |
Create a file named application-test.yml
which is used in the test environment:
micronaut:
server:
port: -1 (1)
1 | Start the micronaut microservice at a random port when running the tests. |
2.2 Inventory Microservice
Create the bookinventory
microservice:
mn create-app --lang groovy example.micronaut.bookinventory.bookinventory
Modify build.gradle
to add validation capabilities.
compile "io.micronaut:micronaut-validation"
compile "io.micronaut.configuration:micronaut-hibernate-validator"
The previous command creates a folder named bookinventory
and a Micronaut app inside it with
default package: example.micronaut.bookinventory
.
Create a Controller:
package example.micronaut.bookinventory
import groovy.transform.CompileStatic
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.validation.Validated
import javax.validation.constraints.NotBlank
@CompileStatic
@Validated (1)
@Controller("/books") (2)
class BooksController {
@Produces(MediaType.TEXT_PLAIN) (3)
@Get("/stock/{isbn}") (4)
Boolean stock(@NotBlank String isbn) {
bookInventoryByIsbn(isbn).map { bi -> bi.getStock() > 0 }.orElse(null)
}
private Optional<BookInventory> bookInventoryByIsbn(String isbn) {
if(isbn == "1491950358") {
return Optional.of(new BookInventory(isbn, 4))
} else if(isbn == "1680502395") {
return Optional.of(new BookInventory(isbn, 0))
}
Optional.empty()
}
}
1 | Define Validated annotation at the class level to signal any class that requires validation. |
2 | The class is defined as a controller with the @Controller annotation mapped to the path /books |
3 | By default a Micronaut’s response uses application/json as Content-Type . We are returning a String not a JSON object. Because of that, we set it to text/plain . |
4 | The @Get annotation is used to map the index method to /books/stock/{isbn} requests that use an HTTP GET. |
The previous controller uses a POGO:
package example.micronaut.bookinventory
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor
@CompileStatic
@TupleConstructor
@EqualsAndHashCode
class BookInventory {
String isbn
Integer stock
}
Write a test:
package example.micronaut.bookinventory
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.exceptions.HttpClientException
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import org.junit.Test
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class BooksControllerSpec extends Specification {
@Shared
@AutoCleanup
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
@Shared
@AutoCleanup
HttpClient client = HttpClient.create(embeddedServer.URL)
void "for a book with inventory true is returned"() {
when:
Boolean hasStock = client.toBlocking().retrieve(HttpRequest.GET("/books/stock/1491950358"), Boolean)
then:
noExceptionThrown()
hasStock
}
void "for a book without inventory false is returned"() {
when:
Boolean hasStock = client.toBlocking().retrieve(HttpRequest.GET("/books/stock/1680502395"), Boolean)
then:
noExceptionThrown()
hasStock == Boolean.FALSE
}
void "for an invalid ISBN 404 is returned"() {
when:
client.toBlocking().retrieve(HttpRequest.GET("/books/stock/XXXXX"))
then:
def e = thrown(HttpClientResponseException)
e.response.status == HttpStatus.NOT_FOUND
}
}
Edit application.yml
micronaut:
application:
name: bookinventory (1)
server:
port: 8082 (2)
1 | Configure the application name. The app name will be used later in the tutorial. |
2 | Configure the app to listen at port 8082 |
Create a file named application-test.yml
which is used in the test environment:
micronaut:
server:
port: -1 (1)
1 | Start the micronaut microservice at a random port when running the tests. |
2.3 Recommendation Microservice
Create the bookrecommendation
microservice:
mn create-app --lang groovy example.micronaut.bookrecommendation.bookrecommendation
The previous command creates a folder named bookrecommendation
and a Micronaut app inside it with
default package: example.micronaut.bookrecommendation
.
Create an interface to map operations with bookcatalogue
and a Micronaut Declarative HTTP Client to consume it.
package example.micronaut.bookrecommendation
import io.reactivex.Flowable
interface BookCatalogueOperations {
Flowable<Book> findAll()
}
package example.micronaut.bookrecommendation
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
@Client("http://localhost:8081") (1)
interface BookCatalogueClient extends BookCatalogueOperations {
@Get("/books")
Flowable<Book> findAll()
}
1 | Use @Client to use declarative HTTP Clients |
The client returns a POGO. Create it in the bookrecommendation
:
package example.micronaut.bookrecommendation
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor
@CompileStatic
@TupleConstructor
@EqualsAndHashCode
class Book {
String isbn
String name
}
Create an interface to map operations with bookinventory
and a Micronaut Declarative HTTP Client to consume it.
package example.micronaut.bookrecommendation
import io.reactivex.Maybe
import javax.validation.constraints.NotBlank
interface BookInventoryOperations {
Maybe<Boolean> stock(@NotBlank String isbn)
}
package example.micronaut.bookrecommendation
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Maybe
import javax.validation.constraints.NotBlank
@Client("http://localhost:8082") (1)
interface BookInventoryClient extends BookInventoryOperations {
@Get("/books/stock/{isbn}")
Maybe<Boolean> stock(@NotBlank String isbn)
}
1 | Use @Client to use declarative HTTP Clients |
Create a Controller which injects both clients.
package example.micronaut.bookrecommendation
import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.reactivex.Flowable
@CompileStatic
@Controller("/books") (1)
class BookController {
private final BookCatalogueOperations bookCatalogueOperations
private final BookInventoryOperations bookInventoryOperations
BookController(BookCatalogueOperations bookCatalogueOperations,
BookInventoryOperations bookInventoryOperations) { (2)
this.bookCatalogueOperations = bookCatalogueOperations
this.bookInventoryOperations = bookInventoryOperations
}
@Get("/") (3)
Flowable<BookRecommendation> index() {
return bookCatalogueOperations.findAll()
.flatMapMaybe { b -> bookInventoryOperations.stock(b.isbn)
.filter { hasStock -> hasStock == Boolean.TRUE }
.map {rsp -> b }
}.map { book -> new BookRecommendation(book.name) }
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /books |
2 | Constructor injection |
3 | The @Get annotation is used to map the index method to /books requests that use an HTTP GET. |
The previous controller returns a Flowable<BookRecommendation>
. Create the BookRecommendation
POGO:
package example.micronaut.bookrecommendation
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor
@TupleConstructor
@CompileStatic
@EqualsAndHashCode
class BookRecommendation {
String name
}
BookCatalogueClient
and BookInventoryClient
will fail to consume the bookcatalogue
and bookinventory
during the tests phase.
Using the @Fallback annotation you can declare a fallback implementation of a client that will be picked up and used once all possible retries have been exhausted
Create @Fallback
alternatives in the test
classpath.
package example.micronaut.bookrecommendation
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.retry.annotation.Fallback
import io.reactivex.Maybe
import javax.inject.Singleton
import javax.validation.constraints.NotBlank
@Requires(env = Environment.TEST)
@Fallback
@Singleton
class BookInventoryClientStub implements BookInventoryOperations {
@Override
Maybe<Boolean> stock(@NotBlank String isbn) {
if(isbn == "1491950358") {
return Maybe.just(Boolean.TRUE)
} else if(isbn == "1680502395") {
return Maybe.just(Boolean.FALSE)
}
Maybe.empty()
}
}
package example.micronaut.bookrecommendation
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.retry.annotation.Fallback
import io.reactivex.Flowable
import javax.inject.Singleton
@Requires(env = Environment.TEST)
@Fallback
@Singleton
class BookCatalogueClientStub implements BookCatalogueOperations {
@Override
Flowable<Book> findAll() {
Book buildingMicroservices = new Book("1491950358", "Building Microservices")
Book releaseIt = new Book("1680502395", "Release It!")
Flowable.just(buildingMicroservices, releaseIt)
}
}
Write a test:
package example.micronaut.bookrecommendation
import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class BookControllerSpec extends Specification {
@Shared
@AutoCleanup
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
@Shared
@AutoCleanup
HttpClient client = HttpClient.create(embeddedServer.URL)
void "retrieve books"() {
when:
HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/books"), Argument.of(List, BookRecommendation))
then:
response.status() == HttpStatus.OK
response.body().size() == 1
response.body().get(0).name == "Building Microservices"
}
}
Edit application.yml
micronaut:
application:
name: bookrecommendation (1)
server:
port: 8080 (2)
1 | Configure the application name. The app name will be used later in the tutorial. |
2 | Configure the app to listen at port 8080 |
Create a file named application-test.yml
which is used in the test environment:
micronaut:
server:
port: -1 (1)
1 | Start the micronaut microservice at a random port when running the tests. |
3 Running the app
Run bookcatalogue
microservice:
bookcatalogue $ ./gradlew run
Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details
> Task :bookcatalogue:compileJava
Note: Creating bean classes for 1 type elements
> Task :bookcatalogue:run
14:28:34.034 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081
Run bookinventory
microservice:
bookinventory $ ./gradlew run
Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details
> Task :bookinventory:compileJava
Note: Creating bean classes for 1 type elements
> Task :bookcatalogue:run
14:31:13.104 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8082
Run bookrecommendation
microservice:
bookrecommendation $ ./gradlew run
Starting a Gradle Daemon, 2 busy and 2 stopped Daemons could not be reused, use --status for details
> Task :bookrecommendation:compileJava
Note: Creating bean classes for 3 type elements
> Task :bookrecommendation:run
14:31:57.389 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080
You can run a cURL command to test the whole application:
$ curl http://localhost:8080/books
[{"name":"Building Microservices"}
4 Consul and Micronaut
4.1 Install Consul via Docker
The quickest way to start using Consul is via Docker:
docker run -p 8500:8500 consul
Alternatively you can install and run a local Consul instance.
The following screenshots show how to install/run Consul via Kitematic; graphical user interface for Docker.

Configure ports:

4.2 Integrate Consul
4.3 Book Catalogue
Modify build.gradle
to add discovery-client
dependency.
dependencies {
...
..
.
runtime "io.micronaut:micronaut-discovery-client"
}
Append to bookcatalogue
service application.yml
the following snippet:
consul:
client:
registration:
enabled: true
defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
Previous configuration registers a Micronaut app with Consul with minimal configuration. Discover a more complete list of Configuration options at ConsulConfiguration.
Disable consul registration in tests:
consul:
client:
registration:
enabled: false
4.4 Book Inventory
Modify build.gradle
to add discovery-client
dependency.
dependencies {
...
..
.
runtime "io.micronaut:micronaut-discovery-client"
}
Also, modify the application.yml
of the bookinventory
application with the following snippet:
consul:
client:
registration:
enabled: true
defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
Disable consul registration in tests:
consul:
client:
registration:
enabled: false
4.5 Book Recommendation
Modify build.gradle
to add discovery-client
dependency.
dependencies {
...
..
.
runtime "io.micronaut:micronaut-discovery-client"
}
Also, append to bookrecommendation
.application.yml
the following snippet:
consul:
client:
registration:
enabled: true
defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
Modify BookInventoryClient
and BookCatalogueClient
to use the service id instead of a harcoded ip.
package example.micronaut.bookrecommendation
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Flowable
@Client(id = "bookcatalogue") (1)
interface BookCatalogueClient extends BookCatalogueOperations {
@Get("/books")
Flowable<Book> findAll()
}
1 | Use the configuration value micronaut.application.name used in bookcatalogue as service id . |
package example.micronaut.bookrecommendation
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.reactivex.Maybe
import javax.validation.constraints.NotBlank
@Client(id = "bookinventory") (1)
interface BookInventoryClient extends BookInventoryOperations {
@Get("/books/stock/{isbn}")
Maybe<Boolean> stock(@NotBlank String isbn)
}
1 | Use the configuration value micronaut.application.name used in bookinventory as service id . |
Disable consul registration in tests:
consul:
client:
registration:
enabled: false
4.6 Running the App
Run bookcatalogue
microservice:
bookcatalogue $ ./gradlew run
...
> Task :bookcatalogue:run
14:28:34.034 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081
Run bookinventory
microservice:
bookinventory $ ./gradlew run
...
> Task :bookcatalogue:run
14:31:13.104 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8082
Run bookrecommendation
microservice:
bookrecommendation $ ./gradlew run
...
> Task :bookrecommendation:run
14:31:57.389 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080
Consul comes with a HTML UI. Open http://localhost:8500/ui in your browser.
You will see the services registered in Consul:

You can run a cURL command to test the whole application:
$ curl http://localhost:8080/books
[{"name":"Building Microservices"}
5 Next Steps
Read more about Consul support inside Micronaut.