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:
- JPA (Java Persistence API): For relational data persistence.
- MongoDB: For non-relational data storage.
- Redis: For caching to enhance performance.
- RabbitMQ: For asynchronous message-driven communication.
- Swagger: For API documentation.
- Actuator: For monitoring and management endpoints.
- Spring Security and OAuth2: For securing the services.
- Reactive Programming with Spring WebFlux: For handling asynchronous data streams.
- Spring Cloud Stream: For message-driven microservices.
- Vault: For secure management of sensitive configuration properties.
- Docker: For containerizing the microservices.
- Kubernetes: For orchestrating and managing the deployment of containerized applications.
- Prometheus and Grafana: For monitoring the services.
- ELK Stack (Elasticsearch, Logstash, Kibana): For logging and log analysis.
- 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.