25-01-2021
How to implement service discovery with Spring?
With the recent evolution of new architectures, it is very common to face new problems and challenges. These require innovative solutions to ensure a more robust, easy to maintain and evolving architecture.
The transition from traditional architectures, based in monoliths to microservices-based architectures, which provide more resilient and elastic solutions, is followed by a fundamental management challenge. What before was a single, cohesive and interconnected piece, is now a set of heterogeneous pieces requiring intercommunication between themselves.
But what are the fundamental characteristics of each of these architectural models?
Monolithic architecture
Monolithic architecture exists since the creation of information systems. Up until these days, it still is an architectural style widely used, especially by proprietary off-the-shelf products, which inherit a significant legacy history of code on top of code (which is not easy nor cheap to change). By splitting the software into tiers — which typically include one or more user interfaces, an application layer, a data access layer and the data persistence layer — the development of new features is done vertically in block.
This inherent essence offers some positive points, such as less transversal concerns (all modules have, in some way, dependencies and strong connections between themselves), easy debugging and functional testing execution (because all functionalities “speak the same language” and pass through the same components) and the simplicity in deploy management (because the system is composed of one or a very limited set of pieces).
However, these architectures’ disadvantages limit a lot of the flexibility and agility required by organisations nowadays. On one side, scalability is limited, since system redundancy is achieved by replicating the entire or a big slice of a system. The risk of introducing changes is another of the inherent problems, as deploys become heavier and, by being done in a block, affect and impact the system’s overall functionality. Service and feature design are restricted by a fixed preconceived model, which can condition the solution’s flexibility and performance. Lastly, these solutions have weak technological versatility, because they restrain the technologies development and programming languages defined to the solution (and that, somewhere in time, bring limitations and become obsolete).
Microservice-based architecture
Microservice-based architectures are recent, and although they already exist for some year only in the last ones they have been widely adopted due to the maturity and expectations they have been delivering. The fundamental concept behind this type of architecture is the separation of the solution implementation in isolated, smaller components, with a specific purpose and responsibility (the microservices).
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.
The great benefits of these architectures are the isolation between components (giving them sufficient autonomy to have independent life cycles to answer to their stakeholder’s necessities) and the high level of scalability (as load and response needs can be managed at each microservice level, instead of the entire solution). Other advantages include: agility since this architecture facilitates and promotes experimentation and prototyping; technology heterogeneity, because each microservice is only required to be communicative, it can be implemented in any technology or programming language which is more appropriate to the actual need; ease of understanding because the solution is designed in a clearer and “cleaner” way with well-defined borders of responsibility between the components; and resilience because we can build systems that handle the total failure of services and degrade functionality accordingly.
Although these advantages are very relevant nowadays, there are some restrictions and difficulties that should be addressed to ensure a successful implementation and management of a microservice-based architecture. The management complexity is one of the most important ones because the solution consists of multiple pieces with very different characteristics, so a high level of process automation is required to help with this Another important challenge is the component distribution, which by its granularity and architectural purpose doesn’t require to be located in one or more specific places and can scale independently. Finally, another difficulty consists of functional testing to applications, as each test circuit can come across multiple microservices designed with distinct behaviours.
Due to this solution’s nature, one of the significant challenges is managing each microservices’ location. One of the good practices with microservices-based architectures is automatically determining the service location, a feature known as service discovery.
Service Discovery
Service Discovery is a functionality that enables the abstraction of consumed resources (services) location from the solution implementation. Because a microservice can be executed in multiple locations, and providing that the same are not fixed and may vary according to microservice scale-in and scale-out, it is necessary to provide applications with the ability to find where the microservice they want to access is located.
The process is relatively simple:
- On start-up, the service instance registers itself in the service directory providing its current location. A service is identified with a logical name, so multiple instances of the same service will register themselves with the same name, represented in numerous locations.
- The service subscriber queries the service directory for the logical name of the service it wants to consume. The service directory returns all known locations of the service. Then it is the responsibility of the subscriber to decide which location to access the service. Afterwards, this functionality can be combined with a load balancing mechanism to manage this access more efficiently. There are several service discovery implementations: Eureka (Netflix) and Kubernetes (Google) are two of the most well-known.
Below is a practical example of a simple setup that you may use as a kickstart to your first project based on microservices architecture.
Practical example: How to implement service discovery with Spring?
To create, configure and implement an example that demonstrates the service discovery functionality the following frameworks and tools can be used:
- IntelliJ IDE
- Spring Initializr
- Spring Boot
- Spring Cloud
- Eureka Service Discovery
- Maven
1. Create the service discovery service
For this demonstration, we’ll use the service discovery implementation available from Netflix: Eureka.
A Eureka server is necessary to host all functionality for service registry and query. For that, create a project named DemoApplication using the Spring Initializr tool.
This project only requires the addition of a single dependency: Eureka Server.
DemoApplication
This server will be hosted in port 8761. The application code only requires the addition of the annotation @EnableEurekaServer to enable a Eureka server.
2. Create the services
It is necessary to create two projects to host the services that will communicate using service discovery. The services are named Demo1, the consumer, and Demo2, the provider.
Both services only require adding the following two dependencies through Spring Initializr:
- Spring Web
- Eureka Discovery Client
Demo 1
Demo 2
3. Startup the services
After the creation of all services projects it’s time to start them up in the following order:
- DemoApplication(Eureka server)
- Demo2 andDemo1 (Eureka clients)
Once started, services Demo1 and Demo2 should have successfully registered in the service directory (Eureka server).
4. Implement service invocation
Now that service discovery is tested, the only thing remaining is implementing the invocation of Demo2 service by Demo1 using its functionality.
For that, a class of type RestController making use of RestTemplate needs to be created to make an HTTP call to Demo2 service.
Afterwards, RestTemplate must be added as a Spring managed bean. The @LoadBalancer annotation instructs Spring Cloud to use its support for load balancing (provided, in this case, by Ribbon).
To accomplish that the following code should be added to the Spring initialization class (DemoApplication).
Afterwards, in Demo2 service project, a RestController class should be created with the following code to handle the service call.
5. Test invocation using service discovery
Once configurations are done, all it’s needed is to restart all projects and make a new request to http://localhost:8080.
Through the console, it’s possible to confirm if the invocation of Demo2 service from Demo1 service using service discovery was successful (“successful request”).