Here is a detailed and production-grade Spring Boot Reactive Web (WebFlux) project structure, complete with folder organization, code examples, and explanations.
A reactive RESTful API for managing products using Spring WebFlux with MongoDB, built using a clean, layered architecture.
π Project Folder Structure
reactive-product-service/
βββ src/
β βββ main/
β β βββ java/com/example/productservice/
β β β βββ config/ # WebFlux configurations
β β β βββ controller/ # REST endpoints
β β β βββ dto/ # Data Transfer Objects
β β β βββ exception/ # Custom exceptions & handlers
β β β βββ mapper/ # DTO <-> Entity mappers
β β β βββ model/ # MongoDB entities
β β β βββ repository/ # Reactive Repositories
β β β βββ service/ # Service interfaces and implementations
β β β βββ util/ # Utility classes
β β β βββ ProductServiceApp.java # Spring Boot main class
β βββ resources/
β βββ application.yml
β βββ data/
β βββ sample-products.json
βββ pom.xml
βββ README.md
π§© Technologies Used
- Spring Boot 3+
- Spring WebFlux
- Spring Data Reactive MongoDB
- Lombok
- ModelMapper
- JUnit5 + Mockito for testing
- Docker (optional)
π§ pom.xml
(Important Dependencies)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Hereβs the complete ProductMapper.java
code that converts between ProductDTO
and Product
entity using ModelMapper:
β
ProductMapper.java
package com.example.productservice.mapper;
import com.example.productservice.dto.ProductDTO;
import com.example.productservice.model.Product;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;
@Component
public class ProductMapper {
private final ModelMapper modelMapper;
public ProductMapper() {
this.modelMapper = new ModelMapper();
}
public Product toEntity(ProductDTO dto) {
return modelMapper.map(dto, Product.class);
}
public ProductDTO toDto(Product entity) {
return modelMapper.map(entity, ProductDTO.class);
}
}
π¦ Ensure You Have This Dependency in pom.xml
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version> <!-- or latest -->
</dependency>
Manual Mapper:
Hereβs how you can write a manual mapper for converting between ProductDTO
and Product
without using any third-party libraries like ModelMapper
.
β
ProductMapper.java
(Manual Mapping)
package com.example.productservice.mapper;
import com.example.productservice.dto.ProductDTO;
import com.example.productservice.model.Product;
import org.springframework.stereotype.Component;
@Component
public class ProductMapper {
public Product toEntity(ProductDTO dto) {
if (dto == null) return null;
Product product = new Product();
product.setId(dto.getId());
product.setName(dto.getName());
product.setDescription(dto.getDescription());
product.setPrice(dto.getPrice());
return product;
}
public ProductDTO toDto(Product entity) {
if (entity == null) return null;
ProductDTO dto = new ProductDTO();
dto.setId(entity.getId());
dto.setName(entity.getName());
dto.setDescription(entity.getDescription());
dto.setPrice(entity.getPrice());
return dto;
}
}
π§ Why Manual Mapping?
β Pros:
- Complete control over transformation logic
- Better for performance-critical apps
- No dependency on external libs
π« Cons:
- More boilerplate code
- Tedious for large models
π DTO Class: ProductDTO.java
package com.example.productservice.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ProductDTO {
private String id;
private String name;
private String description;
private BigDecimal price;
}
π§± MongoDB Entity: Product.java
package com.example.productservice.model;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.math.BigDecimal;
@Document("products")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
@Id
private String id;
private String name;
private String description;
private BigDecimal price;
}
π Mapper: ProductMapper.java
package com.example.productservice.mapper;
import com.example.productservice.dto.ProductDTO;
import com.example.productservice.model.Product;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;
@Component
public class ProductMapper {
private final ModelMapper modelMapper = new ModelMapper();
public Product toEntity(ProductDTO dto) {
return modelMapper.map(dto, Product.class);
}
public ProductDTO toDto(Product entity) {
return modelMapper.map(entity, ProductDTO.class);
}
}
π¦ Repository: ProductRepository.java
package com.example.productservice.repository;
import com.example.productservice.model.Product;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProductRepository extends ReactiveCrudRepository<Product, String> {
Flux<Product> findByNameContainingIgnoreCase(String name);
}
π‘ Service Layer
Interface: ProductService.java
package com.example.productservice.service;
import com.example.productservice.dto.ProductDTO;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ProductService {
Mono<ProductDTO> createProduct(ProductDTO dto);
Mono<ProductDTO> getProductById(String id);
Flux<ProductDTO> getAllProducts();
Flux<ProductDTO> searchByName(String name);
Mono<Void> deleteProduct(String id);
}
Implementation: ProductServiceImpl.java
package com.example.productservice.service;
import com.example.productservice.dto.ProductDTO;
import com.example.productservice.exception.ProductNotFoundException;
import com.example.productservice.mapper.ProductMapper;
import com.example.productservice.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository repository;
private final ProductMapper mapper;
@Override
public Mono<ProductDTO> createProduct(ProductDTO dto) {
return repository.save(mapper.toEntity(dto))
.map(mapper::toDto);
}
@Override
public Mono<ProductDTO> getProductById(String id) {
return repository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)))
.map(mapper::toDto);
}
@Override
public Flux<ProductDTO> getAllProducts() {
return repository.findAll()
.map(mapper::toDto);
}
@Override
public Flux<ProductDTO> searchByName(String name) {
return repository.findByNameContainingIgnoreCase(name)
.map(mapper::toDto);
}
@Override
public Mono<Void> deleteProduct(String id) {
return repository.deleteById(id);
}
}
π REST Controller: ProductController.java
package com.example.productservice.controller;
import com.example.productservice.dto.ProductDTO;
import com.example.productservice.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService service;
@PostMapping
public Mono<ProductDTO> create(@RequestBody ProductDTO dto) {
return service.createProduct(dto);
}
@GetMapping
public Flux<ProductDTO> getAll() {
return service.getAllProducts();
}
@GetMapping("/{id}")
public Mono<ProductDTO> getById(@PathVariable String id) {
return service.getProductById(id);
}
@GetMapping("/search")
public Flux<ProductDTO> search(@RequestParam String name) {
return service.searchByName(name);
}
@DeleteMapping("/{id}")
public Mono<Void> delete(@PathVariable String id) {
return service.deleteProduct(id);
}
}
π¨ Exception Handling
Custom Exception
package com.example.productservice.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String id) {
super("Product not found with id: " + id);
}
}
Global Handler
package com.example.productservice.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<String> handleNotFound(ProductNotFoundException ex) {
return Mono.just(ex.getMessage());
}
}
βοΈ Configuration: application.yml
server:
port: 8080
spring:
data:
mongodb:
uri: mongodb://localhost:27017/productdb
βΆοΈ Main Class: ProductServiceApp.java
package com.example.productservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProductServiceApp {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApp.class, args);
}
}
π§ͺ Example Test (Service Layer)
@SpringBootTest
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository repository;
@InjectMocks
private ProductServiceImpl service;
@Test
void testCreateProduct() {
Product product = new Product("1", "Phone", "Smartphone", new BigDecimal("500"));
ProductDTO dto = new ProductDTO("1", "Phone", "Smartphone", new BigDecimal("500"));
when(repository.save(any(Product.class))).thenReturn(Mono.just(product));
StepVerifier.create(service.createProduct(dto))
.expectNextMatches(p -> p.getName().equals("Phone"))
.verifyComplete();
}
}
Top comments (0)