| name | clean-code |
| description | Use when reviewing or writing code to enforce DRY, KISS, and YAGNI principles with Java-specific guidance. |
Clean Code Principles for Java
DRY - Don't Repeat Yourself
Extract repeated logic into reusable methods or classes.
Bad - Duplicated validation logic
@RestController
@RequestMapping("/api")
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
if (request.getCustomerId() == null || request.getCustomerId().isBlank()) {
throw new IllegalArgumentException("Customer ID is required");
}
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
@PutMapping("/orders/{id}")
public ResponseEntity<Order> updateOrder(@PathVariable Long id, @RequestBody OrderRequest request) {
if (request.getCustomerId() == null || request.getCustomerId().isBlank()) {
throw new IllegalArgumentException("Customer ID is required");
}
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
}
Good - Use Bean Validation
public record OrderRequest(
@NotBlank(message = "Customer ID is required")
String customerId,
@NotNull(message = "Amount is required")
@Positive(message = "Amount must be positive")
BigDecimal amount,
@NotEmpty(message = "At least one item is required")
List<@Valid OrderItemRequest> items
) {}
public record OrderItemRequest(
@NotBlank String productId,
@Min(1) int quantity
) {}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody OrderRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(orderService.create(request));
}
@PutMapping("/{id}")
public ResponseEntity<OrderResponse> updateOrder(
@PathVariable Long id,
@Valid @RequestBody OrderRequest request) {
return ResponseEntity.ok(orderService.update(id, request));
}
}
DRY with Shared Mapper Logic
@Component
public class OrderMapper {
public Order toEntity(OrderRequest request) {
Order order = new Order();
order.setCustomerId(request.customerId());
order.setAmount(request.amount());
order.setItems(request.items().stream()
.map(this::toItemEntity)
.toList());
return order;
}
public OrderResponse toResponse(Order order) {
return new OrderResponse(
order.getId(),
order.getCustomerId(),
order.getAmount(),
order.getStatus().name(),
order.getCreatedAt()
);
}
private OrderItem toItemEntity(OrderItemRequest request) {
OrderItem item = new OrderItem();
item.setProductId(request.productId());
item.setQuantity(request.quantity());
return item;
}
}
KISS - Keep It Simple, Stupid
Avoid unnecessary complexity. Prefer straightforward solutions.
Bad - Over-engineered generic repository
public abstract class AbstractGenericCrudService<T, ID, R extends JpaRepository<T, ID>> {
protected final R repository;
protected final Function<T, ?> entityToDto;
protected final Function<?, T> dtoToEntity;
}
Good - Simple, direct service
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ProductMapper productMapper;
public ProductService(ProductRepository productRepository, ProductMapper productMapper) {
this.productRepository = productRepository;
this.productMapper = productMapper;
}
public ProductResponse findById(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
return productMapper.toResponse(product);
}
public Page<ProductResponse> findAll(Pageable pageable) {
return productRepository.findAll(pageable)
.map(productMapper::toResponse);
}
@Transactional
public ProductResponse create(ProductRequest request) {
Product product = productMapper.toEntity(request);
Product saved = productRepository.save(product);
return productMapper.toResponse(saved);
}
}
KISS with Exception Handling
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.toList();
return ResponseEntity.badRequest()
.body(new ErrorResponse("Validation failed", errors));
}
}
public record ErrorResponse(String message, List<String> details) {
public ErrorResponse(String message) {
this(message, List.of());
}
}
YAGNI - You Aren't Gonna Need It
Do not build features or abstractions until they are needed.
Bad - Premature abstraction
public interface NotificationStrategy {
void send(Notification notification);
}
public class NotificationStrategyFactory {
private final Map<String, NotificationStrategy> strategies;
}
Good - Build what you need now
@Service
public class NotificationService {
private final JavaMailSender mailSender;
public NotificationService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendOrderConfirmation(Order order, String recipientEmail) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(recipientEmail);
message.setSubject("Order Confirmation #" + order.getId());
message.setText("Your order has been confirmed. Total: " + order.getAmount());
mailSender.send(message);
}
}
Method Length and Readability
Bad - Long method doing too much
public OrderResponse processOrder(OrderRequest request) {
}
Good - Small focused methods
@Service
public class OrderProcessingService {
private final CustomerService customerService;
private final InventoryService inventoryService;
private final PricingService pricingService;
private final OrderRepository orderRepository;
private final NotificationService notificationService;
private final OrderMapper orderMapper;
public OrderProcessingService(CustomerService customerService,
InventoryService inventoryService,
PricingService pricingService,
OrderRepository orderRepository,
NotificationService notificationService,
OrderMapper orderMapper) {
this.customerService = customerService;
this.inventoryService = inventoryService;
this.pricingService = pricingService;
this.orderRepository = orderRepository;
this.notificationService = notificationService;
this.orderMapper = orderMapper;
}
@Transactional
public OrderResponse processOrder(OrderRequest request) {
Customer customer = customerService.getVerifiedCustomer(request.customerId());
inventoryService.reserveItems(request.items());
BigDecimal total = pricingService.calculateTotal(request.items(), customer);
Order order = orderMapper.toEntity(request);
order.setTotalAmount(total);
order.setStatus(OrderStatus.CONFIRMED);
Order saved = orderRepository.save(order);
notificationService.sendOrderConfirmation(saved, customer.getEmail());
return orderMapper.toResponse(saved);
}
}
Naming Conventions
- Classes:
PascalCase - OrderService, ProductController
- Methods:
camelCase - findById, calculateTotal
- Constants:
UPPER_SNAKE_CASE - MAX_RETRY_COUNT
- Packages: lowercase -
pl.piomin.services.order.controller
- Booleans: prefix with
is, has, can - isActive, hasPermission
- Collections: pluralize -
List<Order> orders, Set<String> tags