You are currently viewing Building a Robust Microservices Application Using JPA, MongoDB, Redis, RabbitMQ, Swagger, Actuator, Spring Security, OAuth2, Reactive Programming, Spring Cloud Stream, Vault, Docker, Kubernetes, Prometheus, Grafana, and the ELK Stack

Building a Robust Microservices Application Using JPA, MongoDB, Redis, RabbitMQ, Swagger, Actuator, Spring Security, OAuth2, Reactive Programming, Spring Cloud Stream, Vault, Docker, Kubernetes, Prometheus, Grafana, and the ELK Stack

Introduction

Microservices architecture is a modern approach to building scalable, maintainable, and resilient applications by breaking down a large monolithic system into smaller, loosely coupled services. Each service in a microservices architecture is responsible for a specific business capability and can be developed, deployed, and scaled independently. This architectural style is well-suited to the demands of today’s complex and dynamic software environments.

In this comprehensive tutorial, we will build a microservices-based application using a variety of cutting-edge technologies and frameworks. The application will consist of three services: Product Service, Inventory Service, and Notification Service. We will explore the following technologies and concepts:

  1. JPA (Java Persistence API): For relational data persistence.
  2. MongoDB: For non-relational data storage.
  3. Redis: For caching to enhance performance.
  4. RabbitMQ: For asynchronous message-driven communication.
  5. Swagger: For API documentation.
  6. Actuator: For monitoring and management endpoints.
  7. Spring Security and OAuth2: For securing the services.
  8. Reactive Programming with Spring WebFlux: For handling asynchronous data streams.
  9. Spring Cloud Stream: For message-driven microservices.
  10. Vault: For secure management of sensitive configuration properties.
  11. Docker: For containerizing the microservices.
  12. Kubernetes: For orchestrating and managing the deployment of containerized applications.
  13. Prometheus and Grafana: For monitoring the services.
  14. ELK Stack (Elasticsearch, Logstash, Kibana): For logging and log analysis.
  15. Testcontainers: For integration testing with disposable Docker containers.

Throughout this tutorial, we will follow best practices and design patterns to ensure that our microservices are robust, scalable, and maintainable. By the end of this tutorial, you will have a solid understanding of how to build, test, and deploy a microservices-based application using these technologies.

Let’s get started by setting up our project and exploring the overall architecture.

Step 1: Project Setup and Configuration

1. Setting up the Project Structure

Create a new Maven project or Gradle project in your preferred IDE. You can name it “ECommerceMicroservicesTutorial”.

2. Configuring Spring Boot and Dependencies

Add the necessary dependencies for Spring Boot, MongoDB, RabbitMQ, Spring Security, OAuth2, Spring Data JPA, and other technologies mentioned in the list to your project’s pom.xml (if using Maven) or build.gradle (if using Gradle).

Here’s an example pom.xml snippet:

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Data MongoDB -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <!-- RabbitMQ -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <!-- Spring Security OAuth2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <!-- Other dependencies... -->
</dependencies>

3. Create Microservice Modules

Create separate modules for each microservice: Product Service, Order Service, and User Service. This allows for better organization and separation of concerns.

5. Initialize Git Repository

Initialize a Git repository for version control and tracking changes throughout the tutorial.

ECommerceMicroservicesTutorial
│
├───product-service
│ ├───src
│ │ ├───main
│ │ │ ├───java
│ │ │ │ └───com
│ │ │ │ └───example
│ │ │ │ └───productservice
│ │ │ │ ├───controller
│ │ │ │ ├───model
│ │ │ │ ├───repository
│ │ │ │ └───service
│ │ │ └───resources
│ │ └───test
│ │ └───java
│ └───pom.xml
│
├───order-service
│ ├───src
│ │ ├───main
│ │ │ ├───java
│ │ │ │ └───com
│ │ │ │ └───example
│ │ │ │ └───orderservice
│ │ │ │ ├───controller
│ │ │ │ ├───model
│ │ │ │ ├───repository
│ │ │ │ └───service
│ │ │ └───resources
│ │ └───test
│ │ └───java
│ └───pom.xml
│
├───user-service
│ ├───src
│ │ ├───main
│ │ │ ├───java
│ │ │ │ └───com
│ │ │ │ └───example
│ │ │ │ └───userservice
│ │ │ │ ├───config
│ │ │ │ ├───controller
│ │ │ │ ├───model
│ │ │ │ ├───repository
│ │ │ │ └───service
│ │ │ └───resources
│ │ └───test
│ │ └───java
│ └───pom.xml
│
└───pom.xml

Product Service

Project Structure and Hexagonal Architecture

In the Product Service module, we follow the principles of hexagonal architecture to achieve a clean separation of concerns and maintain modularity.

product-service/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   ├── com/
│   │   │   │   ├── example/
│   │   │   │   │   ├── ProductServiceApplication.java       (Main Application Class)
│   │   │   │   │   ├── controller/                          (REST Controllers)
│   │   │   │   │   ├── model/                               (Domain Models)
│   │   │   │   │   ├── repository/                          (JPA Repositories)
│   │   │   │   │   ├── service/                             (Service Layer)
│   │   │   │   │   └── config/                              (Configuration Classes)
│   │   │   ├── resources/
│   │   │   │   ├── application.properties                   (Application Configuration)
│   │   │   │   └── static/                                   (Static Resources)
│   │   └── test/
│   │       └── java/
│   │           └── com/
│   │               └── example/
│   │                   └── controller/                       (Controller Tests)
└── pom.xml                                                   (Maven Project Configuration)

JPA for Data Persistence

We utilize Java Persistence API (JPA) for data persistence, allowing us to interact with the underlying database using object-relational mapping (ORM) techniques.

// Product Entity Class
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(max = 100)
    private String name;

    @NotBlank
    @Size(max = 255)
    private String description;

    @DecimalMin("0.01")
    private BigDecimal price;

    // Getters and Setters
}
// Product Repository Interface
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // Custom query methods can be defined here
}

@Service
public class ProductService {
    @Autowired
    private ProductRepository repository;

    @Cacheable("products")
    public List<Product> getAllProducts() {
        return repository.findAll();
    }

    public Product getProductById(Long productId) {
        return repository.findById(productId)
                         .orElseThrow(() -> new EntityNotFoundException("Product not found with id: " + productId));
    }

    @Transactional
    public Product createProduct(Product product) {
        // Additional business logic or validation can be applied here
        return repository.save(product);
    }

    @Transactional
    public Product updateProduct(Long productId, Product updatedProduct) {
        Product existingProduct = getProductById(productId);
        // Update existingProduct with fields from updatedProduct
        // Validation and business logic can be applied before updating
        return repository.save(existingProduct);
    }

    @Transactional
    public void deleteProduct(Long productId) {
        Product existingProduct = getProductById(productId);
        // Additional checks or business logic can be applied before deletion
        repository.delete(existingProduct);
    }

    // Other service methods like search, filtering, etc.
}

These methods handle common CRUD operations and other related functionalities within the ProductService.

RabbitMQ for Asynchronous Communication

We integrate RabbitMQ to enable asynchronous communication for processing product-related tasks in a decoupled manner.

// RabbitMQ Configuration
@Configuration
public class RabbitMQConfig {
    public static final String QUEUE_NAME = "product-queue";

    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME, false);
    }

    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}
// RabbitMQ Producer
@Component
public class ProductEventProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendProductEvent(Product product, String action) {
        ProductEvent event = new ProductEvent(product, action);
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, event);
    }
}
// RabbitMQ Consumer
@Component
public class ProductEventConsumer {
    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void receiveProductEvent(ProductEvent event) {
        // Process the product event asynchronously
        // Example: Log the event, trigger further processing, etc.
        System.out.println("Received product event: " + event);
    }
}

With RabbitMQ integration, the ProductService can publish product events to RabbitMQ queues for asynchronous processing by consumers. This decouples components and improves scalability and responsiveness.

OpenAPI Documentation with Swagger

We incorporate Swagger to generate interactive API documentation for the Product Service, making it easier for developers to understand and test the available endpoints.

// Swagger Configuration
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}

Once Swagger is configured, you can access the Swagger UI by navigating to http://localhost:8080/swagger-ui.html in your web browser. This UI provides a visual representation of the API endpoints and allows users to interactively explore and test them.

Actuator for Monitoring and Managing

We integrate Spring Boot Actuator to provide monitoring and management endpoints for the Product Service, enabling insights into its runtime behavior and health.

// Actuator Configuration
@Configuration
public class ActuatorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/actuator/**").addResourceLocations("classpath:/META-INF/spring-boot-admin-server/");
    }
}

With Actuator configured, you can access various endpoints to monitor the health, metrics, and other runtime information of the Product Service. For example:

  • /actuator/health: Provides information about the health of the application.
  • /actuator/metrics: Exposes metrics about the application’s behavior.
  • /actuator/env: Displays the current environment properties.

These endpoints can be accessed programmatically or via tools like Spring Boot Admin for centralized monitoring and management.

Inventory Service

Project Structure and Hexagonal Architecture

In the Inventory Service module, we follow the principles of hexagonal architecture to achieve a clean separation of concerns and maintain modularity.

inventory-service/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   ├── com/
│   │   │   │   ├── example/
│   │   │   │   │   ├── InventoryServiceApplication.java       (Main Application Class)
│   │   │   │   │   ├── controller/                            (REST Controllers)
│   │   │   │   │   ├── model/                                 (Domain Models)
│   │   │   │   │   ├── repository/                            (JPA Repositories)
│   │   │   │   │   ├── service/                               (Service Layer)
│   │   │   │   │   └── config/                                (Configuration Classes)
│   │   │   ├── resources/
│   │   │   │   ├── application.properties                     (Application Configuration)
│   │   │   │   └── static/                                     (Static Resources)
│   │   └── test/
│   │       └── java/
│   │           └── com/
│   │               └── example/
│   │                   └── controller/                         (Controller Tests)
└── pom.xml                                                     (Maven Project Configuration)

JPA for Data Persistence

We utilize Java Persistence API (JPA) for data persistence, allowing us to interact with the underlying database using object-relational mapping (ORM) techniques.

// Inventory Item Entity Class
@Entity
public class InventoryItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String productName;
    private int quantity;

    // Getters and Setters
}
// Inventory Item Repository Interface
@Repository
public interface InventoryItemRepository extends JpaRepository<InventoryItem, Long> {
    // Custom query methods can be defined here
}

MongoDB Integration

For MongoDB integration, we’ll use Spring Data MongoDB to interact with the MongoDB database seamlessly.

// MongoDB Configuration
@Configuration
@EnableMongoRepositories(basePackages = "com.example.inventory.repository")
public class MongoConfig extends AbstractMongoClientConfiguration {
    @Override
    protected String getDatabaseName() {
        return "inventory-db";
    }

    @Override
    public MongoClient mongoClient() {
        return MongoClients.create("mongodb://localhost:27017");
    }
}
// Inventory Item Repository Interface (MongoDB)
@Repository
public interface InventoryItemRepositoryMongo extends MongoRepository<InventoryItem, String> {
    // Custom query methods can be defined here
}

With MongoDB integration, the Inventory Service can seamlessly interact with MongoDB for storing and retrieving inventory data.

Redis for Caching

To improve performance, we integrate Redis for caching frequently accessed inventory data.

// Redis Configuration
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.builder(redisConnectionFactory).build();
    }
}
// Inventory Service with Caching
@Service
public class InventoryService {
    @Autowired
    private InventoryItemRepository repository;

    @Cacheable("inventory")
    public List<InventoryItem> getAllInventoryItems() {
        return repository.findAll();
    }

    // Other service methods
}

In this setup, inventory data retrieved from the database is cached using Redis. Subsequent requests for the same data can be served from the cache, improving response times and reducing database load.

RabbitMQ for Asynchronous Communication

We integrate RabbitMQ to enable asynchronous communication for processing inventory-related tasks in a decoupled manner.

// RabbitMQ Configuration
@Configuration
public class RabbitMQConfig {
    public static final String QUEUE_NAME = "inventory-queue";

    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME, false);
    }

    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}
// RabbitMQ Producer
@Component
public class InventoryEventProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendInventoryEvent(InventoryItem item, String action) {
        InventoryEvent event = new InventoryEvent(item, action);
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, event);
    }
}
// RabbitMQ Consumer
@Component
public class InventoryEventConsumer {
    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void receiveInventoryEvent(InventoryEvent event) {
        // Process the inventory event asynchronously
        // Example: Log the event, trigger further processing, etc.
        System.out.println("Received inventory event: " + event);
    }
}

With RabbitMQ integration, the Inventory Service can publish inventory events to RabbitMQ queues for asynchronous processing by consumers. This decouples components and improves scalability and responsiveness.

OpenAPI Documentation with Swagger

We incorporate Swagger to generate interactive API documentation for the Inventory Service, making it easier for developers to understand and test the available endpoints.

// Swagger Configuration
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.inventory.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}

Once Swagger is configured, you can access the Swagger UI by navigating to http://localhost:8080/swagger-ui.html in your web browser. This UI provides a visual representation of the API endpoints and allows users to interactively explore and test them.

With Swagger integration, developers can easily understand the functionality of the Inventory Service and test its endpoints without the need for external documentation.

Actuator for Monitoring and Managing

We integrate Spring Boot Actuator to provide monitoring and management endpoints for the Inventory Service, enabling insights into its runtime behavior and health.

// Actuator Configuration
@Configuration
public class ActuatorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/actuator/**").addResourceLocations("classpath:/META-INF/spring-boot-admin-server/");
    }
}

With Actuator configured, you can access various endpoints to monitor the health, metrics, and other runtime information of the Inventory Service. For example:

  • /actuator/health: Provides information about the health of the application.
  • /actuator/metrics: Exposes metrics about the application’s behavior.
  • /actuator/env: Displays the current environment properties.

These endpoints can be accessed programmatically or via tools like Spring Boot Admin for centralized monitoring and management.

Notification Service

Project Structure and Hexagonal Architecture

In the Notification Service module, we follow the principles of hexagonal architecture to achieve a clean separation of concerns and maintain modularity.

notification-service/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   ├── com/
│   │   │   │   ├── example/
│   │   │   │   │   ├── NotificationServiceApplication.java       (Main Application Class)
│   │   │   │   │   ├── controller/                               (REST Controllers)
│   │   │   │   │   ├── model/                                    (Domain Models)
│   │   │   │   │   ├── repository/                               (JPA Repositories)
│   │   │   │   │   ├── service/                                  (Service Layer)
│   │   │   │   │   └── config/                                   (Configuration Classes)
│   │   │   ├── resources/
│   │   │   │   ├── application.properties                        (Application Configuration)
│   │   │   │   └── static/                                        (Static Resources)
│   │   └── test/
│   │       └── java/
│   │           └── com/
│   │               └── example/
│   │                   └── controller/                            (Controller Tests)
└── pom.xml                                                        (Maven Project Configuration)

JPA for Data Persistence

We utilize Java Persistence API (JPA) for data persistence, allowing us to interact with the underlying database using object-relational mapping (ORM) techniques.

// Notification Entity Class
@Entity
public class Notification {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(max = 255)
    private String message;

    // Getters and Setters
}
// Notification Repository Interface
@Repository
public interface NotificationRepository extends JpaRepository<Notification, Long> {
    // Custom query methods can be defined here
}

MongoDB Integration

For MongoDB integration, we’ll use Spring Data MongoDB to interact with the MongoDB database seamlessly.

// MongoDB Configuration
@Configuration
@EnableMongoRepositories(basePackages = "com.example.repository")
public class MongoConfig extends AbstractMongoClientConfiguration {
    @Override
    protected String getDatabaseName() {
        return "notification-db";
    }

    @Override
    public MongoClient mongoClient() {
        return MongoClients.create("mongodb://localhost:27017");
    }
}
// Notification Repository Interface (MongoDB)
@Repository
public interface NotificationRepositoryMongo extends MongoRepository<Notification, String> {
    // Custom query methods can be defined here
}

Redis for Caching

To improve performance, we integrate Redis for caching frequently accessed notification data.

// Redis Configuration
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.builder(redisConnectionFactory).build();
    }
}
// Notification Service with Caching
@Service
public class NotificationService {
    @Autowired
    private NotificationRepository repository;

    @Cacheable("notifications")
    public List<Notification> getAllNotifications() {
        return repository.findAll();
    }

    // Other service methods
}

With MongoDB integration and Redis caching, the Notification Service is capable of storing and retrieving notification data efficiently.

RabbitMQ for Asynchronous Communication

We integrate RabbitMQ to enable asynchronous communication for processing notification-related tasks in a decoupled manner.

// RabbitMQ Configuration
@Configuration
public class RabbitMQConfig {
    public static final String QUEUE_NAME = "notification-queue";

    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME, false);
    }

    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}
// RabbitMQ Producer
@Component
public class NotificationEventProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendNotificationEvent(Notification notification, String action) {
        NotificationEvent event = new NotificationEvent(notification, action);
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, event);
    }
}
// RabbitMQ Consumer
@Component
public class NotificationEventConsumer {
    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void receiveNotificationEvent(NotificationEvent event) {
        // Process the notification event asynchronously
        // Example: Send notification via email, SMS, push notification, etc.
        System.out.println("Received notification event: " + event);
    }
}

With RabbitMQ integration, the Notification Service can publish notification events to RabbitMQ queues for asynchronous processing by consumers. This decouples components and improves scalability and responsiveness.

Product Service: CRUD Operations and Bean Validation

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/products")
@Validated
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{productId}")
    public ResponseEntity<Product> getProductById(@PathVariable("productId") Long productId) {
        Product product = productService.getProductById(productId);
        return ResponseEntity.ok(product);
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        Product createdProduct = productService.createProduct(product);
        return ResponseEntity.ok(createdProduct);
    }

    @PutMapping("/{productId}")
    public ResponseEntity<Product> updateProduct(@PathVariable("productId") Long productId,
                                                 @Valid @RequestBody Product updatedProduct) {
        Product product = productService.updateProduct(productId, updatedProduct);
        return ResponseEntity.ok(product);
    }

    @DeleteMapping("/{productId}")
    public ResponseEntity<Void> deleteProduct(@PathVariable("productId") Long productId) {
        productService.deleteProduct(productId);
        return ResponseEntity.noContent().build();
    }
}

This controller exposes endpoints for CRUD operations on products, with input data validated using Bean Validation annotations.

Inventory Service: Reactive Programming and Spring Batch

Reactive Programming allows us to handle streams of data asynchronously and with non-blocking behavior. Spring WebFlux, which is built on top of Project Reactor, provides support for reactive programming in Spring applications.

Reactive Service Method

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class InventoryService {

    @Autowired
    private InventoryRepository inventoryRepository;

    public Flux<InventoryItem> getAllInventoryItems() {
        return inventoryRepository.findAll();
    }

    // Other reactive service methods
}

Spring Batch Integration

Spring Batch is a framework for batch processing that provides reusable components and patterns to simplify the creation of batch jobs.

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class BatchConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Autowired
    private InventoryItemProcessor inventoryItemProcessor;

    @Autowired
    private InventoryItemWriter inventoryItemWriter;

    @Bean
    public Job inventoryUpdateJob() {
        return jobBuilderFactory.get("inventoryUpdateJob")
                                .start(inventoryUpdateStep())
                                .build();
    }

    @Bean
    public Step inventoryUpdateStep() {
        return stepBuilderFactory.get("inventoryUpdateStep")
                                 .<InventoryItem, InventoryItem>chunk(10)
                                 .reader(inventoryItemReader())
                                 .processor(inventoryItemProcessor)
                                 .writer(inventoryItemWriter)
                                 .build();
    }

    @Bean
    public InventoryItemReader inventoryItemReader() {
        // Define your item reader implementation
        // Example: return new InventoryItemReader();
    }

    // Other batch-related beans
}

This configuration sets up a simple Spring Batch job for updating inventory items.

Notification Service: Message-Driven Communication with Spring Cloud Stream and Elasticsearch Integration

Spring Cloud Stream Setup

Spring Cloud Stream provides a framework for building highly scalable event-driven microservices connected by messaging systems. We’ll use it to enable message-driven communication in the Notification Service.

import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;

@EnableBinding(Sink.class)
public class MessageConsumer {
    @StreamListener(Sink.INPUT)
    public void receive(String message) {
        // Process the received message
    }
}

This sets up a message consumer that listens to messages from a binding named “input” (default channel).

Elasticsearch Integration

Elasticsearch is a powerful search and analytics engine. We’ll integrate it into our Notification Service to store and search notification events.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {
    @Autowired
    private ElasticsearchOperations elasticsearchOperations;

    public void saveNotificationEvent(NotificationEvent event) {
        elasticsearchOperations.save(event);
    }
}

This service method saves a notification event to Elasticsearch.

Message Producer

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

@Component
public class MessageProducer {
    @Autowired
    private Source source;

    public void sendNotificationEvent(NotificationEvent event) {
        source.output().send(MessageBuilder.withPayload(event).build());
    }
}

This component sends notification events to the messaging system using Spring Cloud Stream.

Implementing Spring Security and OAuth2 for the three services

Product Service:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/products/**").authenticated()
                .anyRequest().permitAll()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

Inventory Service:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/inventory/**").authenticated()
                .anyRequest().permitAll()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

Notification Service:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/notifications/**").authenticated()
                .anyRequest().permitAll()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

These configurations ensure that requests to endpoints specific to each service require authentication using OAuth2 JWT tokens. Adjust the URL patterns and security rules as needed for your application.

To secure the controllers in each service, you can use method-level security annotations provided by Spring Security. These annotations allow you to specify which roles are required to access particular controller methods.

Product Service:

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{productId}")
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<Product> getProductById(@PathVariable("productId") Long productId) {
        Product product = productService.getProductById(productId);
        return ResponseEntity.ok(product);
    }

    @PostMapping
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        Product createdProduct = productService.createProduct(product);
        return ResponseEntity.ok(createdProduct);
    }

    // Other controller methods
}

Inventory Service:

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private InventoryService inventoryService;

    @GetMapping("/{itemId}")
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<InventoryItem> getInventoryItemById(@PathVariable("itemId") Long itemId) {
        InventoryItem item = inventoryService.getInventoryItemById(itemId);
        return ResponseEntity.ok(item);
    }

    @PostMapping
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public ResponseEntity<InventoryItem> createInventoryItem(@Valid @RequestBody InventoryItem item) {
        InventoryItem createdItem = inventoryService.createInventoryItem(item);
        return ResponseEntity.ok(createdItem);
    }

    // Other controller methods
}

Notification Service:

@RestController
@RequestMapping("/notifications")
public class NotificationController {

    @Autowired
    private NotificationService notificationService;

    @GetMapping("/{notificationId}")
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<Notification> getNotificationById(@PathVariable("notificationId") Long notificationId) {
        Notification notification = notificationService.getNotificationById(notificationId);
        return ResponseEntity.ok(notification);
    }

    @PostMapping
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public ResponseEntity<Notification> createNotification(@Valid @RequestBody Notification notification) {
        Notification createdNotification = notificationService.createNotification(notification);
        return ResponseEntity.ok(createdNotification);
    }

    // Other controller methods
}

In these examples, we use @PreAuthorize annotations to specify that certain roles are required to access each controller method. Adjust the roles and access rules as needed for your application.

Product Service:

@RunWith(MockitoJUnitRunner.class)
public class ProductControllerTest {

    @Mock
    private ProductService productService;

    @InjectMocks
    private ProductController productController;

    @Test
    public void testGetProductById() {
        Long productId = 1L;
        Product product = new Product();
        product.setId(productId);
        product.setName("Test Product");
        product.setDescription("Test Description");
        product.setPrice(BigDecimal.TEN);

        Mockito.when(productService.getProductById(productId)).thenReturn(product);

        ResponseEntity<Product> responseEntity = productController.getProductById(productId);

        Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        Assert.assertEquals(productId, responseEntity.getBody().getId());
    }

    @Test
    public void testCreateProduct() {
        Product product = new Product();
        product.setName("Test Product");
        product.setDescription("Test Description");
        product.setPrice(BigDecimal.TEN);

        Mockito.when(productService.createProduct(Mockito.any(Product.class))).thenReturn(product);

        ResponseEntity<Product> responseEntity = productController.createProduct(product);

        Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        Assert.assertNotNull(responseEntity.getBody());
        Assert.assertEquals("Test Product", responseEntity.getBody().getName());
    }

    // Other test methods
}

Inventory Service:

@RunWith(MockitoJUnitRunner.class)
public class InventoryControllerTest {

    @Mock
    private InventoryService inventoryService;

    @InjectMocks
    private InventoryController inventoryController;

    @Test
    public void testGetInventoryItemById() {
        Long itemId = 1L;
        InventoryItem item = new InventoryItem();
        item.setId(itemId);
        item.setName("Test Item");
        item.setQuantity(10);

        Mockito.when(inventoryService.getInventoryItemById(itemId)).thenReturn(item);

        ResponseEntity<InventoryItem> responseEntity = inventoryController.getInventoryItemById(itemId);

        Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        Assert.assertEquals(itemId, responseEntity.getBody().getId());
    }

    @Test
    public void testCreateInventoryItem() {
        InventoryItem item = new InventoryItem();
        item.setName("Test Item");
        item.setQuantity(10);

        Mockito.when(inventoryService.createInventoryItem(Mockito.any(InventoryItem.class))).thenReturn(item);

        ResponseEntity<InventoryItem> responseEntity = inventoryController.createInventoryItem(item);

        Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        Assert.assertNotNull(responseEntity.getBody());
        Assert.assertEquals("Test Item", responseEntity.getBody().getName());
    }

    // Other test methods
}

Notification Service:

@RunWith(MockitoJUnitRunner.class)
public class NotificationControllerTest {

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private NotificationController notificationController;

    @Test
    public void testGetNotificationById() {
        Long notificationId = 1L;
        Notification notification = new Notification();
        notification.setId(notificationId);
        notification.setMessage("Test Notification");

        Mockito.when(notificationService.getNotificationById(notificationId)).thenReturn(notification);

        ResponseEntity<Notification> responseEntity = notificationController.getNotificationById(notificationId);

        Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        Assert.assertEquals(notificationId, responseEntity.getBody().getId());
    }

    @Test
    public void testCreateNotification() {
        Notification notification = new Notification();
        notification.setMessage("Test Notification");

        Mockito.when(notificationService.createNotification(Mockito.any(Notification.class))).thenReturn(notification);

        ResponseEntity<Notification> responseEntity = notificationController.createNotification(notification);

        Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        Assert.assertNotNull(responseEntity.getBody());
        Assert.assertEquals("Test Notification", responseEntity.getBody().getMessage());
    }

    // Other test methods
}

In these examples, we use Mockito to mock the service layer dependencies and test the controller methods in isolation. JUnit is used for writing the test cases and asserting the expected behavior of the controller methods.

Containerization with Docker

Docker provides a platform for developing, shipping, and running applications using containerization. Here’s how you can containerize your microservices:

Dockerfile for Product Service

# Use an OpenJDK runtime as base image
FROM openjdk:11-jre-slim

# Set the working directory in the container
WORKDIR /app

# Copy the packaged JAR file into the container at defined directory
COPY target/product-service.jar /app

# Expose the port the application runs on
EXPOSE 8080

# Define the command to run the application when the container starts
CMD ["java", "-jar", "product-service.jar"]

Dockerfile for Inventory Service

# Use an OpenJDK runtime as base image
FROM openjdk:11-jre-slim

# Set the working directory in the container
WORKDIR /app

# Copy the packaged JAR file into the container at defined directory
COPY target/inventory-service.jar /app

# Expose the port the application runs on
EXPOSE 8080

# Define the command to run the application when the container starts
CMD ["java", "-jar", "inventory-service.jar"]

Dockerfile for Notification Service

# Use an OpenJDK runtime as base image
FROM openjdk:11-jre-slim

# Set the working directory in the container
WORKDIR /app

# Copy the packaged JAR file into the container at defined directory
COPY target/notification-service.jar /app

# Expose the port the application runs on
EXPOSE 8080

# Define the command to run the application when the container starts
CMD ["java", "-jar", "notification-service.jar"]

Configuration Management with Vault

Vault is a tool for securely managing secrets and sensitive data. You can use it to manage configuration properties for your microservices. Here’s how you can integrate Vault into your setup:

Vault Configuration for Product Service

spring:
  profiles: development
  cloud:
    vault:
      host: localhost
      port: 8200
      scheme: http
      token: your-vault-token
      kv:
        enabled: true
        backend: secret
        default-context: application

Vault Configuration for Inventory Service

spring:
  profiles: development
  cloud:
    vault:
      host: localhost
      port: 8200
      scheme: http
      token: your-vault-token
      kv:
        enabled: true
        backend: secret
        default-context: application

Vault Configuration for Notification Service

spring:
  profiles: development
  cloud:
    vault:
      host: localhost
      port: 8200
      scheme: http
      token: your-vault-token
      kv:
        enabled: true
        backend: secret
        default-context: application

In these configurations, Vault is used to fetch sensitive configuration properties like database credentials, API keys, etc., which are then used by the microservices.

Deployment on Kubernetes or Docker Swarm

Both Kubernetes and Docker Swarm are popular container orchestration platforms that allow you to deploy, manage, and scale containerized applications. Here’s how you can deploy your microservices using Kubernetes:

Kubernetes Deployment Configuration for Product Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: your-docker-registry/product-service:latest
        ports:
        - containerPort: 8080

Kubernetes Deployment Configuration for Inventory Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inventory-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: inventory-service
  template:
    metadata:
      labels:
        app: inventory-service
    spec:
      containers:
      - name: inventory-service
        image: your-docker-registry/inventory-service:latest
        ports:
        - containerPort: 8080

Kubernetes Deployment Configuration for Notification Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: notification-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: notification-service
  template:
    metadata:
      labels:
        app: notification-service
    spec:
      containers:
      - name: notification-service
        image: your-docker-registry/notification-service:latest
        ports:
        - containerPort: 8080

In these configurations, Kubernetes Deployment objects are used to define and manage the deployment of each microservice. Adjust the number of replicas, image names, and other parameters as per your requirements.

Monitoring with Prometheus, Grafana, and ELK Stack

Monitoring and logging are critical for maintaining the health and performance of your microservices. Prometheus and Grafana are popular tools for monitoring, while the ELK stack (Elasticsearch, Logstash, Kibana) is widely used for logging and analysis.

Prometheus Setup

Prometheus is an open-source systems monitoring and alerting toolkit. You can configure Prometheus to scrape metrics from your microservices.

Prometheus Configuration

Create a prometheus.yml file:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'product-service'
    static_configs:
      - targets: ['product-service:8080']

  - job_name: 'inventory-service'
    static_configs:
      - targets: ['inventory-service:8080']

  - job_name: 'notification-service'
    static_configs:
      - targets: ['notification-service:8080']

Run Prometheus using Docker:

docker run -d --name=prometheus -p 9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

Grafana Setup

Grafana is used for visualizing metrics collected by Prometheus.

Run Grafana using Docker:

docker run -d --name=grafana -p 3000:3000 grafana/grafana

Access Grafana at http://localhost:3000, and add Prometheus as a data source.

ELK Stack Setup

The ELK stack consists of Elasticsearch, Logstash, and Kibana. It is used for searching, analyzing, and visualizing log data.

Elasticsearch Setup

Run Elasticsearch using Docker:

docker run -d --name=elasticsearch -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.10.1

Logstash Setup

Create a logstash.conf file:

input {
  file {
    path => "/var/log/*.log"
    start_position => "beginning"
  }
}

output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
  }
}

Run Logstash using Docker:

docker run -d --name=logstash -p 5044:5044 -v $(pwd)/logstash.conf:/usr/share/logstash/pipeline/logstash.conf logstash:7.10.1

Kibana Setup

Run Kibana using Docker:

docker run -d --name=kibana -p 5601:5601 --link elasticsearch kibana:7.10.1

Access Kibana at http://localhost:5601 to visualize logs stored in Elasticsearch.

Instrumenting Your Microservices

For Prometheus metrics and Spring Boot Actuator endpoints:

Prometheus Metrics Exporter

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Enable Prometheus metrics in your application.yml:

management:
  endpoints:
    web:
      exposure:
        include: "*"
  metrics:
    export:
      prometheus:
        enabled: true
  endpoint:
    prometheus:
      enabled: true

With these configurations, your microservices will expose metrics at /actuator/prometheus, which Prometheus can scrape.

Logging Configuration

Configure logging in your Spring Boot applications to write logs to files that Logstash can process.

application.yml

logging:
  file:
    name: /var/log/application.log

Ensure that Logstash is configured to read these log files and send them to Elasticsearch.

Integrating Testcontainers for Integration Testing

Testcontainers is a Java library that supports JUnit tests, providing lightweight, disposable instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. Here’s how you can use Testcontainers to run integration tests for your microservices:

Product Service: Integration Test with Testcontainers

Add Testcontainers Dependency

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Integration Test Class

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ProductServiceIntegrationTest {

    @Container
    public static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:13-alpine"))
        .withDatabaseName("productdb")
        .withUsername("postgres")
        .withPassword("password");

    @LocalServerPort
    private int port;

    private String baseUrl;

    @Autowired
    private RestTemplate restTemplate;

    @BeforeAll
    public void setUp() {
        baseUrl = "http://localhost:" + port + "/products";
    }

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
        registry.add("spring.datasource.username", postgresContainer::getUsername);
        registry.add("spring.datasource.password", postgresContainer::getPassword);
    }

    @Test
    public void testCreateProduct() {
        Product product = new Product();
        product.setName("Test Product");
        product.setDescription("Test Description");
        product.setPrice(BigDecimal.TEN);

        Product createdProduct = restTemplate.postForObject(baseUrl, product, Product.class);

        assertThat(createdProduct).isNotNull();
        assertThat(createdProduct.getName()).isEqualTo("Test Product");
    }

    @Test
    public void testGetProductById() {
        Product product = new Product();
        product.setName("Test Product");
        product.setDescription("Test Description");
        product.setPrice(BigDecimal.TEN);

        Product createdProduct = restTemplate.postForObject(baseUrl, product, Product.class);
        Product fetchedProduct = restTemplate.getForObject(baseUrl + "/" + createdProduct.getId(), Product.class);

        assertThat(fetchedProduct).isNotNull();
        assertThat(fetchedProduct.getName()).isEqualTo("Test Product");
    }
}

Inventory Service: Integration Test with Testcontainers

Add Testcontainers Dependency

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mongodb</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Integration Test Class

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class InventoryServiceIntegrationTest {

    @Container
    public static MongoDBContainer mongoContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4.6"));

    @LocalServerPort
    private int port;

    private String baseUrl;

    @Autowired
    private RestTemplate restTemplate;

    @BeforeAll
    public void setUp() {
        baseUrl = "http://localhost:" + port + "/inventory";
    }

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.mongodb.uri", mongoContainer::getReplicaSetUrl);
    }

    @Test
    public void testCreateInventoryItem() {
        InventoryItem item = new InventoryItem();
        item.setName("Test Item");
        item.setQuantity(10);

        InventoryItem createdItem = restTemplate.postForObject(baseUrl, item, InventoryItem.class);

        assertThat(createdItem).isNotNull();
        assertThat(createdItem.getName()).isEqualTo("Test Item");
    }

    @Test
    public void testGetInventoryItemById() {
        InventoryItem item = new InventoryItem();
        item.setName("Test Item");
        item.setQuantity(10);

        InventoryItem createdItem = restTemplate.postForObject(baseUrl, item, InventoryItem.class);
        InventoryItem fetchedItem = restTemplate.getForObject(baseUrl + "/" + createdItem.getId(), InventoryItem.class);

        assertThat(fetchedItem).isNotNull();
        assertThat(fetchedItem.getName()).isEqualTo("Test Item");
    }
}

Notification Service: Integration Test with Testcontainers

Add Testcontainers Dependency

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>rabbitmq</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Integration Test Class

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class NotificationServiceIntegrationTest {

    @Container
    public static RabbitMQContainer rabbitContainer = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management"));

    @LocalServerPort
    private int port;

    private String baseUrl;

    @Autowired
    private RestTemplate restTemplate;

    @BeforeAll
    public void setUp() {
        baseUrl = "http://localhost:" + port + "/notifications";
    }

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.rabbitmq.host", rabbitContainer::getHost);
        registry.add("spring.rabbitmq.port", rabbitContainer::getAmqpPort);
    }

    @Test
    public void testCreateNotification() {
        Notification notification = new Notification();
        notification.setMessage("Test Notification");

        Notification createdNotification = restTemplate.postForObject(baseUrl, notification, Notification.class);

        assertThat(createdNotification).isNotNull();
        assertThat(createdNotification.getMessage()).isEqualTo("Test Notification");
    }

    @Test
    public void test

GetNotificationById() {
        Notification notification = new Notification();
        notification.setMessage("Test Notification");

        Notification createdNotification = restTemplate.postForObject(baseUrl, notification, Notification.class);
        Notification fetchedNotification = restTemplate.getForObject(baseUrl + "/" + createdNotification.getId(), Notification.class);

        assertThat(fetchedNotification).isNotNull();
        assertThat(fetchedNotification.getMessage()).isEqualTo("Test Notification");
    }
}

Conclusion

In this tutorial, we have built a comprehensive microservices application using a variety of modern technologies and best practices. We covered a wide range of topics and tools, ensuring that our microservices are robust, scalable, and maintainable.

Leave a Reply