<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Libre+Caslon+Text:ital,wght@0,400;0,700;1,400&family=Libre+Franklin:wght@400..800&family=JetBrains+Mono:wght@400;500;700&display=swap" />
Late edition Stop the presses

Suspect at large

A backend developer is fleeing the scene. The record is being set.

Setting the type Inking the plates Running the presses

Skip to the front page
Kathmandu, NepalThe Backend EditionEst. 2021

The personal record of a backend developer

Wednesday 5 August 2026Vol. ISelected works & notesPrice: one coffee
← Back to the dispatchesDatabase · 10 min read

Database

Database Optimization Techniques for Spring Data JPA

Advanced techniques for optimizing database queries in Spring Data JPA applications, including lazy loading, query optimization, and performance monitoring.

By · Backend Developer — Spring Boot & DevOps5 January 2024 · 10 min read

Database performance is crucial for any application's success. This guide covers advanced optimization techniques for Spring Data JPA applications.

Introduction

Spring Data JPA simplifies database operations, but without proper optimization, it can lead to performance issues. Understanding how JPA works under the hood is essential for building efficient applications.

Common Performance Issues

  • N+1 query problems
  • Inefficient lazy loading
  • Missing indexes
  • Poor query design
  • Lack of caching

N+1 Query Problem

The N+1 problem occurs when you fetch a list of entities and then access their related entities, causing additional queries.

Problem Example

java
@Entity
public class Author {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;
}

@Entity
public class Book {
    @Id
    private Long id;
    private String title;
    
    @ManyToOne
    private Author author;
}

// This causes N+1 queries
List<Author> authors = authorRepository.findAll();
for (Author author : authors) {
    System.out.println(author.getBooks().size()); // Triggers additional query
}

Solutions

1. Use @EntityGraph

java
@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
    
    @EntityGraph(attributePaths = {"books"})
    List<Author> findAllWithBooks();
    
    @EntityGraph(attributePaths = {"books", "books.publisher"})
    List<Author> findAllWithBooksAndPublisher();
}

2. Use JOIN FETCH in JPQL

java
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllAuthorsWithBooks();

@Query("SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books b LEFT JOIN FETCH b.publisher")
List<Author> findAllAuthorsWithBooksAndPublisher();

3. Use Projections

java
public interface AuthorBookProjection {
    Long getId();
    String getName();
    List<BookProjection> getBooks();
    
    interface BookProjection {
        Long getId();
        String getTitle();
    }
}

@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<AuthorBookProjection> findAllAuthorsWithBooksProjection();

Lazy Loading Strategies

Understanding Fetch Types

java
@Entity
public class Order {
    @Id
    private Long id;
    
    // Eager loading - always fetched
    @OneToMany(fetch = FetchType.EAGER)
    private List<OrderItem> items;
    
    // Lazy loading - fetched when accessed
    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;
}

Batch Fetching

java
@Entity
@BatchSize(size = 10)
public class Author {
    @Id
    private Long id;
    
    @OneToMany(mappedBy = "author")
    @BatchSize(size = 5)
    private List<Book> books;
}

Subselect Fetching

java
@Entity
public class Department {
    @OneToMany(mappedBy = "department")
    @Fetch(FetchMode.SUBSELECT)
    private List<Employee> employees;
}

Query Optimization

Custom Queries with Pagination

java
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    
    @Query(value = "SELECT b FROM Book b WHERE b.publishedDate >= :date",
           countQuery = "SELECT count(b) FROM Book b WHERE b.publishedDate >= :date")
    Page<Book> findRecentBooks(@Param("date") LocalDate date, Pageable pageable);
    
    @Query("SELECT new com.example.dto.BookSummary(b.id, b.title, a.name) " +
           "FROM Book b JOIN b.author a WHERE b.genre = :genre")
    List<BookSummary> findBookSummariesByGenre(@Param("genre") String genre);
}

Native Queries for Complex Operations

java
@Query(value = "SELECT * FROM books b " +
               "WHERE b.rating > :rating " +
               "AND b.published_date BETWEEN :startDate AND :endDate " +
               "ORDER BY b.rating DESC, b.published_date DESC",
       nativeQuery = true)
List<Book> findTopRatedBooksInPeriod(@Param("rating") Double rating,
                                     @Param("startDate") LocalDate startDate,
                                     @Param("endDate") LocalDate endDate);

Bulk Operations

java
@Modifying
@Query("UPDATE Book b SET b.price = b.price * 1.1 WHERE b.genre = :genre")
int increasePriceByGenre(@Param("genre") String genre);

@Modifying
@Query("DELETE FROM Book b WHERE b.publishedDate < :date")
int deleteOldBooks(@Param("date") LocalDate date);

Caching Strategies

Second-Level Cache

java
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "category")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private List<Product> products;
}

Query Result Cache

java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    @QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
    @Query("SELECT p FROM Product p WHERE p.featured = true")
    List<Product> findFeaturedProducts();
}

Spring Cache Abstraction

java
@Service
@Transactional
public class ProductService {
    
    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) {
        return productRepository.findById(id).orElse(null);
    }
    
    @CacheEvict(value = "products", key = "#product.id")
    public Product save(Product product) {
        return productRepository.save(product);
    }
    
    @CacheEvict(value = "products", allEntries = true)
    public void clearCache() {
        // Method implementation
    }
}

Performance Monitoring

Enable SQL Logging

properties
# Show SQL queries
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Show parameter values
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# Show statistics
spring.jpa.properties.hibernate.generate_statistics=true

Custom Performance Interceptor

java
@Component
public class QueryCountInterceptor implements Interceptor {
    private static final ThreadLocal<Integer> queryCount = new ThreadLocal<>();
    
    public static void startCounter() {
        queryCount.set(0);
    }
    
    public static int getQueryCount() {
        return queryCount.get() == null ? 0 : queryCount.get();
    }
    
    public static void clear() {
        queryCount.remove();
    }
    
    @Override
    public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        incrementCounter();
        return false;
    }
    
    private void incrementCounter() {
        Integer count = queryCount.get();
        if (count == null) {
            count = 0;
        }
        queryCount.set(count + 1);
    }
}

Performance Testing

java
@Test
public void testQueryPerformance() {
    QueryCountInterceptor.startCounter();
    
    List<Author> authors = authorRepository.findAllWithBooks();
    
    int queryCount = QueryCountInterceptor.getQueryCount();
    assertThat(queryCount).isLessThanOrEqualTo(1); // Should be 1 query with JOIN FETCH
    
    QueryCountInterceptor.clear();
}

Database Indexing

JPA Index Annotations

java
@Entity
@Table(indexes = {
    @Index(name = "idx_book_title", columnList = "title"),
    @Index(name = "idx_book_author_genre", columnList = "author_id, genre"),
    @Index(name = "idx_book_published_date", columnList = "published_date")
})
public class Book {
    @Id
    private Long id;
    
    @Column(length = 255)
    private String title;
    
    @Column(length = 100)
    private String genre;
    
    @Column(name = "published_date")
    private LocalDate publishedDate;
    
    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
}

Composite Indexes

java
@Entity
@Table(indexes = {
    @Index(name = "idx_order_customer_date", 
           columnList = "customer_id, order_date DESC"),
    @Index(name = "idx_order_status_date", 
           columnList = "status, order_date")
})
public class Order {
    // Entity fields
}

Connection Pool Optimization

HikariCP Configuration

properties
# Connection pool settings
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.leak-detection-threshold=60000

Best Practices Summary

1. Entity Design

  • Use appropriate fetch types
  • Implement equals() and hashCode() properly
  • Use @BatchSize for collections
  • Consider using DTOs for read operations

2. Query Optimization

  • Use projections for read-only operations
  • Implement pagination for large datasets
  • Use native queries for complex operations
  • Avoid N+1 problems with JOIN FETCH

3. Caching Strategy

  • Enable second-level cache for reference data
  • Use query result cache for expensive queries
  • Implement application-level caching with Spring Cache

4. Monitoring and Testing

  • Enable SQL logging in development
  • Monitor query counts and execution times
  • Write performance tests
  • Use database profiling tools

Conclusion

Database optimization in Spring Data JPA requires understanding of JPA internals, proper entity design, and strategic use of caching. By following these techniques, you can significantly improve your application's performance:

  • Eliminate N+1 query problems
  • Use appropriate loading strategies
  • Implement effective caching
  • Monitor and measure performance
  • Design proper database indexes

Remember that optimization is an iterative process. Always measure before and after implementing changes to ensure they provide the expected performance improvements.

Backend developer specializing in Java and Spring Boot. Building scalable, reliable systems that power modern applications. This broadsheet is hand-set in Caslon and Franklin.

The desk

Tech stack

  • Java & Spring Boot
  • MySQL & PostgreSQL
  • Docker & Microservices
  • JWT & OAuth2
Case closed

System up since 2021 · Building backend systems · Learning new technologies · Contributing to open source

© 2026 Utsab Dahal · All rights reserved · Printed in Kathmandu