Custom Exceptions

Exceptions That Describe Business Problems

package com.shopease.common.error;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, Object id) {
        super(resource + " " + id + " not found");
    }
}

public class InsufficientStockException extends RuntimeException {
    public InsufficientStockException(String product, int requested, int available) {
        super("Only " + available + " left of " + product + ", requested " + requested);
    }
}

public class DuplicateSkuException extends RuntimeException {
    public DuplicateSkuException(String sku) {
        super("SKU " + sku + " already exists");
    }
}
  • Extend RuntimeException (unchecked): service methods stay clean and @Transactional rolls back on them by default.
  • Name them after the business problem, not the HTTP status. The service should not know about HTTP.

Throwing Them from the Service

@Service
public class ProductService {
    private final ProductRepository repository;

    public ProductService(ProductRepository repository) { this.repository = repository; }

    public Product getById(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Product", id));
    }

    public Product reserve(Long id, int quantity) {
        Product p = getById(id);
        if (p.getStock() < quantity)
            throw new InsufficientStockException(p.getName(), quantity, p.getStock());
        p.setStock(p.getStock() - quantity);
        return repository.save(p);
    }
}

Note: ResponseStatusException (e.g. throw new ResponseStatusException(HttpStatus.NOT_FOUND, "...")) is fine for quick prototypes, but it ties your service to HTTP. Prefer your own exceptions plus a handler.