Microservice using spring boot

A hands on tutorial about spring and microservice

What is a microservice?

Microservices are a software architecture approach where an application is divided into small, independent and self-contained services that communicate with each other through APIs. This allows for faster development, deployment and scalability.

a microservices architecture built with Spring Boot. Here's a breakdown of its components:

  • Student and School: These represent two example microservices within the system. You can replace them with the functionalities of your application (e.g., ProductService, OrderService).

  • Docker: Docker containers are used to package and deploy the microservices. This promotes independent deployments and scalability.

  • API Gateway: Spring Cloud API Gateway serves as a single entry point for incoming API requests. It routes the requests to the appropriate microservice based on the path.

  • Eureka (Discovery Server): Eureka Server helps with service discovery. Microservices register themselves with Eureka, and the client application retrieves the service addresses to interact with them.

  • Config Server: This component is responsible for managing configuration details (like database connection strings) across all the microservices. Each microservice retrieves its configurations from the Config Server.

  • ZIPKIN: Zipkin is a distributed tracing system, helpful for monitoring and debugging the interactions between the various microservices within the architecture.

Overall, this architecture leverages Spring Boot to create separate microservices that communicate with each other via APIs. The API Gateway acts as a central point of entry, while Eureka Server facilitates service discovery. This distributed system is designed to be scalable and maintainable.

Config Server

Here's an explanation of configuring a Spring Boot Config Server with both Git and native search location approaches:

1. Config Server with Git:

This approach retrieves configurations from a remote Git repository.

  • Add Dependencies:

Include the following dependencies in your Config Server's pom.xml

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

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

add the @EnableConfigServer in the main class

  • Properties Configuration:

Set the following properties in your application.yml file:

spring:
  profiles:
    active: native
  cloud:
    config:
      server:
        git:
          uri: https://stackoverflow.com/questions/31801271/what-are-the-supported-git-url-formats  
          # Replace with your Git repository URL (e.g., https://github.com/user/config-repo.git)
          # Optional: Username and password for private repositories
          username: username
          password: password

2. Config Server with Native Search Locations:

This approach loads configurations from the local file system or classpath.

The required dependencies are included by default in Spring Cloud Config Server.

  • Properties Configuration:

Set the following property in your application.yml file:

spring:
  profiles:
    active: native
  application:
    name: config-server
  cloud:
    config:
      server:
        native:
          search-locations: classpath:/configs

Below is the folder structure. name should be exactly the same as application name. <application.name>.yml

Discovery Server:

as we set up our config server, so we need config client in our discovery server.

application.yml

spring:
  config:
    import: optional:configserver:http://localhost:8888
  application:
    name: discovery

as you can see config importing from config server. Now goto config server and update the discovery.yml.

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
  port: 8761

set @EnableEurekaServer in the main class

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {

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

Gateway

First we need these dependencies

now set the application.yml

spring:
  application:
    name: gateway
  config:
    import: optional:configserver:http://localhost:8888

here as you can see config are importing from config server. Now got to config server and update the gateway.yml

eureka:
  client:
    register-with-eureka: false
server:
  port: 8222
spring:
  application:
    name: gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: students
          uri: lb://students
          predicates:
            - Path=/api/v1/students/**
        - id: schools
          uri: lb://schools
          predicates:
            - Path=/api/v1/schools/**

Student/School services:

Now its time to create our student and school microservices

Dependencies we need:

properties.yml

spring:
  application:
    name: students
  config:
    import: optional:configserver:http://localhost:8888

now update the config server properties for students.

eureka:
  instance:
    hostname: localhost
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

server:
  port: 8090
spring:
  application:
    name: students
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/students
    username: admin
    password: admin
  jpa:
    hibernate:
      ddl-auto: create
    database: postgresql
    database-platform: org.hibernate.dialect.PostgreSQLDialect

using api calls using feign clinet

@EnableFeignClients
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(SchoolApplication.class, args);
    }
}
@FeignClient(name = "student-service", url = "${application.config.students-url}")
public interface StudentClient {

    @GetMapping("/school/{school-id}")
    List<Student> findAllStudentsBySchool(@PathVariable("school-id") Integer schoolId);
}

Zipkin:

Prerequisites:

  • Docker installed and running on your system.

  • Basic understanding of Spring Boot and microservices architecture.

1. Define Your Microservices:

  • Develop your Spring Boot microservices with functionalities like product management, order processing, etc.

2. Add Dependencies:

Include the following dependencies in your microservice pom.xml files:

XML

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-micrometer</artifactId>
</dependency>

3. Run Zipkin Server:

docker run -d -p 9411:9411 openzipkin/zipkin

4. Enable Micrometer Tracing (Optional):

In each microservice's application.yml file, you can configure Micrometer with a tracing implementation (e.g., Zipkin):

YAML

micrometer:
  tracing:
    sampling:
      probability: 1.0
    zipkin:
      endpoint: http://localhost:9411/api/v2/spans  
      # Replace with your Zipkin server URL

This configuration tells Micrometer to send tracing data to the Zipkin server running on http://localhost:9411.

5. Feign Client with Tracing:

  • Enable FeignClient on interfaces that interact with other microservices in your project.

  • The feign-micrometer dependency automatically integrates tracing with Feign clients.

6. Service Discovery (Optional):

If your microservices use service discovery (e.g., Eureka), ensure your clients discover the service addresses correctly.

7. Running the Microservices:

  • Build and package each microservice into a jar file.

  • Run each microservice using java -jar <your-application.jar>.

8. Accessing Zipkin UI:

  • Open localhost:9411 in your web browser to access the Zipkin server UI.

  • You should see traces being generated for your microservice interactions.

Additional Notes:

  • You can configure Brave for additional tracing features like sampling and logging.

  • Consider security aspects like authentication for accessing the Zipkin server in production environments.

Dockerize microservices

For on demand docker-compose file where we can only run the container for our local test.

services:
  postgresql:
    container_name: postgresql
    image: postgres
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: admin
      PGDATA: /data/postgres
    volumes:
      - postgres:/data/postgres
    ports:
      - "5432:5432"
    networks:
      - postgres
    restart: unless-stopped
  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
      PGADMIN_CONFIG_SERVER_MODE: 'False'
    volumes:
      - pgadmin:/var/lib/pgadmin
    ports:
      - "5050:80"
    networks:
      - postgres
    restart: unless-stopped
  zipkin:
    container_name: zipkin
    image: openzipkin/zipkin
    ports:
      - "9411:9411"
    networks:
      - zipkin

networks:
  postgres:
    driver: bridge
  zipkin:
    driver: bridge

volumes:
  postgres:
  pgadmin:

But If we want to docarize the full application you can follow below instructions.

Step 1: Dockerize Each Microservice

For each microservice, create a Dockerfile to package the application into a Docker image. Here's an example Dockerfile for a Spring Boot microservice:

FROM openjdk:17-jre-slim

WORKDIR /app

COPY build/libs/<your-microservice.jar> app.jar

EXPOSE <your-microservice-port>

ENTRYPOINT ["java", "-jar", "app.jar"]

Step 2: Create Docker Compose File

Create a docker-compose.yml file to define and run your services. Here's an example with three services (microservices, PostgreSQL, and Zipkin):

version: '3.8'

services:
  config-server:
    build: ./config-server # Replace with path to your Config Server Dockerfile
    ports:
      - "8888:8888"
    networks:
      - microservice-network
    environment:
      spring.profiles.active: native

  discovery-server:
    build: ./discovery-server # Replace with path to your Discovery Server Dockerfile
    ports:
      - "8761:8761"
    networks:
      - microservice-network
    depends_on:
      - config-server

  api-gateway:
    build: ./api-gateway # Replace with path to your API Gateway Dockerfile
    ports:
      - "8080:8080"
    networks:
      - microservice-network
    depends_on:
      - config-server
      - discovery-server

  microservice1:
    build: ./microservice1 # Replace with path to your microservice1 Dockerfile
    ports:
      - "8081:8080"
    networks:
      - microservice-network
    environment:
      SPRING_CLOUD_CONFIG_URI: http://config-server:8888
      SPRING_CLOUD_REGISTRY_HOST: discovery-server:8761
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres1:5432/your_database_name
      SPRING_DATASOURCE_USERNAME: your_username
      SPRING_DATASOURCE_PASSWORD: your_password  
    depends_on:
      - discovery-server
      - postgres1
    volumes:
      - postgres_data1:/var/lib/postgresql/data  # Optional, for persistent storage (modify based on your needs)

  microservice2:
    build: ./microservice2 # Replace with path to your microservice2 Dockerfile
    ports:
      - "8082:8080"
    networks:
      - microservice-network
    environment:
      SPRING_CLOUD_CONFIG_URI: http://config-server:8888
      SPRING_CLOUD_REGISTRY_HOST: discovery-server:8761
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres2:5432/your_database_name
      SPRING_DATASOURCE_USERNAME: your_username
      SPRING_DATASOURCE_PASSWORD: your_password  
    depends_on:
      - discovery-server
      - postgres2
    volumes:
      - postgres_data2:/var/lib/postgresql/data  # Optional, for persistent storage (modify based on your needs)

  postgres1:
    image: postgres:latest
    environment:
      POSTGRES_DB: your_database_name
      POSTGRES_USER: your_username
      POSTGRES_PASSWORD: your_password
    ports:
      - "5432:5432"
    networks:
      - microservice-network
    restart: unless-stopped

  postgres2:
    image: postgres:latest
    environment:
      POSTGRES_DB: your_database_name
      POSTGRES_USER: your_username
      POSTGRES_PASSWORD: your_password
    ports:
      - "5433:5432"
    networks:
      - microservice-network
    restart: unless-stopped

networks:
  microservice-network:
    driver: bridge

volumes:
  postgres_data1: {}  # Optional, empty driver definition for local storage
  postgres_data2: {}  # Optional, empty driver definition for local storage

Step 3: Build and Run Services

Run docker-compose up from the directory containing your docker-compose.yml file. Docker Compose will build the images and start the containers for each service.

Step 4: Access Microservices and Zipkin

Notes

  • Ensure that your microservices are configured to connect to the correct PostgreSQL databases.

  • Adjust the database URLs (SPRING_DATASOURCE_URL) and ports in the Docker Compose file as needed.

  • Ensure that your microservices are instrumented with Spring Cloud Sleuth to send tracing data to Zipkin.

Resilence 4j

Resilience4j functionalities like Circuit Breaker, Retry, Bulkhead, and RateLimiter in your Spring Boot project:

1. Add Dependency:

First, add the required dependency to your project's pom.xml file:

Need to add aop also

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>

2. Global Configuration (Optional):

While not mandatory, you can define global configurations for Resilience4j beans in your application.yml file:

YAML

resilience4j:
  circuitBreaker:
    instances:
      default:  # Default configuration name
        ringBufferSizeInHalfOpenState: 10  # Adjust as needed
        waitDurationInOpenState: 10s  # Adjust as needed
  retry:
    instances:
      default:  # Default configuration name
        maxAttempts: 3  # Adjust as needed
        waitDuration: 1s  # Adjust as needed
  bulkhead:
    instances:
      default:  # Default configuration name
        maxConcurrentCalls: 10  # Adjust as needed
  rateLimiter:  # Requires additional configuration
    instances:
      default:  # Default configuration name
        limitForPeriod: 100  # Adjust as needed
        limitRefreshPeriod: 1m  # Adjust as needed

These configurations will be applied to all Circuit Breaker, Retry, Bulkhead, and RateLimiter instances unless overridden at the bean level.

3. Circuit Breaker Pattern:

The Circuit Breaker pattern protects your application from cascading failures by:

  • Monitoring Call Success/Failure: Tracks the success/failure ratio of calls to an external service.

  • Open/Closed/Half-Open States: The Circuit Breaker can be in Open (preventing calls), Closed (allowing calls), or Half-Open (allowing limited calls) state based on failure rates.

  • Configuration: You can define retry attempts, wait duration in open state, etc.

Here's an example:

Java

@Service
public class MyService {

    @CircuitBreaker(name = "myService", fallbackMethod = "fallback")
    public String callExternalService() {
        // Your logic to call the external service
        return "Success";
    }

    private String fallback(Exception ex) {
        // Handle fallback logic when Circuit Breaker trips
        return "Fallback triggered due to external service issue!";
    }
}

In this example:

  • @CircuitBreaker annotation on the callExternalService method enables circuit breaker functionality.

  • name attribute defines the circuit breaker instance name.

  • fallbackMethod specifies the fallback method to be called when the circuit breaker trips.

4. Retry Pattern:

The Retry pattern attempts a failing operation a certain number of times before giving up:

  • Configuration: You can define the number of retry attempts, wait duration between retries, etc.

Here's an example:

Java

@Service
public class MyService {

    @Retry(name = "retryService", fallbackMethod = "fallbackOnRetry")
    public String callExternalService() {
        // Your logic to call the external service
        return "Success";
    }

    private String fallbackOnRetry(Exception ex) {
        // Handle fallback logic after retries are exhausted
        return "Failed to call external service even after retries!";
    }
}

Similar to the Circuit Breaker example:

  • @Retry annotation activates retry functionality for the callExternalService method.

  • name attribute defines the retry instance name.

  • fallbackMethod specifies the fallback method to be called if all retries fail.

5. Bulkhead Pattern:

The Bulkhead pattern prevents overloading a service by limiting the number of concurrent calls it can handle:

Here's an example:

Java

@Service
public class MyService {

    @Bulkhead(name = "myServiceBulkhead", maxConcurrentCalls = 10)  // Adjust as needed
    public String processData(String data) {
        // Your logic to process the data
        return "Data processed successfully!";
    }
}

In this example:

  • @Bulkhead annotation activates the Bulkhead pattern for the processData method.

  • name attribute defines the Bulkhead instance name.

  • maxConcurrentCalls property specifies the maximum number of concurrent calls allowed.

Actuator Endpoints for Resilience4j

Resilience4j leverages Spring Boot Actuator to expose endpoints for monitoring the state of its functionalities (Circuit Breaker, Retry, etc.). Here's a breakdown of the relevant endpoints and how to enable them:

Available Endpoints:

  • CircuitBreaker:

    • /actuator/health: Provides overall health status of Circuit Breakers, including open/closed state and details about individual instances.

    • /actuator/circuitbreakers: Offers detailed information about each Circuit Breaker instance, including call attempts, success/failure rates, and current state.

  • Retry:

    • /actuator/metrics: Exposes metrics related to retries, such as the number of successful/failed attempts and retry durations. (Requires additional configuration)
  • Bulkhead: (Limited endpoint availability)

    • /actuator/health: May include information about Bulkhead state as part of the overall health check. (Subject to change based on Resilience4j version)

Enabling Endpoints:

1. Enable Management Endpoint:

By default, Spring Boot Actuator endpoints are disabled for security reasons. To enable them, you need to add the following dependency to your pom.xml file:

XML

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2. Optional Configuration (Metrics):

For detailed retry metrics, you might need to configure Micrometer and a metrics reporter in your application.yml file:

YAML

management:
  metrics:
    enabled: true
  endpoints:
    web:
      exposure:
        include: "*"  # Expose all endpoints (or specify individual endpoints)

RabbitMQ

1. Project Setup:

  • Ensure you have two Spring Boot microservices developed and ready for communication.

  • Download and install RabbitMQ server according to your operating system (instructions available on https://www.rabbitmq.com/docs/download).

2. Add Dependencies:

Add the following dependency to the pom.xml file of both your microservices:

XML

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

3. RabbitMQ Configuration (Optional):

If you're not using the default RabbitMQ instance running on localhost:5672, you can configure connection details in your microservices' application.yml files:

Producer Service:

YAML

spring:
  rabbitmq:
    host: <your-rabbitmq-host>  # Replace with your RabbitMQ server hostname/IP
    port: <your-rabbitmq-port>  # Replace with your RabbitMQ server port (default: 5672)

Consumer Service: (Same configuration as producer service)

4. Producer Service:

  • Define a @RabbitListener annotated method to listen for messages from a specific queue.

  • Use the @SendTo annotation to specify the queue to send messages to.

  • Utilize the RabbitTemplate bean to send messages to the RabbitMQ broker.

Here's an example:

Java

@Service
public class ProducerService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @SendTo("myQueue")  // Queue to send messages to
    public String sendMessage(String message) {
        rabbitTemplate.convertAndSend("myExchange", "", message);  // Exchange and routing key (optional)
        return "Message sent: " + message;
    }
}

Explanation:

  • @RabbitListener is not used in the producer service in this example.

  • @SendTo("myQueue") defines the target queue for messages sent using the rabbitTemplate.

  • rabbitTemplate.convertAndSend sends a message to the specified exchange ("" for default exchange) with an optional routing key.

5. Consumer Service:

  • Define a @RabbitListener annotated method to handle incoming messages from a specific queue.

Here's an example:

Java

@Service
public class ConsumerService {

    @RabbitListener(queues = "myQueue")  // Queue to listen on
    public void processMessage(String message) {
        System.out.println("Received message: " + message);
        // Your logic to process the message
    }
}

Explanation:

  • @RabbitListener(queues = "myQueue") specifies that this method listens for messages from the "myQueue" queue.

  • The method receives the message as a string and performs the desired processing logic.

6. Running the Services:

  1. Start your RabbitMQ server.

  2. Build and run each microservice using mvn spring-boot:run or a similar command.

https://dip-mazumder.medium.com/spring-boot-microservice-use-cases-messaging-with-rabbitmq-b7e99f02bf13

SAGA Pattern

https://www.youtube.com/watch?v=3gSkhhic4sM&ab_channel=JavaPuzzle

https://www.youtube.com/watch?v=WGI_ciUa3FE&ab_channel=JavaTechSolutions