Monday, February 19, 2024

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

No comments:

Post a Comment