Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot/spring-data-jpa/references/reference.md
2025-11-29 18:28:30 +08:00

90 KiB

Spring Data JPA - Reference Documentation

Repository Interfaces

Repository Hierarchy

Spring Data JPA provides a clear inheritance hierarchy for repositories:

Repository (marker interface)
├── CrudRepository (basic CRUD operations)
│   ├── PagingAndSortingRepository (add pagination/sorting)
│   │   └── JpaRepository (JPA-specific operations)
│   └── ListCrudRepository (Spring Data 3+)
└── Reactive variants (ReactiveCrudRepository, etc.)

CrudRepository

Basic CRUD operations for all entities.

public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
    // CREATE/UPDATE
    <S extends T> S save(S entity);                    // Save or update entity
    <S extends T> Iterable<S> saveAll(Iterable<S> entities); // Save multiple entities

    // READ
    Optional<T> findById(ID id);                        // Find by primary key
    T getById(ID id);                                   // Returns proxy, never null
    T getReferenceById(ID id);                          // Like getById
    boolean existsById(ID id);                         // Check existence
    Iterable<T> findAll();                             // Find all entities
    Iterable<T> findAllById(Iterable<ID> ids);          // Find by multiple IDs

    // AGGREGATE
    long count();                                      // Count entities

    // DELETE
    void deleteById(ID id);                            // Delete by ID
    void delete(T entity);                             // Delete entity
    void deleteAllById(Iterable<? extends ID> ids);    // Delete by multiple IDs
    void deleteAll(Iterable<? extends T> entities);    // Delete multiple entities
    void deleteAll();                                  // Delete all entities
}

PagingAndSortingRepository

Extends CrudRepository with pagination and sorting capabilities.

public interface PagingAndSortingRepository<T, ID extends Serializable>
    extends CrudRepository<T, ID> {

    // Sorting
    Iterable<T> findAll(Sort sort);                   // Find all with sorting

    // Pagination
    Page<T> findAll(Pageable pageable);                // Find all with pagination
}

JpaRepository

The most comprehensive interface extending PagingAndSortingRepository with JPA-specific operations.

public interface JpaRepository<T, ID extends Serializable>
    extends PagingAndSortingRepository<T, ID> {

    // Enhanced read operations
    List<T> findAll();                                 // Returns List instead of Iterable
    List<T> findAllById(Iterable<ID> ids);            // Returns List instead of Iterable
    List<T> findAll(Sort sort);                       // Returns List instead of Iterable
    Page<T> findAll(Pageable pageable);               // Returns Page

    // Batch operations
    <S extends T> List<S> saveAll(Iterable<S> entities); // Save multiple with return

    // Batch delete operations
    void deleteInBatch(Iterable<T> entities);          // Delete without flushing
    void deleteAllInBatch(Iterable<ID> ids);          // Delete by IDs without flushing
    void deleteAllInBatch();                           // Delete all without flushing

    // Flush operations
    void flush();                                      // Flush to database
    <S extends T> S saveAndFlush(S entity);           // Save and immediately flush
    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities); // Save all and flush
}

Query Methods

Derived Query Methods

Spring Data automatically generates queries from method names following naming conventions.

Simple Lookups

Optional<User> findByEmail(String email);             // Single result
List<User> findByUsername(String username);           // Multiple results
User findFirstByEmail(String email);                // First result
User findTopByOrderByAgeDesc();                      // Top by age descending

Conditional Operators

// Equality
List<User> findByStatus(String status);
List<User> findByStatusNot(String status);

// Comparison
List<User> findByAgeGreaterThan(Integer age);
List<User> findByAgeLessThanEqual(Integer age);
List<User> findByAgeBetween(Integer min, Integer max);
List<User> findByAgeGreaterThanEqual(25);           // Static comparison

// Null/Empty checks
List<User> findByEmailIsNull();
List<User> findByEmailIsNotNull();
List<User> findByEmailNotEmpty();

// Boolean properties
List<User> findByActiveTrue();
List<User>ByEmailActiveFalse();

String Operations

// Pattern matching
List<User> findByEmailContaining(String pattern);    // LIKE '%pattern%'
List<User> findByEmailStartsWith(String prefix);     // LIKE 'prefix%'
List<User> findByEmailEndsWith(String suffix);       // LIKE '%suffix'
List<User> findByEmailLike(String pattern);          // Exact LIKE pattern

// Case sensitivity
List<User> findByEmailIgnoreCase(String email);

Date and Time Operations

// After/Before
List<Order> findByOrderDateAfter(LocalDate date);
List<Order> findByCreatedDateBefore(LocalDateTime dateTime);

// Between
List<Order> findByOrderDateBetween(LocalDate start, LocalDate end);

// Range queries
List<Order> findByTotalPriceGreaterThan(BigDecimal min);
List<Order> findByTotalPriceBetween(BigDecimal min, BigDecimal max);

// Current date comparisons
List<Order> findByCreatedDateBefore(LocalDateTime.now().minusDays(7));

Ordering

// Simple ordering
List<User> findByStatusOrderByCreatedDateDesc(String status);
List<User> findAllByOrderByLastNameAsc();

// Multiple sort criteria
List<Product> findByCategoryOrderByPriceAscNameDesc(String category);

// Dynamic sorting
List<User> findAll(Sort.by("lastName").ascending());
List<User> findByActiveTrue(Sort.by("createdDate").descending());

Pagination Integration

Page<User> findByStatus(String status, Pageable pageable);
Slice<User> findByActiveTrue(Pageable pageable);     // No total count
List<User> findTop10ByOrderByCreatedDateDesc();      // Fixed limit

Delete Operations

// Delete with return count
long deleteByEmail(String email);
long deleteByStatusAndAge(String status, Integer age);

// Delete entities
void deleteByStatus(String status);                  // Bulk delete

Custom Queries with @Query

JPQL Queries

Use Java Persistence Query Language for complex queries.

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // Basic query with named parameters
    @Query("SELECT o FROM Order o WHERE o.status = :status AND o.totalPrice > :minPrice")
    List<Order> findActiveOrdersAbovePrice(
        @Param("status") String status,
        @Param("minPrice") BigDecimal minPrice
    );

    // Query with JOIN FETCH to avoid N+1 problem
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.customerId = :customerId")
    List<Order> findOrdersWithItems(@Param("customerId") Long customerId);

    // Aggregate function
    @Query("SELECT COUNT(o) FROM Order o WHERE o.status = 'COMPLETED'")
    long countCompletedOrders();

    // IN clause
    @Query("SELECT o FROM Order o WHERE o.status IN :statuses")
    List<Order> findByStatuses(@Param("statuses") List<String> statuses);

    // EXISTS clause
    @Query("SELECT o FROM Order o WHERE EXISTS (SELECT 1 FROM o.items i WHERE i.quantity = 0)")
    List<Order> findOrdersWithZeroQuantityItems();
}

Native SQL Queries

Use native SQL for database-specific queries.

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // Simple native query
    @Query(value = "SELECT * FROM products WHERE category = :category AND price < :maxPrice",
           nativeQuery = true)
    List<Product> findProductsByCategory(
        @Param("category") String category,
        @Param("maxPrice") BigDecimal maxPrice
    );

    // Native query with mapping
    @Query(value = """
        SELECT p.id, p.name, p.price, c.name as category_name
        FROM products p
        JOIN categories c ON p.category_id = c.id
        WHERE p.price > :minPrice
        ORDER BY p.price DESC
        """, nativeQuery = true)
    @QueryResults projection = ProductSummary.class;  // Custom projection
    List<ProductSummary> findExpensiveProductSummaries(@Param("minPrice") BigDecimal minPrice);
}

Modifying Queries

Use @Modifying for INSERT, UPDATE, DELETE operations.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Transactional
    @Query("UPDATE User u SET u.lastLoginDate = :now WHERE u.id = :userId")
    void updateLastLoginDate(
        @Param("userId") Long userId,
        @Param("now") LocalDateTime now
    );

    @Modifying
    @Transactional
    @Query("DELETE FROM User u WHERE u.createdDate < :cutoffDate")
    int deleteInactiveUsers(@Param("cutoffDate") LocalDateTime cutoffDate);

    @Modifying
    @Transactional
    @Query(value = "UPDATE users SET status = 'INACTIVE' WHERE last_login < :cutoff",
           nativeQuery = true)
    int deactivateInactiveUsersNative(@Param("cutoff") LocalDateTime cutoff);
}

Entity Relationships

One-to-One Relationship

Foreign Key Approach

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String email;

    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    private Address address;
}

@Entity
@Table(name = "addresses")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String street;
    private String city;
    private String postalCode;

    @OneToOne(mappedBy = "address")
    private User user;
}

Shared Primary Key Approach

@Entity
@Table(name = "employees")
public class Employee {
    @Id
    private Long id;  // Shared with profile

    @Column(nullable = false)
    private String firstName;

    @OneToOne(mappedBy = "employee", fetch = FetchType.LAZY)
    private EmployeeProfile profile;
}

@Entity
@Table(name = "employee_profiles")
public class EmployeeProfile {
    @Id
    private Long id;  // Same as employee ID

    @Column(length = 500)
    private String bio;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId  // Maps to employee.id
    @JoinColumn(name = "id")
    private Employee employee;
}

One-to-Many Relationship

@Entity
@Table(name = "categories")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @OneToMany(mappedBy = "category",
               cascade = CascadeType.ALL,
               orphanRemoval = true,
               fetch = FetchType.LAZY)
    private List<Product> products = new ArrayList<>();

    // Helper methods
    public void addProduct(Product product) {
        products.add(product);
        product.setCategory(this);
    }

    public void removeProduct(Product product) {
        products.remove(product);
        product.setCategory(null);
    }
}

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 255)
    private String name;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", nullable = false)
    private Category category;
}

Many-to-Many Relationship

Simple Join Table

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();

    public void addCourse(Course course) {
        courses.add(course);
        course.getStudents().add(this);
    }

    public void removeCourse(Course course) {
        courses.remove(course);
        course.getStudents().remove(this);
    }
}

@Entity
@Table(name = "courses")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String title;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}

Join Table with Additional Attributes

@Entity
@Table(name = "enrollments")
public class Enrollment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id", nullable = false)
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id", nullable = false)
    private Course course;

    @Column(nullable = false)
    private LocalDateTime enrolledAt;

    @Column(precision = 3, scale = 2)
    private Double grade;  // Can be null

    @Column(length = 20)
    private String status;  // ACTIVE, WITHDRAWN, COMPLETED

    // Composite primary key (alternative approach)
    @EmbeddedId
    private EnrollmentId enrollmentId;

    @ManyToOne
    @JoinColumn(name = "student_id", insertable = false, updatable = false)
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id", insertable = false, updatable = false)
    private Course course;
}

@Embeddable
public class EnrollmentId implements Serializable {
    private Long studentId;
    private Long courseId;

    // equals(), hashCode() implementation
}

Bidirectional Relationships

Best Practices

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
    private List<Employee> employees = new ArrayList<>();

    // Helper method for consistency
    public void addEmployee(Employee employee) {
        employees.add(employee);
        employee.setDepartment(this);
    }
}

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne
    @JoinColumn(name = "department_id", nullable = false)
    private Department department;
}

Pagination and Sorting

Pagination Basics

@Service
public class ProductService {
    private final ProductRepository repository;

    public Page<Product> getProductsPage(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return repository.findAll(pageable);
    }

    public Page<Product> getProductsWithSorting(int page, int size, String sortField) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortField));
        return repository.findAll(pageable);
    }

    public Page<Product> getProductsWithMultiSort(int page, int size) {
        Sort sort = Sort.by("price").ascending()
                      .and(Sort.by("name").ascending());
        Pageable pageable = PageRequest.of(page, size, sort);
        return repository.findAll(pageable);
    }
}

Advanced Pagination

@Service
public class OrderService {
    private final OrderRepository repository;

    public Page<Order> getOrdersByStatus(String status, int page, int size) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("createdDate").descending());
        return repository.findByStatus(status, pageable);
    }

    public Slice<Order> getRecentOrders(int page, int size) {
        // Slice doesn't count total elements (more efficient for large datasets)
        Pageable pageable = PageRequest.of(page, size);
        return repository.findByStatus("NEW", pageable);
    }

    public Stream<Order> streamAllOrders() {
        // Stream for large datasets
        return repository.streamAllBy();
    }
}

Custom Pagination Queries

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.active = true")
    Page<User> findActiveUsers(Pageable pageable);

    @Query(value = "SELECT * FROM users WHERE created_at > :date",
           nativeQuery = true)
    Page<User> findUsersCreatedAfter(@Param("date") LocalDateTime date, Pageable pageable);
}

Database Auditing

Spring Data JPA Auditing

Configuration

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
@EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class)
public class AuditingConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName)
            .or(() -> Optional.of("system"));
    }

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        return transactionManager;
    }
}

Auditing Entities

@Entity
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime lastModifiedDate;

    @CreatedBy
    @Column(nullable = false, updatable = false, length = 50)
    private String createdBy;

    @LastModifiedBy
    @Column(nullable = false, length = 50)
    private String lastModifiedBy;

    @Version
    private Long version;  // Optimistic locking
}

@Entity
public class Product extends BaseEntity {
    @Column(nullable = false, length = 255)
    private String name;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Enumerated(EnumType.STRING)
    private ProductStatus status;
}

public enum ProductStatus {
    ACTIVE, INACTIVE, DISCONTINUED
}

Custom Auditor Provider

@Component
public class CustomAuditorProvider implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // Try to get from security context first
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            return Optional.of(authentication.getName());
        }

        // Fallback to system user or throw exception
        return Optional.of("system");
    }
}

JPA Lifecycle Callbacks

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status;

    @Column(nullable = false)
    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
        if (status == null) {
            status = "PENDING";
        }
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    @PreRemove
    protected void onRemove() {
        // Cleanup logic before deletion
    }

    @PostLoad
    protected void onLoad() {
        // Post-load processing
    }
}

Hibernate Envers for Auditing

Configuration

@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRepositoryFactoryBean.class)
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class EnversConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName);
    }
}

Audited Entities

@Entity
@Audited
@RevisionEntity(EntityRevisionListener.class)
public class Document {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @Enumerated(EnumType.STRING)
    private DocumentStatus status;

    @ManyToOne
    @JoinColumn(name = "author_id", nullable = false)
    private User author;
}

@RevisionEntity
public class EntityRevisionListener implements RevisionListener {
    @Override
    public void newRevision(Object revisionEntity) {
        EntityRevision revision = (EntityRevision) revisionEntity;
        revision.setRevisionDate(LocalDateTime.now());
        revision.setUsername(SecurityContextHolder.getContext()
            .getAuthentication().getName());
    }
}

@Entity
public class EntityRevision {
    @Id
    @GeneratedValue
    private Integer id;

    private LocalDateTime revisionDate;

    @Column(length = 50)
    private String username;
}

Repository Usage

public interface DocumentRepository extends JpaRepository<Document, Long>,
                                            JpaEntityRepository<Document, Long, Integer> {

    @Query("SELECT d FROM Document d WHERE d.id = :id AND d.revisionNumber <= :revision")
    Document findHistoricalVersion(@Param("id") Long id, @Param("revision") Integer revision);

    List<Number> findRevisions(Long documentId);

    <T> T findRevision(Class<T> entityClass, Number revision);
}

@Service
public class DocumentAuditService {
    private final DocumentRepository documentRepository;
    private final AuditReader auditReader;

    public List<DocumentRevision> getDocumentHistory(Long documentId) {
        List<Number> revisions = auditReader.getRevisions(Document.class, documentId);
        return revisions.stream()
            .map(revision -> new DocumentRevision(
                revision,
                auditReader.find(Document.class, documentId, revision),
                auditReader.getRevisionDateForRevision(revision)
            ))
            .collect(Collectors.toList());
    }
}

Transactions and Deletion

Transaction Management

Transaction Configuration

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;
    private final InventoryService inventoryService;

    @Transactional
    public Order createOrder(Order order, List<Payment> payments) {
        // Start transaction
        Order savedOrder = orderRepository.save(order);

        // Process payments
        payments.forEach(payment -> {
            payment.setOrderId(savedOrder.getId());
            paymentRepository.save(payment);
        });

        // Update inventory
        inventoryService.reserveItems(savedOrder.getItems());

        return savedOrder;
    }

    @Transactional(readOnly = true)
    public Order getOrderWithDetails(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new EntityNotFoundException("Order not found: " + orderId));
    }

    @Transactional(rollbackFor = {PaymentException.class, InventoryException.class})
    public void processPayment(Long orderId) throws PaymentException {
        Order order = getOrderWithDetails(orderId);

        // Process payment logic
        paymentService.process(order.getPayments());

        // Update order status
        order.setStatus("PROCESSING");
        orderRepository.save(order);

        // This will trigger rollback if thrown
        if (paymentFailed) {
            throw new PaymentException("Payment processing failed");
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrderCreation(Order order) {
        // Always creates new transaction
        auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
    }
}

Propagation Types

@Service
public class TransactionalService {

    @Transactional
    public void methodA() {
        methodB();  // Runs in same transaction
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // Runs in new transaction
    }

    @Transactional(propagation = Propagation.NESTED)
    public void methodC() {
        // Runs in nested transaction (savepoint)
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodD() {
        // Runs in existing transaction or none
    }
}

Isolation Levels

@Service
public class OrderService {

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public Order getOrderWithConsistentData(Long orderId) {
        // Read committed isolation - prevents dirty reads
        return orderRepository.findById(orderId).orElseThrow();
    }

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void processInventoryUpdate(List<InventoryItem> items) {
        // Serializable isolation - prevents all concurrency issues
        items.forEach(inventoryService::updateStock);
    }

    @Transactional(timeout = 30)  // 30 seconds timeout
    public void processLongRunningTask() {
        // Long-running operation with timeout
        externalApiService.callExternalSystem();
    }
}

Delete Operations

Repository Delete Methods

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // Basic delete operations
    void deleteById(Long id);
    void delete(Order entity);
    void deleteAllById(Iterable<Long> ids);
    void deleteAll(Iterable<Order> entities);
    void deleteAll();

    // Derived delete queries
    long deleteByStatus(String status);
    long deleteByCreatedDateBefore(LocalDateTime date);
    long deleteByTotalPriceLessThan(BigDecimal threshold);

    // Custom delete query
    @Modifying
    @Transactional
    @Query("DELETE FROM Order o WHERE o.status = :status AND o.totalPrice < :minPrice")
    int deleteOldPendingOrders(
        @Param("status") String status,
        @Param("minPrice") BigDecimal minPrice
    );
}

Service Layer Delete Operations

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final OrderItemRepository orderItemRepository;

    @Transactional
    public void deleteOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        // Delete related items first (or use cascade)
        orderItemRepository.deleteByOrderId(orderId);

        // Delete order
        orderRepository.delete(order);
    }

    @Transactional
    public long cleanupExpiredOrders(LocalDateTime cutoffDate) {
        return orderRepository.deleteByCreatedDateBefore(cutoffDate);
    }

    @Transactional
    public int cleanupOldPendingOrders(BigDecimal minPrice) {
        return orderRepository.deleteOldPendingOrders("PENDING", minPrice);
    }

    @Transactional
    public void batchDeleteOrders(List<Long> orderIds) {
        // Batch delete for better performance
        orderRepository.deleteAllById(orderIds);
    }
}

Cascade and Orphan Removal

@Entity
@Table(name = "categories")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @OneToMany(mappedBy = "category",
               cascade = CascadeType.ALL,     // Cascade save/update/delete
               orphanRemoval = true)           // Remove children when removed from collection
    private List<Product> products = new ArrayList<>();

    @OneToMany(mappedBy = "category",
               cascade = CascadeType.PERSIST,  // Only cascade save operations
               orphanRemoval = false)
    private List<Product> inactiveProducts = new ArrayList<>();

    public void addProduct(Product product) {
        products.add(product);
        product.setCategory(this);
    }

    public void removeProduct(Product product) {
        products.remove(product);
        product.setCategory(null);  // Triggers orphan removal
    }
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 255)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", nullable = false)
    private Category category;

    // Bidirectional relationship management
    public void setCategory(Category category) {
        if (this.category != null) {
            this.category.removeProduct(this);
        }
        this.category = category;
        if (category != null) {
            category.addProduct(this);
        }
    }
}

Batch Operations

@Service
public class BatchOrderService {
    private final OrderRepository orderRepository;

    @Transactional
    public void batchUpdateOrders(List<OrderUpdate> updates) {
        // Use batch processing for large updates
        int batchSize = 50;
        for (int i = 0; i < updates.size(); i++) {
            OrderUpdate update = updates.get(i);

            Order order = orderRepository.findById(update.orderId())
                .orElseThrow();

            order.setStatus(update.status());
            order.setNotes(update.notes());

            if (i % batchSize == 0) {
                orderRepository.flush();  // Flush periodically
                orderRepository.clear();   // Clear persistence context
            }
        }
    }

    @Transactional
    public void batchDeleteOrdersByStatus(List<String> statuses) {
        // Batch delete by status
        statuses.forEach(status ->
            orderRepository.deleteByStatus(status));
    }
}

UUID as Primary Key

Modern Approach (Hibernate 6.2+ with JPA 3.1)

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(nullable = false, length = 100, unique = true)
    private String email;

    @Column(length = 50)
    private String username;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @CreatedDate
    private LocalDateTime createdDate;
}

@Entity
@Table(name = "sessions")
public class UserSession {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "user_id", nullable = false)
    private UUID userId;

    @Column(nullable = false, unique = true)
    private String token;

    @Column(name = "expires_at", nullable = false)
    private LocalDateTime expiresAt;
}

@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
    Optional<User> findByEmail(String email);
    Optional<User> findByUsername(String username);
    List<User> findByStatus(UserStatus status);
}

@Service
public class UserService {
    private final UserRepository repository;

    public User createUser(CreateUserRequest request) {
        if (repository.findByEmail(request.email()).isPresent()) {
            throw new EmailAlreadyExistsException(request.email());
        }

        User user = new User();
        user.setEmail(request.email());
        user.setUsername(request.username());
        user.setStatus(UserStatus.ACTIVE);

        return repository.save(user);
    }

    public User getUser(UUID id) {
        return repository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }
}

Hibernate-Specific UUID Generation

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @UuidGenerator(style = UuidGenerator.Style.TIME)  // Version 1: Time-based
    private UUID id;

    @Column(nullable = false, length = 50, unique = true)
    private String orderNumber;

    @Column(precision = 12, scale = 2)
    private BigDecimal totalAmount;

    @CreatedDate
    private LocalDateTime createdAt;
}

@Entity
@Table(name = "products")
public class Product {
    @Id
    @UuidGenerator(style = UuidGenerator.Style.RANDOM)  // Version 4: Random
    private UUID id;

    @Column(nullable = false, length = 255)
    private String name;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Column(name = "sku", length = 50, unique = true)
    private String sku;
}

@Entity
@Table(name = "events")
public class SystemEvent {
    @Id
    @UuidGenerator  // Default: RANDOM (Version 4)
    private UUID id;

    @Column(nullable = false, length = 50)
    private String eventType;

    @Column(columnDefinition = "TEXT")
    private String eventData;

    @CreatedDate
    private LocalDateTime timestamp;
}

UUID Storage Options

@Entity
@Table(name = "transactions")
public class Transaction {
    @Id
    @UuidGenerator
    @Column(columnDefinition = "VARCHAR(36)")  // Store as string for some databases
    private String id;

    @Column(precision = 19, scale = 4)  // High precision for financial data
    private BigDecimal amount;

    @Enumerated(EnumType.STRING)
    private TransactionStatus status;

    @Column(name = "reference_id", length = 36)
    private String referenceId;  // Secondary UUID field
}

@Entity
@Table(name = "audit_logs")
public class AuditLog {
    @Id
    @UuidGenerator
    private UUID id;  // Native UUID type (PostgreSQL, etc.)

    @Column(name = "entity_id", length = 36)  // May need VARCHAR for MySQL
    private String entityId;  // Foreign key as string for compatibility

    @Column(nullable = false, length = 100)
    private String action;

    @Column(columnDefinition = "TEXT")
    private String changes;

    @CreatedDate
    private LocalDateTime timestamp;
}

UUID vs Sequential ID Comparison

// UUID Entity: Better for distributed systems
@Entity
@Table(name = "articles")
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;  // Good for: microservices, distributed DBs, offline-first

    @Column(nullable = false, length = 200)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @ManyToOne
    private User author;
}

// Sequential Entity: Better for single database
@Entity
@Table(name = "comments")
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // Good for: single database, better index performance

    @Column(nullable = false, columnDefinition = "TEXT")
    private String text;

    @ManyToOne
    private Article article;
}

// Hybrid approach: Best of both worlds
@Entity
@Table(name = "blogs")
public class Blog {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;  // Unique identifier

    @Column(unique = true, name = "slug")
    private String slug;  // URL-friendly, sequential-like identifier

    @Column(nullable = false, length = 200)
    private String title;

    @ManyToOne
    private User owner;
}

Performance Considerations

@Service
public class UserService {
    private final UserRepository repository;

    // Batch operations with UUIDs
    @Transactional
    public List<User> batchCreateUsers(List<CreateUserRequest> requests) {
        List<User> users = requests.stream()
            .map(request -> {
                User user = new User();
                user.setEmail(request.email());
                user.setUsername(request.username());
                return user;
            })
            .collect(Collectors.toList());

        return repository.saveAll(users);
    }

    // Index optimization for UUID queries
    @Transactional(readOnly = true)
    public Page<User> findUsersByEmailPattern(String pattern, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return repository.findByEmailContaining(pattern, pageable);
    }

    // Cache UUID lookups
    @Cacheable(value = "users", key = "#id")
    public User getUser(UUID id) {
        return repository.findById(id).orElseThrow();
    }
}

Database Indexing

Basic Index Definitions

@Entity
@Table(
    name = "products",
    indexes = {
        @Index(name = "idx_product_name", columnList = "name"),
        @Index(name = "idx_product_category", columnList = "category_id")
    }
)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 255)
    private String name;

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Column(name = "created_date")
    private LocalDateTime createdDate;
}

@Entity
@Table(
    name = "users",
    indexes = {
        // Unique index for email
        @Index(name = "idx_user_email_unique", columnList = "email", unique = true),

        // Index for username (unique)
        @Index(name = "idx_user_username", columnList = "username", unique = true),

        // Index for status filtering
        @Index(name = "idx_user_status", columnList = "status")
    }
)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100, unique = true)
    private String email;

    @Column(length = 50, unique = true)
    private String username;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

Composite Indexes

@Entity
@Table(
    name = "orders",
    indexes = {
        // Single column indexes
        @Index(name = "idx_order_status", columnList = "status"),
        @Index(name = "idx_order_customer", columnList = "customer_id"),

        // Composite index for common query pattern
        @Index(name = "idx_status_customer_created",
               columnList = "status, customer_id, created_at DESC"),

        // Another composite index
        @Index(name = "idx_order_created_status",
               columnList = "created_at DESC, status"),

        // Index for reporting queries
        @Index(name = "idx_order_report",
               columnList = "status, total_amount, created_at")
    }
)
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20)
    private String status;  // PENDING, PROCESSING, COMPLETED, CANCELLED

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @Column(precision = 12, scale = 2)
    private BigDecimal totalAmount;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Enumerated(EnumType.STRING)
    private OrderType type;  // RETAIL, WHOLESALE, B2B
}

@Entity
@Table(
    name = "order_items",
    indexes = {
        // Index for finding items by order
        @Index(name = "idx_order_item_order", columnList = "order_id"),

        // Composite index for order and product queries
        @Index(name = "idx_order_item_order_product",
               columnList = "order_id, product_id"),

        // Index for price-based queries
        @Index(name = "idx_order_item_price",
               columnList = "unit_price DESC, quantity"),

        // Index for inventory sync
        @Index(name = "idx_order_item_product_created",
               columnList = "product_id, created_at DESC")
    }
)
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @ManyToOne
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(precision = 10, scale = 2, name = "unit_price")
    private BigDecimal unitPrice;

    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

Index Sorting and Uniqueness

@Entity
@Table(
    name = "products",
    indexes = {
        // Ascending indexes (default)
        @Index(name = "idx_product_price_asc", columnList = "price ASC"),
        @Index(name = "idx_product_name_asc", columnList = "name ASC"),

        // Descending indexes for "newest first" queries
        @Index(name = "idx_product_created_desc",
               columnList = "created_at DESC"),

        // Unique indexes
        @Index(name = "idx_product_sku_unique",
               columnList = "sku", unique = true),
        @Index(name = "idx_product_code_unique",
               columnList = "product_code", unique = true),

        // Multi-column unique index
        @Index(
            name = "idx_category_code_unique",
            columnList = "category_id, product_code",
            unique = true
        ),

        // Mixed sort order
        @Index(
            name = "idx_category_price",
            columnList = "category_id ASC, price DESC"
        ),

        // Index for search
        @Index(
            name = "idx_product_search",
            columnList = "name, description"
        )
    }
)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, length = 50)
    private String sku;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(columnDefinition = "TEXT")
    private String description;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @ManyToOne
    private Category category;

    @Column(name = "product_code", length = 20)
    private String productCode;
}

Indexes for Search and Filtering

@Entity
@Table(
    name = "articles",
    indexes = {
        // Full-text search indexes
        @Index(name = "idx_article_title", columnList = "title"),
        @Index(name = "idx_article_content", columnList = "content"),
        @Index(name = "idx_article_author", columnList = "author_id"),

        // Indexes for filtering and sorting
        @Index(name = "idx_article_status_date",
               columnList = "status, published_at DESC"),
        @Index(name = "idx_author_date",
               columnList = "author_id, published_at DESC"),
        @Index(name = "idx_category_date",
               columnList = "category_id, published_at DESC"),

        // Range query indexes
        @Index(name = "idx_article_published_range",
               columnList = "published_at DESC"),
        @Index(name = "idx_article_created_range",
               columnList = "created_at DESC"),

        // Pagination optimization
        @Index(name = "idx_article_id_date",
               columnList = "id, published_at DESC"),

        // Status-specific indexes
        @Index(name = "idx_article_published",
               columnList = "status = 'PUBLISHED'"),
        @Index(name = "idx_article_draft",
               columnList = "status = 'DRAFT'")
    }
)
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @Enumerated(EnumType.STRING)
    private ArticleStatus status;  // DRAFT, PUBLISHED, ARCHIVED

    @Column(name = "published_at")
    private LocalDateTime publishedDate;

    @Column(name = "created_at")
    private LocalDateTime createdDate;

    @ManyToOne
    private User author;

    @ManyToOne
    private Category category;

    @Column(length = 100)
    private String slug;  // URL-friendly identifier

    @Column(length = 50)
    private String excerpt;
}

@Repository
public interface ArticleRepository extends JpaRepository<Article, Long> {
    // Uses idx_article_status_date
    Page<Article> findByStatusOrderByPublishedDateDesc(
        String status,
        Pageable pageable
    );

    // Uses idx_author_date
    Page<Article> findByAuthorOrderByPublishedDateDesc(
        User author,
        Pageable pageable
    );

    // Uses idx_article_published_range
    List<Article> findByPublishedDateAfterOrderByPublishedDateDesc(
        LocalDateTime date
    );

    // Custom query that benefits from multiple indexes
    @Query("""
        SELECT a FROM Article a
        WHERE a.status = :status
        AND a.publishedDate BETWEEN :start AND :end
        AND a.category = :category
        ORDER BY a.publishedDate DESC
        """)
    Page<Article> findPublishedInCategoryAndDateRange(
        @Param("status") String status,
        @Param("start") LocalDateTime start,
        @Param("end") LocalDateTime end,
        @Param("category") Category category,
        Pageable pageable
    );
}

@Service
@Transactional(readOnly = true)
public class ArticleSearchService {
    private final ArticleRepository repository;

    public Page<Article> getPublishedArticles(int page, int size) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("publishedDate").descending());
        return repository.findByStatusOrderByPublishedDateDesc("PUBLISHED", pageable);
    }

    public Page<Article> getArticlesByAuthor(User author, int page, int size) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("publishedDate").descending());
        return repository.findByAuthorOrderByPublishedDateDesc(author, pageable);
    }

    public List<Article> getRecentArticles(int limit) {
        return repository.findByPublishedDateAfterOrderByPublishedDateDesc(
            LocalDateTime.now().minusMonths(1)
        ).stream().limit(limit).collect(Collectors.toList());
    }
}

Indexes on Relationships

@Entity
@Table(
    name = "comments",
    indexes = {
        // Index for finding comments by article (most common query)
        @Index(name = "idx_comment_article", columnList = "article_id"),

        // Composite index for recent comments by article
        @Index(name = "idx_comment_article_created",
               columnList = "article_id, created_at DESC"),

        // Index for approved comments filtering
        @Index(name = "idx_comment_article_approved",
               columnList = "article_id, approved"),

        // Index for finding user comments
        @Index(name = "idx_comment_user", columnList = "user_id"),

        // Index for moderation
        @Index(name = "idx_comment_status_created",
               columnList = "status, created_at DESC"),

        // Index for spam detection
        @Index(name = "idx_comment_user_article",
               columnList = "user_id, article_id"),

        // Index for sentiment analysis
        @Index(name = "idx_comment_sentiment",
               columnList = "sentiment_score")
    }
)
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "article_id", nullable = false)
    private Article article;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Column(columnDefinition = "TEXT")
    private String text;

    @Column(name = "created_at")
    private LocalDateTime createdDate;

    private boolean approved;

    @Column(name = "approved_at")
    private LocalDateTime approvedDate;

    @Enumerated(EnumType.STRING)
    private CommentStatus status;  // PENDING, APPROVED, REJECTED, SPAM

    private Integer sentimentScore;  // For sentiment analysis
}

@Entity
@Table(
    name = "categories",
    indexes = {
        // Basic index
        @Index(name = "idx_category_name", columnList = "name"),

        // Composite index for product queries
        @Index(name = "idx_category_products_count",
               columnList = "parent_id, product_count DESC"),

        // Index for hierarchical navigation
        @Index(name = "idx_category_hierarchy",
               columnList = "parent_id, sort_order"),

        // Index for search
        @Index(name = "idx_category_search",
               columnList = "name, description, slug"),

        // Index for statistics
        @Index(name = "idx_category_stats",
               columnList = "is_active, product_count, created_at")
    }
)
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(length = 200)
    private String description;

    @Column(name = "slug", length = 100, unique = true)
    private String slug;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent;

    private Integer sortOrder;

    private boolean isActive;

    @Column(name = "product_count")
    private Long productCount;

    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
    // Uses idx_comment_article
    List<Comment> findByArticle(Article article);

    // Uses idx_comment_article_created
    List<Comment> findByArticleOrderByCreatedDateDesc(Article article);

    // Uses idx_comment_article_approved
    long countByArticleAndApproved(Article article, boolean approved);

    // Uses idx_comment_user
    List<Comment> findByUser(User user);

    // Uses idx_comment_status_created
    List<Comment> findByStatusOrderByCreatedDateDesc(CommentStatus status);

    // Multiple indexes considered by query optimizer
    @Query("""
        SELECT c FROM Comment c
        WHERE c.article = :article
        AND c.approved = true
        ORDER BY c.createdDate DESC
        """)
    List<Comment> findApprovedCommentsByArticle(
        @Param("article") Article article,
        Pageable pageable
    );
}

Index Best Practices

@Entity
@Table(
    name = "products",
    indexes = {
        // Rule 1: Index on columns used in WHERE clauses
        @Index(name = "idx_product_status", columnList = "status"),

        // Rule 2: Index columns used in JOIN conditions
        @Index(name = "idx_product_category", columnList = "category_id"),

        // Rule 3: Create composite indexes for common multi-column queries
        @Index(name = "idx_category_status",
               columnList = "category_id, status"),

        // Rule 4: Include sorting columns in composite indexes
        @Index(name = "idx_status_created",
               columnList = "status, created_at DESC"),

        // Rule 5: Avoid over-indexing - only index what's frequently queried
        @Index(name = "idx_product_popularity",
               columnList = "view_count, sales_count DESC"),

        // Rule 6: Use partial indexes for filtered queries
        @Index(name = "idx_active_products",
               columnList = "status = 'ACTIVE'"),

        // Rule 7: Consider covering indexes for common query patterns
        @Index(name = "idx_product_covering",
               columnList = "category_id, name, price")
    }
)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 255)
    private String name;

    @Enumerated(EnumType.STRING)
    private ProductStatus status;

    @ManyToOne
    private Category category;

    private Double viewCount;
    private Double salesCount;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

// Example queries that benefit from proper indexing
@Service
public class ProductService {
    private final ProductRepository repository;

    // Benefits from idx_category_status
    public List<Product> getActiveProductsByCategory(Category category) {
        return repository.findByCategoryAndStatus(category, "ACTIVE");
    }

    // Benefits from idx_status_created
    public Page<Product> getRecentActiveProducts(int page, int size) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("createdAt").descending());
        return repository.findByStatus("ACTIVE", pageable);
    }

    // Benefits from idx_product_popularity
    public List<Product> getPopularProducts(int limit) {
        return repository.findTop10ByOrderBySalesCountDesc()
            .stream()
            .limit(limit)
            .collect(Collectors.toList());
    }
}

Multiple Database Configuration

Basic Configuration for Multiple Databases

Primary Database Configuration

@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.users.repository",
    entityManagerFactoryRef = "usersEntityManager",
    transactionManagerRef = "usersTransactionManager"
)
@PropertySource("classpath:application-users.properties")
public class UsersDbConfig {

    @Bean
    @ConfigurationProperties(prefix = "users.datasource")
    public DataSource usersDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean usersEntityManager(
            DataSource usersDataSource) {
        LocalContainerEntityManagerFactoryBean em =
            new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(usersDataSource);
        em.setPackagesToScan("com.example.users.model");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        em.setJpaProperties(hibernateProperties());

        return em;
    }

    @Bean
    public PlatformTransactionManager usersTransactionManager(
            @Qualifier("usersEntityManager")
            LocalContainerEntityManagerFactoryBean usersEntityManager) {
        return new JpaTransactionManager(usersEntityManager.getObject());
    }

    private Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect",
            "org.hibernate.dialect.MySQL8Dialect");
        properties.setProperty("hibernate.hbm2ddl.auto", "validate");
        properties.setProperty("hibernate.show_sql", "false");
        properties.setProperty("hibernate.format_sql", "true");
        return properties;
    }
}

Secondary Database Configuration

@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.products.repository",
    entityManagerFactoryRef = "productsEntityManager",
    transactionManagerRef = "productsTransactionManager"
)
@PropertySource("classpath:application-products.properties")
public class ProductsDbConfig {

    @Bean
    @ConfigurationProperties(prefix = "products.datasource")
    public DataSource productsDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean productsEntityManager(
            DataSource productsDataSource) {
        LocalContainerEntityManagerFactoryBean em =
            new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(productsDataSource);
        em.setPackagesToScan("com.example.products.model");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        em.setJpaProperties(hibernateProperties());

        return em;
    }

    @Bean
    public PlatformTransactionManager productsTransactionManager(
            @Qualifier("productsEntityManager")
            LocalContainerEntityManagerFactoryBean productsEntityManager) {
        return new JpaTransactionManager(productsEntityManager.getObject());
    }

    private Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect",
            "org.hibernate.dialect.PostgreSQLDialect");
        properties.setProperty("hibernate.hbm2ddl.auto", "update");
        properties.setProperty("hibernate.show_sql", "false");
        return properties;
    }
}

Properties Configuration

# users.properties
users.datasource.url=jdbc:mysql://localhost:3306/users_db
users.datasource.username=root
users.datasource.password=password
users.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# products.properties
products.datasource.url=jdbc:postgresql://localhost:5432/products_db
products.datasource.username=postgres
products.datasource.password=postgres
products.datasource.driver-class-name=org.postgresql.Driver

Entity Configuration for Multiple Databases

// Users database entities
@Entity
@Table(name = "users", schema = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100, unique = true)
    private String email;

    @Column(length = 50, unique = true)
    private String username;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

@Entity
@Table(name = "user_profiles", schema = "users")
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;

    @Column(length = 100)
    private String firstName;

    @Column(length = 100)
    private String lastName;

    @Column(length = 20)
    private String phoneNumber;

    @Enumerated(EnumType.STRING)
    private LanguagePreference language;
}

// Products database entities
@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 255)
    private String name;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @ManyToOne
    private Category category;

    private Boolean isActive;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "categories")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @ManyToOne
    private Category parent;

    private Integer sortOrder;

    private Boolean isActive;
}

Repository Configuration

// Users repositories
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> findByUsername(String username);
    List<User> findByStatus(UserStatus status);
}

@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
    Optional<UserProfile> findByUserId(Long userId);
}

// Products repositories
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByCategory(Category category);
    List<Product> findByIsActiveTrue();
    Page<Product> findByIsActiveTrue(Pageable pageable);
}

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
    List<Category> findByParentIsNull();
    List<Category> findByParent(Category parent);
    List<Category> findByIsActiveTrueOrderBySortOrderAsc();
}

Service Layer Configuration

@Service
@Transactional("usersTransactionManager")
public class UserService {
    private final UserRepository userRepository;
    private final UserProfileRepository userProfileRepository;

    public UserService(UserRepository userRepository,
                     UserProfileRepository userProfileRepository) {
        this.userRepository = userRepository;
        this.userProfileRepository = userProfileRepository;
    }

    @Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
    public User createUser(UserCreateRequest request) {
        User user = new User();
        user.setEmail(request.email());
        user.setUsername(request.username());
        user.setStatus(UserStatus.ACTIVE);

        User savedUser = userRepository.save(user);

        UserProfile profile = new UserProfile();
        profile.setUser(savedUser);
        profile.setFirstName(request.firstName());
        profile.setLastName(request.lastName());
        profile.setLanguage(request.language());

        userProfileRepository.save(profile);

        return savedUser;
    }

    @Transactional(value = "usersTransactionManager", readOnly = true)
    public Optional<User> getUserByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    @Transactional(value = "usersTransactionManager")
    public void updateUserProfile(Long userId, ProfileUpdateRequest request) {
        UserProfile profile = userProfileRepository.findByUserId(userId)
            .orElseThrow(() -> new UserProfileNotFoundException(userId));

        profile.setFirstName(request.firstName());
        profile.setLastName(request.lastName());
        profile.setPhoneNumber(request.phoneNumber());

        userProfileRepository.save(profile);
    }
}

@Service
@Transactional("productsTransactionManager")
public class ProductService {
    private final ProductRepository productRepository;
    private final CategoryRepository categoryRepository;

    public ProductService(ProductRepository productRepository,
                         CategoryRepository categoryRepository) {
        this.productRepository = productRepository;
        this.categoryRepository = categoryRepository;
    }

    @Transactional(value = "productsTransactionManager", readOnly = true)
    public Page<Product> getActiveProducts(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return productRepository.findByIsActiveTrue(pageable);
    }

    @Transactional(value = "productsTransactionManager")
    public Product createProduct(ProductCreateRequest request) {
        Category category = categoryRepository.findById(request.categoryId())
            .orElseThrow(() -> new CategoryNotFoundException(request.categoryId()));

        Product product = new Product();
        product.setName(request.name());
        product.setPrice(request.price());
        product.setCategory(category);
        product.setIsActive(true);

        return productRepository.save(product);
    }

    @Transactional(value = "productsTransactionManager")
    public void updateProduct(Long productId, ProductUpdateRequest request) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        product.setName(request.name());
        product.setPrice(request.price());
        product.setUpdatedAt(LocalDateTime.now());

        productRepository.save(product);
    }
}

Cross-Database Transactions

@Service
public class OrderService {
    private final UserService userService;
    private final ProductService productService;
    private final OrderRepository orderRepository;

    @Transactional
    public Order createOrder(OrderRequest request) {
        // Start transaction (will handle cross-database operations)
        Order order = new Order();
        order.setOrderNumber(generateOrderNumber());
        order.setTotalAmount(request.totalAmount());
        order.setStatus(OrderStatus.PENDING);

        // Get user from users database
        User user = userService.getUserByEmail(request.userEmail())
            .orElseThrow(() -> new UserNotFoundException(request.userEmail()));

        // Get products from products database
        List<Product> products = request.productIds().stream()
            .map(productId -> productService.getProductById(productId)
                .orElseThrow(() -> new ProductNotFoundException(productId)))
            .collect(Collectors.toList());

        // Calculate total amount and validate
        BigDecimal calculatedTotal = products.stream()
            .map(Product::getPrice)
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        if (!calculatedTotal.equals(request.totalAmount())) {
            throw new OrderAmountMismatchException();
        }

        // Save order in products database
        Order savedOrder = orderRepository.save(order);

        // Create order items (cross-database operation)
        products.forEach(product -> {
            OrderItem item = new OrderItem();
            item.setOrder(savedOrder);
            item.setProduct(product);
            item.setQuantity(1); // Simplified for example
            item.setUnitPrice(product.getPrice());

            orderRepository.saveOrderItem(item); // Custom method
        });

        return savedOrder;
    }

    @Transactional(value = "usersTransactionManager")
    @Transactional(value = "productsTransactionManager", propagation = Propagation.REQUIRES_NEW)
    public void processPayment(Order order, Payment payment) {
        // Payment processing logic
        // Updates both user and product databases
    }
}

Database-Specific Configuration

// Custom Hibernate configuration for each database
@Configuration
public class HibernateConfig {

    @Bean
    @Primary
    public HibernateJpaVendorAdapter primaryJpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setDatabasePlatform("org.hibernate.dialect.MySQL8Dialect");
        return adapter;
    }

    @Bean
    public HibernateJpaVendorAdapter secondaryJpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setDatabasePlatform("org.hibernate.dialect.PostgreSQLDialect");
        return adapter;
    }

    @Bean
    public JpaProperties jpaProperties() {
        return new JpaProperties();
    }
}

// DataSource routing for dynamic database selection
@Component
public class DataSourceRouter extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getCurrentDatabase();
    }
}

@Component
public class DatabaseContextHolder {
    private static final ThreadLocal<DatabaseType> CONTEXT = new ThreadLocal<>();

    public static void setCurrentDatabase(DatabaseType databaseType) {
        CONTEXT.set(databaseType);
    }

    public static DatabaseType getCurrentDatabase() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

public enum DatabaseType {
    USERS, PRODUCTS
}

// Dynamic database selection aspect
@Aspect
@Component
public class DatabaseAspect {

    @Around("@annotation(selectDatabase)")
    public Object selectDatabase(ProceedingJoinPoint joinPoint,
                                SelectDatabase selectDatabase) throws Throwable {
        DatabaseType databaseType = selectDatabase.value();
        DatabaseType previousType = DatabaseContextHolder.getCurrentDatabase();

        try {
            DatabaseContextHolder.setCurrentDatabase(databaseType);
            return joinPoint.proceed();
        } finally {
            DatabaseContextHolder.setCurrentDatabase(previousType);
        }
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SelectDatabase {
    DatabaseType value();
}

Advanced Configuration with Connection Pooling

@Configuration
@EnableConfigurationProperties(DbProperties.class)
public class MultiDatabaseConfig {

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "primary.datasource.hikari")
    public HikariDataSource primaryDataSource(DbProperties dbProperties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dbProperties.getPrimary().getUrl());
        dataSource.setUsername(dbProperties.getPrimary().getUsername());
        dataSource.setPassword(dbProperties.getPrimary().getPassword());
        dataSource.setDriverClassName(dbProperties.getPrimary().getDriverClassName());

        // Connection pool settings
        dataSource.setMaximumPoolSize(20);
        dataSource.setMinimumIdle(5);
        dataSource.setConnectionTimeout(30000);
        dataSource.setIdleTimeout(600000);
        dataSource.setMaxLifetime(1800000);

        return dataSource;
    }

    @Bean
    @ConfigurationProperties(prefix = "secondary.datasource.hikari")
    public HikariDataSource secondaryDataSource(DbProperties dbProperties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dbProperties.getSecondary().getUrl());
        dataSource.setUsername(dbProperties.getSecondary().getUsername());
        dataSource.setPassword(dbProperties.getSecondary().getPassword());
        dataSource.setDriverClassName(dbProperties.getSecondary().getDriverClassName());

        // Different connection pool settings for secondary DB
        dataSource.setMaximumPoolSize(10);
        dataSource.setMinimumIdle(2);
        dataSource.setConnectionTimeout(20000);

        return dataSource;
    }
}

@ConfigurationProperties
@Data
public class DbProperties {
    private DataSourceConfig primary;
    private DataSourceConfig secondary;

    @Data
    public static class DataSourceConfig {
        private String url;
        private String username;
        private String password;
        private String driverClassName;
    }
}

Examples

Complete CRUD Service

@Service
@Transactional("usersTransactionManager")
public class UserService {
    private final UserRepository repository;
    private final UserProfileRepository profileRepository;
    private final RoleRepository roleRepository;

    public UserService(UserRepository repository,
                     UserProfileRepository profileRepository,
                     RoleRepository roleRepository) {
        this.repository = repository;
        this.profileRepository = profileRepository;
        this.roleRepository = roleRepository;
    }

    @Transactional(readOnly = true)
    public List<User> getAllUsers() {
        return repository.findAll();
    }

    @Transactional(readOnly = true)
    public Page<User> getUsersPage(int page, int size) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("createdDate").descending());
        return repository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public User getUser(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));
    }

    @Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
    public User createUser(CreateUserRequest request) {
        if (repository.findByEmail(request.email()).isPresent()) {
            throw new EmailAlreadyExistsException(request.email());
        }

        User user = new User();
        user.setEmail(request.email());
        user.setUsername(request.username());
        user.setStatus(UserStatus.ACTIVE);

        // Set default roles
        List<Role> defaultRoles = roleRepository.findByNameIn(Arrays.asList("USER"));
        user.setRoles(new HashSet<>(defaultRoles));

        User savedUser = repository.save(user);

        // Create profile
        UserProfile profile = new UserProfile();
        profile.setUser(savedUser);
        profile.setFirstName(request.firstName());
        profile.setLastName(request.lastName());
        profile.setLanguage(request.language());

        profileRepository.save(profile);

        return savedUser;
    }

    @Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
    public User updateUser(Long id, UpdateUserRequest request) {
        User user = getUser(id);

        if (!request.email().equals(user.getEmail()) &&
            repository.findByEmail(request.email()).isPresent()) {
            throw new EmailAlreadyExistsException(request.email());
        }

        user.setEmail(request.email());
        user.setUsername(request.username());
        user.setStatus(request.status());

        User updatedUser = repository.save(user);

        // Update profile
        UserProfile profile = profileRepository.findByUserId(id)
            .orElseThrow(() -> new UserProfileNotFoundException(id));

        profile.setFirstName(request.firstName());
        profile.setLastName(request.lastName());
        profile.setLanguage(request.language());

        profileRepository.save(profile);

        return updatedUser;
    }

    @Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
    public void deleteUser(Long id) {
        User user = getUser(id);

        // Soft delete by setting status
        user.setStatus(UserStatus.DELETED);
        repository.save(user);

        // Delete profile
        profileRepository.deleteByUserId(id);
    }

    @Transactional(value = "usersTransactionManager", readOnly = true)
    public long countByStatus(UserStatus status) {
        return repository.countByStatus(status);
    }

    @Transactional(value = "usersTransactionManager", readOnly = true)
    public List<User> searchUsers(UserSearchCriteria criteria) {
        if (criteria.isEmpty()) {
            return repository.findAll();
        }

        return repository.findByEmailContainingOrUsernameContainingOrFirstNameContainingOrLastNameContaining(
            criteria.searchTerm(),
            criteria.searchTerm(),
            criteria.searchTerm(),
            criteria.searchTerm()
        );
    }
}

Search with Multiple Criteria

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // Derived query methods
    List<Product> findByCategory(Category category);
    List<Product> findByPriceGreaterThan(BigDecimal price);
    List<Product> findByStatusAndPriceBetween(ProductStatus status,
                                            BigDecimal minPrice, BigDecimal maxPrice);

    // Custom search query
    @Query("""
        SELECT p FROM Product p
        WHERE (:name IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :name, '%')))
        AND (:category IS NULL OR p.category.id = :category)
        AND (:minPrice IS NULL OR p.price >= :minPrice)
        AND (:maxPrice IS NULL OR p.price <= :maxPrice)
        AND (:status IS NULL OR p.status = :status)
        AND p.isActive = true
        ORDER BY p.createdAt DESC
        """)
    Page<Product> searchProducts(
        @Param("name") String name,
        @Param("category") Long category,
        @Param("minPrice") BigDecimal minPrice,
        @Param("maxPrice") BigDecimal maxPrice,
        @Param("status") ProductStatus status,
        Pageable pageable
    );

    // Native query for complex filtering
    @Query(value = """
        SELECT p.* FROM products p
        LEFT JOIN categories c ON p.category_id = c.id
        WHERE (:name IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :name, '%')))
        AND (:category IS NULL OR c.id = :category)
        AND (:minPrice IS NULL OR p.price >= :minPrice)
        AND (:maxPrice IS NULL OR p.price <= :maxPrice)
        AND (:status IS NULL OR p.status = :status)
        AND p.is_active = true
        ORDER BY p.created_at DESC
        """, nativeQuery = true)
    Page<Product> searchProductsNative(
        @Param("name") String name,
        @Param("category") Long category,
        @Param("minPrice") BigDecimal minPrice,
        @Param("maxPrice") BigDecimal maxPrice,
        @Param("status") String status,
        Pageable pageable
    );
}

@Service
public class ProductSearchService {
    private final ProductRepository repository;

    public Page<Product> search(ProductSearchCriteria criteria, int page, int size) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("createdAt").descending());

        return repository.searchProducts(
            criteria.name(),
            criteria.categoryId(),
            criteria.minPrice(),
            criteria.maxPrice(),
            criteria.status(),
            pageable
        );
    }

    public List<Product> findPopularProducts(int limit) {
        return repository.findTop10ByOrderBySalesCountDesc()
            .stream()
            .limit(limit)
            .collect(Collectors.toList());
    }

    public List<Product> findRelatedProducts(Product product, int limit) {
        return repository.findByCategoryAndIdNot(product.getCategory(), product.getId())
            .stream()
            .limit(limit)
            .collect(Collectors.toList());
    }
}

// Search criteria DTO
public record ProductSearchCriteria(
    @NotBlank String name,
    Long categoryId,
    BigDecimal minPrice,
    BigDecimal maxPrice,
    String status
) {}

// Search criteria builder
public class ProductSearchCriteriaBuilder {
    private String name;
    private Long categoryId;
    private BigDecimal minPrice;
    private BigDecimal maxPrice;
    private String status;

    public ProductSearchCriteriaBuilder name(String name) {
        this.name = name;
        return this;
    }

    public ProductSearchCriteriaBuilder categoryId(Long categoryId) {
        this.categoryId = categoryId;
        return this;
    }

    public ProductSearchCriteriaBuilder priceRange(BigDecimal min, BigDecimal max) {
        this.minPrice = min;
        this.maxPrice = max;
        return this;
    }

    public ProductSearchCriteriaBuilder status(String status) {
        this.status = status;
        return this;
    }

    public ProductSearchCriteria build() {
        return new ProductSearchCriteria(name, categoryId, minPrice, maxPrice, status);
    }
}

Best Practices

Entity Design

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, length = 255)
    private String name;

    @Column(name = "price", precision = 10, scale = 2, nullable = false)
    private BigDecimal price;

    @Column(name = "created_date", nullable = false, updatable = false)
    private LocalDateTime createdDate;

    @Column(name = "updated_date")
    private LocalDateTime updatedDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", nullable = false)
    private Category category;

    @Enumerated(EnumType.STRING)
    private ProductStatus status;

    // Constructor injection pattern
    public Product(String name, BigDecimal price, Category category) {
        this.name = name;
        this.price = price;
        this.category = category;
        this.status = ProductStatus.ACTIVE;
        this.createdDate = LocalDateTime.now();
        this.updatedDate = LocalDateTime.now();
    }

    // Business logic methods
    public void updatePrice(BigDecimal newPrice) {
        if (newPrice.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Price must be positive");
        }
        this.price = newPrice;
        this.updatedDate = LocalDateTime.now();
    }

    public void activate() {
        this.status = ProductStatus.ACTIVE;
        this.updatedDate = LocalDateTime.now();
    }

    public void deactivate() {
        this.status = ProductStatus.INACTIVE;
        this.updatedDate = LocalDateTime.now();
    }

    // equals() and hashCode() based on id
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Product product = (Product) o;
        return Objects.equals(id, product.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

Repository Queries

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Good: Simple derived query
    Optional<User> findByEmail(String email);

    // Good: Complex query with @Query
    @Query("""
        SELECT u FROM User u
        WHERE u.status = :status
        AND u.createdDate >= :startDate
        ORDER BY u.createdDate DESC
        """)
    List<User> findActiveUsersSince(
        @Param("status") UserStatus status,
        @Param("startDate") LocalDateTime startDate
    );

    // Good: Using projection for better performance
    @Query("""
        SELECT new com.example.dto.UserSummary(u.id, u.email, u.status, u.createdDate)
        FROM User u
        WHERE u.status = :status
        """)
    List<UserSummary> findUserSummariesByStatus(@Param("status") UserStatus status);

    // Avoid: Long method name
    // List<User> findByStatusAndCreatedDateGreaterThanAndLastLoginDateLessThan(...)

    // Avoid: Not using Optional for single results
    // User findByEmail(String email);  // Should return Optional<User>
}

Pagination Best Practices

@Service
public class ProductService {
    private final ProductRepository repository;

    public Page<Product> getProducts(int page, int size) {
        // Validate pagination parameters
        if (page < 0) {
            throw new IllegalArgumentException("Page number must be non-negative");
        }
        if (size <= 0 || size > 100) {
            throw new IllegalArgumentException("Page size must be between 1 and 100");
        }

        Pageable pageable = PageRequest.of(page, size,
            Sort.by("id").ascending());
        return repository.findAll(pageable);
    }

    public List<Product> getRecentProducts(int limit) {
        // Use list for smaller datasets where you need exact count
        if (limit <= 0 || limit > 100) {
            throw new IllegalArgumentException("Limit must be between 1 and 100");
        }

        return repository.findTop10ByOrderByCreatedAtDesc()
            .stream()
            .limit(limit)
            .collect(Collectors.toList());
    }

    public Slice<Product> getRecentProductsWithCursor(Long lastId, int size) {
        // Use cursor-based pagination for better performance
        Sort sort = Sort.by("id").ascending();
        Pageable pageable = PageRequest.of(0, size, sort);

        if (lastId != null) {
            return repository.findByIdGreaterThan(lastId, pageable);
        } else {
            return repository.findAll(pageable);
        }
    }
}

Transaction Management

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;

    @Transactional
    public Order createOrder(Order order, Payment payment) {
        // Validate business rules
        if (order.getItems().isEmpty()) {
            throw new EmptyOrderException("Order must contain at least one item");
        }

        // Calculate total amount
        BigDecimal total = order.getItems().stream()
            .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        if (total.compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidOrderAmountException("Order total must be positive");
        }

        // Set order details
        order.setTotalAmount(total);
        order.setStatus(OrderStatus.PENDING);
        order.setOrderNumber(generateOrderNumber());

        // Save order
        Order savedOrder = orderRepository.save(order);

        // Process payment
        payment.setOrderId(savedOrder.getId());
        payment.setAmount(total);
        paymentRepository.save(payment);

        return savedOrder;
    }

    @Transactional(readOnly = true)
    public Order getOrderWithDetails(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }

    @Transactional(rollbackFor = {PaymentException.class, InventoryException.class})
    public void processPayment(Long orderId) throws PaymentException {
        Order order = getOrderWithDetails(orderId);

        // Process payment logic
        paymentService.process(order.getPayment());

        // Update order status
        order.setStatus(OrderStatus.PROCESSING);
        order.setUpdatedAt(LocalDateTime.now());

        orderRepository.save(order);

        // This will trigger rollback if thrown
        if (paymentFailed) {
            throw new PaymentException("Payment processing failed");
        }

        // Update inventory after successful payment
        inventoryService.reserveItems(order.getItems());
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrderCreation(Order order) {
        // Always creates new transaction
        auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
    }
}

N+1 Query Problem Solutions

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // Solution 1: Use JOIN FETCH
    @Query("""
        SELECT DISTINCT o FROM Order o
        JOIN FETCH o.items
        JOIN FETCH o.customer
        WHERE o.id = :orderId
        """)
    Optional<Order> findOrderWithItemsAndCustomer(@Param("orderId") Long orderId);

    // Solution 2: Use EntityGraph
    @EntityGraph(attributePaths = {"items", "customer", "items.product"})
    Optional<Order> findWithDetailsById(Long orderId);

    // Solution 3: Use batch loading
    @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId")
    List<Order> findByCustomerId(@Param("customerId") Long customerId);

    // Solution 4: Use DTO projection
    @Query("""
        SELECT new com.example.dto.OrderSummary(
            o.id, o.orderNumber, o.totalAmount, o.status,
            c.firstName, c.lastName
        ) FROM Order o
        JOIN o.customer c
        WHERE o.customer.id = :customerId
        """)
    List<OrderSummary> findOrderSummariesByCustomerId(@Param("customerId") Long customerId);
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;

    // Solution using JOIN FETCH
    @Transactional(readOnly = true)
    public Order getOrderWithDetails(Long orderId) {
        return orderRepository.findOrderWithItemsAndCustomer(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }

    // Solution using batch loading
    @Transactional(readOnly = true)
    public List<Order> getCustomerOrdersWithDetails(Long customerId) {
        List<Order> orders = orderRepository.findByCustomerId(customerId);

        // Batch load items for all orders
        Map<Long, List<OrderItem>> orderItems = orderRepository.findOrderItemsByOrderIds(
            orders.stream().map(Order::getId).collect(Collectors.toList())
        );

        // Batch load products for all items
        Map<Long, Product> products = orderRepository.findProductsByItemIds(
            orderItems.values().stream()
                .flatMap(List::stream)
                .map(OrderItem::getId)
                .collect(Collectors.toList())
        );

        // Associate items with their orders
        orders.forEach(order -> {
            List<OrderItem> items = orderItems.getOrDefault(order.getId(), Collections.emptyList());
            items.forEach(item -> {
                item.setProduct(products.get(item.getProductId()));
                order.addItem(item);
            });
        });

        return orders;
    }

    // Solution using DTO projection (most efficient)
    @Transactional(readOnly = true)
    public List<OrderSummary> getCustomerOrderSummaries(Long customerId) {
        return orderRepository.findOrderSummariesByCustomerId(customerId);
    }
}

Exception Handling

@Service
public class ProductService {
    private final ProductRepository repository;

    public Product getProduct(Long id) {
        return repository.findById(id)
            .orElseThrow(() ->
                new ResourceNotFoundException("Product not found: " + id)
            );
    }

    public Product createProduct(ProductCreateRequest request) {
        // Validate business rules
        if (request.name() == null || request.name().trim().isEmpty()) {
            throw new InvalidProductException("Product name cannot be empty");
        }

        if (request.price() == null || request.price().compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidProductException("Product price must be non-negative");
        }

        if (repository.findByNameIgnoreCase(request.name()).isPresent()) {
            throw new DuplicateProductException("Product with name '" + request.name() + "' already exists");
        }

        Product product = new Product();
        product.setName(request.name());
        product.setPrice(request.price());
        product.setDescription(request.description());
        product.setStatus(ProductStatus.ACTIVE);

        return repository.save(product);
    }

    public Product updateProduct(Long id, ProductUpdateRequest request) {
        Product product = getProduct(id);

        if (request.name() != null) {
            if (!request.name().equals(product.getName()) &&
                repository.findByNameIgnoreCase(request.name()).isPresent()) {
                throw new DuplicateProductException("Product with name '" + request.name() + "' already exists");
            }
            product.setName(request.name());
        }

        if (request.price() != null) {
            if (request.price().compareTo(BigDecimal.ZERO) < 0) {
                throw new InvalidProductException("Product price must be non-negative");
            }
            product.setPrice(request.price());
        }

        if (request.description() != null) {
            product.setDescription(request.description());
        }

        return repository.save(product);
    }

    @Transactional(rollbackFor = Exception.class)
    public void deleteProduct(Long id) {
        Product product = getProduct(id);

        // Check if product can be deleted (e.g., has orders)
        if (orderService.hasOrdersForProduct(id)) {
            throw new ProductDeletionException("Cannot delete product with existing orders");
        }

        repository.delete(product);
    }
}

// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

public class DuplicateProductException extends RuntimeException {
    public DuplicateProductException(String message) {
        super(message);
    }
}

public class InvalidProductException extends RuntimeException {
    public InvalidProductException(String message) {
        super(message);
    }
}

public class ProductDeletionException extends RuntimeException {
    public ProductDeletionException(String message) {
        super(message);
    }
}

References