Friday, July 28, 2023

Spring Boot Microservice - Service Registration and Discovery With Eureka

In this article we'll see how to do service registration and discovery using the Netflix Eureka service registry in Spring Boot microservices development.

What is service registration and discovery

In the post Spring Boot Microservices Example we have seen how to have interservice communication using RestTemplate.

ResponseEntity<List<AccountDto>> response = restTemplate.exchange("http://localhost:8082/account/customer/"+id, HttpMethod.GET, null, new ParameterizedTypeReference<List<AccountDto>>(){});

But the problem here is the hardcoding of IP address and port.

To communicate with other service, you do need to know where it is deployed and the port it is listening to but at the same time number of instances of Microservice and their locations are dynamic. Therefore, it is not at all advisable to hardcode this data.

Hardcoding the location and port has the following side effects-

  1. If URL changes that would mean change inside the code.
  2. You are coupling with a specific instance of the microservice.
  3. If you are tightly coupled with a specific instance of the microservice that means no load balancing too.
  4. With microservices deployed in cloud trying to hardcode instances is an even bigger problem.

Now, when we have established that hardcoding location of the microservice instances is not good question arise how will I know about the location of any microservice instance.

What if there is a tool that can keep track of all the service instances and inform about their current location when inquired. That's what SpringCloud Netflix Eureka brings to the table.

Using Eureka for Service registration and discovery

SpringCloud Netflix Eureka provides functionality to act as a service registry. Each microservice registers itself with the service registry which means providing data about its location- host name, port number, node name and any other data. Microservice registers itself to the service registry under the name you specify in the spring.application.name property.

Since Eureka service registry has the location information of each microservice instance, any microservice which needs to communicate with other services can use Eureka as service discovery to discover location information about the microservices.

Here note that SpringCloud Netflix Eureka provides client-side discovery implementation. In client-side discovery, client (service consumer) has the responsibility of determining the network location of other services. Client queries the service registry (Eureka server) to get that information, receives it from the registry and then send request directly to the microservice.

Eureka service discovery

The steps in the image can be explained as-

  1. Microservices register itself with Eureka service registry.
  2. Service consumer wishes to communicate with Microservice-1 so it sends a request to Eureka to get location information of Microservice-1.
  3. Server sends back a response to service consumer with required information.
  4. Client may also use load-balancing to choose one of the available service instances and performs a request directly.

Spring Boot Microservice with Eureka example

We'll use the example used in Spring Boot Microservices Example as our base example and make changes to it to include Eureka for Service registration and discovery.

Setting up the Eureka server

To setup Eureka server we'll create a new Spring Boot project with eureka server starter as dependency.

In STS create a new Spring Starter Project and give details as given here.

Spring Boot STS

Select "Eureka Server" as dependency under "Spring Cloud Discovery". With that the created pom.xml should look like this-

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.2</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.netjstech</groupId>
  <artifactId>discoveryservice</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>eureka-discovery-service</name>
  <description>Eureka Discovery Service</description>
  <properties>
    <java.version>17</java.version>
    <spring-cloud.version>2022.0.3</spring-cloud.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
In the properties you can notice the cloud version used for this example
<spring-cloud.version>2022.0.3</spring-cloud.version>

Adding configuration

In src/main/resources create a application.yml file and the following properties.

server:
  port:
    8761

spring:
  application:
    name:
      discovery-service
      
eureka:
  client:
    register-with-eureka:
      false
    fetch-registry:
      false
  instance:
    hostname: localhost  

By default, the registry also tries to register itself, so you need to disable that behavior by giving the following two properties-

eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

I am running Eureka on the same system so instance is localhost and the port Eureka server listens to is 8761.

Adding @EnableEurekaServer annotation

In the application class add @EnableEurekaServer annotation along with @SpringBootApplication which tells Spring to configure this service as a Eureka server which can act as a service registry and discovery.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaDiscoveryServiceApplication {

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

}

With these changes we are done with the service so you can run it and then access http://localhost:8761/ to see the UI for Eureka server.

Eureka Server UI

Right now, no instance is registered with Eureka server so now let's work on configuring our microservices as Eureka client so that the server can register them.

Registering Microservices as client

In our example there are two microservices Customer-Service and Account-Service so we need to add the following dependency in pom.xml of both services.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Also add dependencyManagement section.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

And spring cloud version in the properties section.

<properties>
  <java.version>17</java.version>
  <spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>

Now in the application.yml file of customer-service, add the following properties-

spring:
  application:
    name: customer-service

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

Same way in the application.yml file of account-service, add the following properties-

spring:
  application:
    name: account-service

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

A service with org.springframework.cloud:spring-cloud-starter-eureka on the classpath will be registered with the Eureka registry by its spring.application.name that is why this property is there.

Another Configuration is required to locate the Eureka server.

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka

With these changes we have configured both the services so that they can register with Eureka server. Having spring-cloud-starter-netflix-eureka-client on the classpath lets the service register itself with Eureka and also let the services act as clients which can query the registry to locate other services.

Please start both services and ensure Eureka Server service is also running. Once customer-service and account-service start successfully you can verify the registration of both instances in the UI of Eureka server.

Eureka Service Registry

Now change the getCustomerById() method in CustomerServiceImpl. URL passed in resttemplate.exchange should be changed from "http://localhost:8082/account/customer/"+id to http://ACCOUNT-SERVICE/account/customer/"+id.

@Override
public CustomerDto getCustomerById(Long id) {
  Customer customer = customerRepository.findById(id)
               .orElseThrow(()-> new CustomerServiceException("No customer found for the given id - " + id));
  
  ResponseEntity<List<AccountDto>> response = restTemplate.exchange("http://ACCOUNT-SERVICE/account/customer/"+id, HttpMethod.GET, null, 
      new ParameterizedTypeReference<List<AccountDto>>(){});
  List<AccountDto> accounts = response.getBody();
  CustomerDto customerDto = CustomerDto.builder()
         .id(customer.getId())
         .name(customer.getName())
         .age(customer.getAge())
         .city(customer.getCity())
         .accounts(accounts)
         .build();
  return customerDto;
}

Also add @LoadBalanced annotation when configuring RestTemplate as a bean.

@Configuration
public class TemplateConfig {
	
  @Bean 
  @LoadBalanced
  RestTemplate restTemplate() { 
    return new RestTemplate(); 
  }
}

Now, if you access http://localhost:8081/customer/1 internally in making a call to account-service from customer-service

restTemplate.exchange("http://ACCOUNT-SERVICE/account/customer/"+id, HttpMethod.GET, null, 
        new ParameterizedTypeReference<List<AccountDto>>(){});

ACCOUNT_SERVICE in the URL will be resolved by querying Eureka server to provide network information for the service registered as ACCOUNT-SERVICE.

That's all for this topic Spring Boot Microservice - Service Registration and Discovery With Eureka. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Spring Tutorial Page


Related Topics

  1. Spring Boot Microservices Introduction
  2. Spring Boot Microservice Example Using WebClient
  3. Spring Boot Microservice - Eureka + LoadBalancer + Feign
  4. Spring Boot Microservice Circuit Breaker Using Resilience4j
  5. Spring Boot Observability - Distributed Tracing, Metrics

You may also like-

  1. Spring Constructor Based Dependency Injection
  2. Spring MVC PDF Generation Example
  3. Spring Transaction Management Example - @Transactional Annotation and JDBC
  4. @FunctionalInterface Annotation in Java
  5. Interface in Java With Examples
  6. Reading File in Java Using Files.lines And Files.newBufferedReader
  7. Angular Attribute Binding With Examples
  8. Controlled and Uncontrolled Components in React

Thursday, July 27, 2023

Spring Boot Microservices Example

In the post Spring Boot Microservices Introduction we have already got an idea of what are Microservices and basic Microservice architecture. In this post we'll see an example of creating Microservice using Spring Boot.

In this Spring Boot Microservice example we'll have two services CustomerService and AccountService to cater to customer and account related functionality respectively.

When customer information is retrieved it should also get the accounts associated with the specific customer. For getting the associated accounts for a customer we'll have to make a call from CustomerService to AccountService. That way we'll also see communication between Microservices.

Let's go the whole nine yards (well atleast 7 and a half yards!) while creating these microservices. So, we'll have DB table for Customer and Account (using MySQL DB), JPA repository using Spring Data JPA so that we don't have to implement data access layer thus reducing the amount of boilerplate code, Lombok to further avoid boilerplate code for getters and setters and for creating objects.

Technologies used in the example

  • STS 4.1.19
  • Spring Boot 3.1.1
  • Spring 6.x
  • Lombok 1.8.28
  • MySQL 8.x

Customer Service Microservice

Let's start with Customer Service, we'll first create it without getting any account details and add that part later.

In STS create a new Spring Starter Project and give details as given here.

Spring Tool Suite Microservice config
STS Spring boot Microservice

With that the created pom.xml should look like this-

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.1</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.netjstech</groupId>
  <artifactId>customer-service</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>customer-service</name>
  <description>Customer Service</description>
  <properties>
    <java.version>17</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

DB related changes

In MySQL DB I have created a new schema Customer and connection URL points to that schema. In the resources folder create application.yml file and add following configuration.

server:
  port: 8081
 
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/customer
    username: DB_USERNAME
    password: DB_PASSWORD
  application:
    name: customer-service
  jpa:
    properties:
      hibernate:
        sqldialect: org.hibernate.dialect.MySQLDialect
        showsql: true

Please change the database URL, user name and password as per your DB configuration.

Customer Table

CREATE TABLE `customer`.`customer` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NULL,
  `age` INT NULL,
  `city` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

Customer Entity

Entity class which maps to this Customer table.

package com.netjstech.customerservice.dao.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name="customer")
public class Customer {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private int age;
	private String city;
}

As you can see Lombok annotations @Data (to generate getters, setters, equals and hashcode methods), @AllArgsConstructor, @NoArgsConstructor (no-args constructor is required as per specification) are used to generate the boilerplate code.

Customer Repository

Add an interface that extends JPARepository interface.

package com.netjstech.customerservice.dao.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.netjstech.customerservice.dao.entity.Customer;

public interface CustomerRepository extends JpaRepository<Customer, Long>{
  
}

Custom Exception class

A custom exception class CustomerServiceException class is also created.

public class CustomerServiceException extends RuntimeException{
    public CustomerServiceException() {
        super();
    }

	public CustomerServiceException(String msg){
		super(msg);
	}
	
    public CustomerServiceException (String msg, Throwable t) {
        super(msg, t);
    }
}

Service interface and implementation

package com.netjstech.customerservice.service;

import com.netjstech.customerservice.dao.entity.Customer;

public interface CustomerService {
	Customer saveCustomer(Customer customer);
	Customer getCustomerById(Long id);
}

CustomerServiceImpl class

package com.netjstech.customerservice.service;

import org.springframework.stereotype.Service;
import com.netjstech.customerservice.dao.entity.Customer;
import com.netjstech.customerservice.dao.repository.CustomerRepository;
import com.netjstech.customerservice.exception.CustomerServiceException;

@Service
public class CustomerServiceImpl implements CustomerService{
  private final CustomerRepository customerRepository;
  
  CustomerServiceImpl(CustomerRepository customerRepository){
    this.customerRepository = customerRepository;
    
  }

  @Override
  public Customer saveCustomer(Customer customer) {
    return customerRepository.save(customer);
  }
  
  @Override
  public Customer getCustomerById(Long id) {
    return customerRepository.findById(id)
                 .orElseThrow(()-> new CustomerServiceException("No customer found for the given id - " + id));
  }

}

CustomerRepository is injected here using constructor injection.

Customer Controller class

package com.netjstech.customerservice.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.netjstech.customerservice.dao.entity.Customer;
import com.netjstech.customerservice.service.CustomerService;

@RestController
@RequestMapping("/customer")
public class CustomerController {
  
  @Autowired
  private CustomerService customerService;
  
  @PostMapping
  public ResponseEntity<Customer> saveCustomer(@RequestBody Customer customer) {
    Customer returnedCustomer = customerService.saveCustomer(customer);
    return ResponseEntity.ok(returnedCustomer);
  }
  
  @GetMapping("/{id}")
  public ResponseEntity<Customer> getCustomerById(@PathVariable("id") Long customerId){
    Customer customer = customerService.getCustomerById(customerId);
    
    return ResponseEntity.ok(customer);
  }
}

Methods to save and find customer are mapped here using post mapping and get mapping. Methods return ResponseEntity instance with the HTTPStatus code as ok. As body of Response instance of customer is returned.

With this we are done with Customer Service, right click on the generated CustomerServiceApplication class and run it as Spring Boot App. If everything is fine then your application should be deployed on Tomcat and server should listen on port 8081 (server.port is given the value 8081 in application.yml file).

@SpringBootApplication
public class CustomerServiceApplication {

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

Let's have a quick check of our REST API using Postman.

Get with URL as http://localhost:8081/customer/1 should give you the Customer details.

As you can see right now there are no account details that we'll add a little bit later.

Account-Service MicroService

In order to create Account-Service as a Spring Boot microservice, in STS create a new Spring Starter Project and give details as given here.

name: account-service
Group id: com.netjstech
Artifact id: accountservice

Add the same dependencies as given in Customer Service.

DB related changes

In MySQL DB I have created a new schema Account and connection URL points to that schema. In the resources folder create application.yml file and add following configuration.

server:
  port: 8082
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/account
    username: DB_USERNAME
    password: DB_PASSWORD
  application:
    name: account-service
  jpa:
    properties:
      hibernate:
        sqldialect: org.hibernate.dialect.MySQLDialect
        showsql: true

Entries are very similar to what we did in Customer Service, port is 8082 this time so that both applications can run at the same time without any problem. DB URL and application name are also changed as per Account Service.

Please change the database URL, user name and password as per your DB configuration.

Account Table

SQL for creating Account table.

CREATE TABLE `account` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `account_num` varchar(255) NOT NULL,
  `balance` double NOT NULL,
  `customer_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_20mg37dn97899fquqomy38gjg` (`account_num`)
)

Surrogate key id is used as primary key and account number has unique constraint so that value for account number is unique.

Account Entity

Entity class which maps to the Account table.

package com.netjstech.accountservice.dao.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@Data
@Table(name="account")
public class Account {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	@Column(name = "account_num", nullable = false, unique = true)
	private String accountNumber;
	private double balance;
	private Long customerId;

}

Account Repository

package com.netjstech.accountservice.dao.repository;

import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.netjstech.accountservice.dao.entity.Account;

public interface AccountRepository extends JpaRepository<Account, Long>{
  List<Account> findAccountByCustomerId(Long id);
  Optional<Account> findByAccountNumber(String accountNumber);
}

Here two methods are added findAccountByCustomerId() and findByAccountNumber(). For methods starting with such names as "find", "count", "remove", "delete", Spring framework can automatically generate the correct query.

AccountServiceException class

A custom exception class AccountServiceException is also created.

package com.netjstech.accountservice.exception;

public class AccountServiceException extends RuntimeException{
    public AccountServiceException() {
        super();
    }

	public AccountServiceException(String msg){
		super(msg);
	}
	
    public AccountServiceException (String msg, Throwable t) {
        super(msg, t);
    }

}

Service interface and implementation

package com.netjstech.accountservice.service;

import java.util.List;
import com.netjstech.accountservice.dao.entity.Account;

public interface AccountService {
  
  Account saveAccount(Account account);
  Account getAccountByAccountNumber(String accountNumber);
  List<Account> getAccountsByCustomerId(Long costomerId);
  
}

AccountServiceImpl class

package com.netjstech.accountservice.service;

import java.util.List;
import org.springframework.stereotype.Service;
import com.netjstech.accountservice.dao.entity.Account;
import com.netjstech.accountservice.dao.repository.AccountRepository;
import com.netjstech.accountservice.exception.AccountServiceException;

@Service
public class AccountServiceImpl implements AccountService {
  
  private final AccountRepository accountRepository;
  AccountServiceImpl(AccountRepository accountRepository)  {
    this.accountRepository = accountRepository;
  }
  
  @Override
  public Account saveAccount(Account account) {
    return accountRepository.save(account);
  }
  @Override
  public Account getAccountByAccountNumber(String accountNumber) {
    Account account = accountRepository.findByAccountNumber(accountNumber)
             .orElseThrow(() -> new AccountServiceException("No Account found for the given account number: " + accountNumber));
    return account;
  }
  @Override
  public List<Account> getAccountsByCustomerId(Long costomerId) {
    List<Account> accounts = accountRepository.findAccountByCustomerId(costomerId);
    return accounts;
  }
}

AccountController class

package com.netjstech.accountservice.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.netjstech.accountservice.dao.entity.Account;
import com.netjstech.accountservice.service.AccountService;


@RestController
@RequestMapping("/account")
public class AccountController {
  private final AccountService accountService;
  
  AccountController(AccountService accountService){
    this.accountService = accountService;
  }
  
  @PostMapping
  public ResponseEntity<Account> saveAccount(@RequestBody Account account) {
    return ResponseEntity.ok(accountService.saveAccount(account));
  }
  
  @GetMapping("/{accountNumber}")
  public ResponseEntity<Account> getAccountByAccountNumber(@PathVariable("accountNumber") String accountNumber){
    System.out.println("id .. " + accountNumber);
    Account account = accountService.getAccountByAccountNumber(accountNumber);
    
    return ResponseEntity.ok(account);
  }
  
  @GetMapping("/customer/{id}")
  public List<Account> getAccountsByCustomerId(@PathVariable("id") Long customerId){
    System.out.println("id .. " + customerId);
    List<Account> accounts = accountService.getAccountsByCustomerId(customerId);
    
    return accounts;
  }
  
}

With this we are done with Account Service, right click on the generated AccountServiceApplication class and run it as Spring Boot App. If everything is fine then your application should be deployed on Tomcat and server should listen on port.

@SpringBootApplication
public class AccountServiceApplication {

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

We'll check AccountService methods using Postman.

With method selected as Post, URL as http://localhost:8082/account and Body (JSON) as

{
    "accountNumber": "A001",
    "balance": 8000.00,
    "customerId": 1

}

It should insert a record in Account table. Run it again with the following Body so that we have two accounts for customer with id 1.

 {
    "accountNumber": "A002",
    "balance": 8000.00,
    "customerId": 1

}

Now with method as Get and URL as http://localhost:8082/account/customer/1 you should get both accounts.

[
    {
        "id": 1,
        "accountNumber": "A001",
        "balance": 6000.0,
        "customerId": 1
    },
    {
        "id": 2,
        "accountNumber": "A002",
        "balance": 8000.0,
        "customerId": 1
    }
]

Communication between Microservices

At this point we have two separate Microservices running, catering to the Customer and Account functionality. As per our initial requirement when we get Customer by id, it should also show the associated accounts. For that we need communication between Customer Microservice and Account Microservice.

In this example we'll use RestTemplate for communicating with Microservice. Note that RestTemplate is going to be deprecated in coming Spring versions as it is in maintenance mode from Spring 5. WebClient is the suggested way as per Spring framework going forward.

Configuration for RestTemplate

package com.netjstech.customerservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class TemplateConfig {
	
  @Bean 
  RestTemplate restTemplate() { 
    return new RestTemplate(); 
  }
}

This configuration is done to instantiate RestTemplate so that it can be injected where it is required.

Since we need Account information in Customer Service so AccountDto is created and also a CustomerDto where List of accounts is also added.

AccountDto class

package com.netjstech.customerservice.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AccountDto {
	private Long id;
	private String accountNumber;
	private double balance;
	private Long customerId;
}

CustomerDto class

package com.netjstech.customerservice.dto;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CustomerDto {
  private Long id;
  private String name;
  private int age;
  private String city;
  private List<AccountDto> accounts;
}

Updated CustomerService interface and CustomerServiceImpl

public interface CustomerService {
	Customer saveCustomer(Customer customer);
	CustomerDto getCustomerById(Long id);
}
Now getCustomerById() method returns CustomerDto.

CustomerServiceImpl class

@Service
public class CustomerServiceImpl implements CustomerService{
  private final CustomerRepository customerRepository;
  private final RestTemplate restTemplate;
  
  CustomerServiceImpl(CustomerRepository customerRepository, RestTemplate restTemplate){
    this.customerRepository = customerRepository;
    this.restTemplate = restTemplate;    
  }

  @Override
  public Customer saveCustomer(Customer customer) {
    return customerRepository.save(customer);
  }
  
  @Override
  public CustomerDto getCustomerById(Long id) {
    Customer customer = customerRepository.findById(id)
                 .orElseThrow(()-> new CustomerServiceException("No customer found for the given id - " + id));
    
    ResponseEntity<List<AccountDto>> response = restTemplate.exchange("http://localhost:8082/account/customer/"+id, HttpMethod.GET, null, new ParameterizedTypeReference<List<AccountDto>>(){});

    List<AccountDto> accounts = response.getBody();
    CustomerDto customerDto = CustomerDto.builder()
           .id(customer.getId())
           .name(customer.getName())
           .age(customer.getAge())
           .city(customer.getCity())
           .accounts(accounts)
           .build();
    return customerDto;
    
  }
}

RestTemplate is also injected in the Service class. In the method getCustomerById() call is make to the AccountService (http://localhost:8082/account/customer/{id}) using the resttemplate.exchange() method. Here URL is hardcoded which is not the best way to communicate with a Microservice. Once the Response is received from the call (Note that RestTemplate is a Synchronous client to perform HTTP requests so it waits until response is received) CutomerDto object is created using builder() which uses builder pattern and available for use with @Builder annotation provided by Lombok.

Refer this post- Spring Boot Microservice - Service Registration and Discovery With Eureka to see a better way to have inter-service communication using service registry and discovery

Changes in CustomerController

Need to change return type of getCustomerById() method to CustomerDto.

@GetMapping("/{id}")
public ResponseEntity<CustomerDto> getCustomerById(@PathVariable("id") Long customerId){
  CustomerDto customer = customerService.getCustomerById(customerId);
  
  return ResponseEntity.ok(customer);
}
With these changes you should also get List of associated accounts along with customer information.
Spring boot microservice communication

That's all for this topic Spring Boot Microservices Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Spring Tutorial Page


Related Topics

  1. Spring Boot Microservice Example Using WebClient
  2. Spring Boot Microservice Example Using FeignClient
  3. Spring Boot Microservice - Load-Balancing With Spring Cloud LoadBalancer
  4. Spring Boot Microservice - Externalized Configuration With Spring Cloud Config
  5. Spring Boot Microservice Circuit Breaker Using Resilience4j

You may also like-

  1. Spring Constructor Based Dependency Injection
  2. Data Access in Spring Framework
  3. Spring MVC Excel Generation Example
  4. Passing Arguments to getBean() Method in Spring
  5. Unmodifiable or Immutable List in Java
  6. Binary Tree Traversal Using Breadth First Search Java Program
  7. What if run() Method Called Directly Instead of start() Method - Java Multi-Threading
  8. Angular Routing Concepts With Example

Tuesday, July 25, 2023

Spring Boot Microservices Introduction

This article gives an understanding of what is a Microservice, Monolith architecture Vs Microservice architecture, Spring Boot support for Microservices.

What is a Microservice

Microservice is an architectural style where your application is composed of small independent services. Microservices architectures makes it faster to develop applications and easier to scale.

Monolithic Architecture

In monolithic architecture all the software components of an application are assembled together and tightly packaged in a single code base.

Monolithic architecture

With this monolithic architecture you may argue that the benefits are-

  1. Development is easy- Since all the code is at one place so understanding flow is simpler and making changes is also simple. Inter-modular communication also doesn’t pose any problem.
  2. Deployment is easy- You need to deploy the archive for one application (JAR, WAR or EAR) on a server.

On the other hand. challenges of monolithic architecture are-

  • Monolithic code base may become quite huge making it hard to understand and maintain.
  • IDE may become slow because of the code base which means less productive developers.
  • Merging the codebase in repository may also become time consuming because of large code base
  • Deployment also takes more time and overloads the server.
  • Continuous development and integration is difficult because update in one component results in the redeployment of whole application.
  • Scaling of the application means making the copy of the whole application.
  • A monolithic architecture binds you to the technology stack you chose at the start of development.

Microservice Architecture

Defines an architecture that structures the application as a set of loosely coupled, collaborating services.

Microservice Architecture

Advantages of using Microservices

  1. Code base is small- Code base is not huge. Code of one service focuses on one business capability. That results in rapid development and maintenance is also easy.
  2. Smaller teams- Enables having smaller teams focused on their specific services.
  3. Loosely coupled- Enables independent development of services without being impacted by changes to other services and without affecting other services.
  4. Independently deployable- Enables a team to deploy their service without having to coordinate with other teams.
  5. Scalability- Services can be scaled independently. Any service requiring more resources can be scaled without scaling out the entire application.

Microservices with Spring Boot

Spring Boot provides a great platform for building microservices. Spring Boot, through its Spring Cloud starter provides many ready-to-run cloud patterns. Spring Cloud can help with service discovery, load-balancing, circuit-breaking, distributed tracing, and monitoring, it can also act as an API gateway.

That's all for this topic Spring Boot Microservices Introduction. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Spring Tutorial Page


Related Topics

  1. Spring Boot Microservices Example
  2. Spring Boot Microservice Example Using WebClient
  3. Spring Boot Microservice Example Using FeignClient
  4. Spring Boot Microservice + API Gateway + Resilience4J
  5. Spring Boot REST API CRUD Example With Spring Data JPA

You may also like-

  1. Spring Constructor Based Dependency Injection
  2. Circular Dependency in Spring Framework
  3. Spring Component Scan Example
  4. @FunctionalInterface Annotation in Java
  5. Java join() Method - Joining Strings
  6. What if run() Method Called Directly Instead of start() Method - Java Multi-Threading
  7. AtomicInteger in Java With Examples
  8. Marker Interface in Java