Using DTOs and ModelMapper for Clean Architecture [Java Spring Boot Mastery Series – Part 5]
🎯 Objective
Separate internal models from external API contracts using DTOs (Data Transfer Objects) and simplify object mapping using ModelMapper.
📦 Step 1: Create a ProductDTO Class
public class ProductDTO {
private Long id;
@NotBlank(message = "Product name is required")
private String name;
@NotNull(message = "Price is required")
@Min(value = 0, message = "Price must be positive")
private Double price;
// Getters and Setters
}
🔍 Explanation
- This class mirrors the Product entity, but is designed for API interaction.
- Keeps entity class decoupled from client-facing code.
🔧 Step 2: Add ModelMapper Bean
@Configuration
public class AppConfig {
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
🔍 Explanation
- Configures
ModelMapperas a Spring bean so it can be autowired anywhere.
🔁 Step 3: Update Service to Use DTOs
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ModelMapper modelMapper;
public List<ProductDTO> getAll() {
return productRepository.findAll().stream()
.map(product -> modelMapper.map(product, ProductDTO.class))
.collect(Collectors.toList());
}
public ProductDTO getById(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
return modelMapper.map(product, ProductDTO.class);
}
public ProductDTO save(ProductDTO dto) {
Product product = modelMapper.map(dto, Product.class);
Product saved = productRepository.save(product);
return modelMapper.map(saved, ProductDTO.class);
}
public void delete(Long id) {
productRepository.deleteById(id);
}
}
🔍 Explanation
- Converts between entity and DTO using
ModelMapper.map(). - Maintains a clear separation between internal model and external contract.
🌐 Step 4: Update Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<ProductDTO> getAllProducts() {
return productService.getAll();
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
return ResponseEntity.ok(productService.getById(id));
}
@PostMapping
public ResponseEntity<ProductDTO> createProduct(@Valid @RequestBody ProductDTO productDTO) {
return new ResponseEntity<>(productService.save(productDTO), HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
}
🔍 Explanation
- Uses
ProductDTOinstead of entity in controller layer. - Keeps HTTP interface separate from database logic.
🛠️ Benefits of Using DTOs
- 🔒 Hides sensitive fields (like passwords or internal IDs).
- 💡 Allows custom formats for clients.
- 🧱 Clean separation of concerns.
- 🔁 Prevents accidental data overwrites from external input.
➡️ Next Up: Part 6 – Entity Relationships and JPA Associations
