DEV Community

DevCorner2
DevCorner2

Posted on

🌐 Project Overview: Reactive Product Service

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
Enter fullscreen mode Exit fullscreen mode

🧩 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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ Ensure You Have This Dependency in pom.xml

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.1.1</version> <!-- or latest -->
</dependency>
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

🧠 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;
}
Enter fullscreen mode Exit fullscreen mode

🧱 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;
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ 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);
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

🌐 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

🚨 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

βš™οΈ Configuration: application.yml

server:
  port: 8080

spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/productdb
Enter fullscreen mode Exit fullscreen mode

▢️ 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)