Spring Data JPA & Hibernate — Interview Q&A Matrix
Parent stack: Spring Ecosystem Technology: Spring Data JPA & Hibernate Levels: Junior · Senior · Architect Questions: 4
Each entry contains a Question, the Expected Depth, an Ideal Answer, and Red Flags.
Junior
Q1 — Spring Data JPA repositories
Question: How do Spring Data JPA repositories work, and how would you fetch active users by email domain?
Expected Depth:
- Explains repository proxies and the purpose of
JpaRepository. - Uses derived queries for simple filters and parameterized JPQL for clearer complex queries.
- Knows that JPQL uses entity and field names, not table and column names.
- Chooses between entities, interface projections, DTO projections,
Optional, and pages. - Understands transaction boundaries and avoids exposing managed entities as API contracts.
Ideal Answer:
- Extend
JpaRepository<User, Long>. Spring Data creates a proxy implementation at runtime and provides CRUD, paging, and sorting operations. - Use a derived query when the name stays short and clear. Use parameterized JPQL when the query needs joins, projections, or several conditions:
public interface UserRepository extends JpaRepository<User, Long> {
List<UserSummary> findByActiveTrueAndEmailEndingWithIgnoreCase(
String domainSuffix);
@Query("""
select new com.example.UserSummary(u.id, u.email)
from User u
where u.active = true
and lower(u.email) like lower(concat('%@', :domain))
order by u.email
""")
List<UserSummary> findActiveByDomain(@Param("domain") String domain);
}- A DTO projection reads only the fields needed by the use case and avoids exposing a managed entity:
public record UserSummary(Long id, String email) {}- Return
Optional<User>for zero-or-one results. UsePageablefor large result sets instead of loading every row:
Page<UserSummary> findByActiveTrue(Pageable pageable);- Keep repository access inside a service transaction when several reads or writes form one operation. Map entities to API DTOs before leaving the service layer.
Red Flags:
- Builds JPQL or SQL by concatenating user input.
- Uses a very long derived method name that nobody can review safely.
- Returns every row as a
Listwith no page or limit. - Exposes JPA entities directly as public API models.
- Assumes that JPQL uses physical table and column names.
Senior
Q2 — Solving the N+1 query problem
Question: A list endpoint issues one query for orders then one per order for its line items. Diagnose and fix the N+1 problem while avoiding a Cartesian explosion.
Expected Depth:
- Identifies N+1 by counting SQL statements, not only by looking at Java code.
- Explains how lazy access and some eager-loading plans can create extra queries.
- Chooses among fetch joins, entity graphs, DTO projections, and batch fetching.
- Understands collection fetch joins, Cartesian multiplication, multiple-bag limits, and duplicate parent rows.
- Handles pagination with a two-query approach instead of paginating a collection fetch join.
- Defines transaction boundaries and does not depend on Open Session in View to hide lazy loads.
Ideal Answer:
- N+1 means one query loads the parent rows and then additional queries load associations for each parent. It often appears when application code accesses a lazy collection in a loop.
EAGERdoes not guarantee one SQL query and can also create N+1 behavior. - For a small, bounded result, use a fetch join or entity graph:
@Query("""
select distinct o
from Order o
left join fetch o.items
where o.status = :status
""")
List<Order> findWithItems(@Param("status") Status status);- Do not combine normal pagination with a collection fetch join. Hibernate may need to load many rows and paginate in memory. Page the order IDs first, then fetch the required graph:
@Query("""
select o.id from Order o
where o.status = :status
order by o.createdAt desc
""")
Page<Long> findPageIds(
@Param("status") Status status,
Pageable pageable);
@Query("""
select distinct o from Order o
left join fetch o.items
where o.id in :ids
""")
List<Order> findWithItemsByIdIn(Collection<Long> ids);- Batch fetching reduces many association queries to a smaller number of
INqueries:
spring.jpa.properties.hibernate.default_batch_fetch_size=50- A DTO query is often better for a read-only endpoint because it loads exactly the required shape and avoids managed collection behavior.
- Verify the fix with a SQL statement counter in an integration test. SQL logging is useful locally, but structured datasource metrics are safer in production.
- Consider disabling Open Session in View so lazy database access does not happen during JSON serialization:
spring.jpa.open-in-view=falseRed Flags:
- Changes every association to
EAGER. - Fetch-joins several large collections and ignores Cartesian multiplication.
- Paginates a collection fetch join without checking generated SQL and memory use.
- Keeps Open Session in View only to hide lazy-loading problems.
- Claims the issue is fixed without counting SQL statements.
Q3 — Optimistic locking & concurrent updates
Question: Two users update the same account balance concurrently and one overwrites the other (lost update). How do you prevent this with JPA?
Expected Depth:
- Explains lost updates and how
@Versionchanges the generatedUPDATE. - Retries the full business transaction with fresh state, not only
save(). - Runs each retry in a new transaction and avoids repeating external side effects.
- Compares optimistic and pessimistic locking using measured contention.
- Uses lock timeouts and keeps pessimistic transactions short.
- Knows when an atomic database update is simpler than loading an entity.
Ideal Answer:
- Add a
@Versionfield. Hibernate includes the old version in theUPDATEcondition and increments it. If another transaction changed the row, zero rows are updated and JPA throws an optimistic-lock exception:
@Entity
class Account {
@Id
private Long id;
@Version
private long version;
private BigDecimal balance;
}- On conflict, retry the whole operation: start a new transaction, reload the account, validate the business rule, and apply the change again. Do not continue with the failed persistence context.
- Keep external calls outside the retried transaction. Otherwise, a retry may send a payment or email twice.
- For high contention, a pessimistic lock may be clearer:
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Account a where a.id = :id")
Optional<Account> findForUpdate(@Param("id") Long id);
}- Keep the transaction short and configure a lock timeout so requests fail instead of waiting forever.
- For a simple numeric change, an atomic SQL update can avoid the read-modify-write race:
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update Account a
set a.balance = a.balance - :amount,
a.version = a.version + 1
where a.id = :id and a.balance >= :amount
""")
int debitIfEnoughFunds(
@Param("id") Long id,
@Param("amount") BigDecimal amount);- Bulk JPQL bypasses normal entity state tracking and callbacks, so clear the persistence context and update the version explicitly. Check the affected-row count. Zero means the account was missing or had insufficient funds.
Red Flags:
- Reads and writes shared state with no version or lock.
- Catches an optimistic-lock exception and retries
save()on the same stale entity. - Retries inside the same failed transaction.
- Repeats an external side effect on every optimistic retry.
- Uses pessimistic locks for every query without measuring contention.
- Holds a database lock while calling another service.
Architect
Q4 — Persistence architecture: read/write splitting and CQRS boundaries
Question: A read-heavy platform saturates the primary DB. Design a persistence architecture using Spring that scales reads without sacrificing consistency where it matters.
Expected Depth:
- Routes transactions explicitly and acquires the connection only after routing context is known.
- Understands that
@Transactional(readOnly = true)is a routing hint, not a security boundary. - Handles replication lag, read-your-writes, failover, and replica health.
- Sizes separate primary and replica pools across all application instances.
- Introduces CQRS only when query models and scaling needs justify eventual consistency.
- Uses an outbox and idempotent projectors for reliable read-model updates.
Ideal Answer:
- Use replicas for reads that can tolerate delay. Keep writes and consistency-sensitive reads on the primary.
- A routing datasource can inspect the transaction's read-only flag:
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "replica"
: "primary";
}
}- Wrap the router with
LazyConnectionDataSourceProxyso Spring chooses a physical connection after the transaction attributes are available. Test this behavior because early connection acquisition can route incorrectly. readOnly = trueis normally a hint. Do not rely on it to prevent writes. Use database permissions, separate credentials, and tests so the replica datasource cannot perform application writes.- Read-your-writes paths, such as "show the order I just created," must use the primary or a consistency token that waits until a replica catches up. Dashboards and search often accept bounded staleness.
- Monitor replica lag and health. Stop routing to a stale or failed replica. Define behavior during primary failover and avoid writing through a connection that still points to the old primary.
- Use separate bounded pools and count the total connections across every service instance.
- Add CQRS only when read queries need different models, independent scaling, or precomputed data. Publish changes reliably through a transactional outbox and update the read model with idempotent consumers:
Primary transaction -> outbox event -> projector -> read model- Store projector offsets or event IDs, monitor projection lag, and provide a rebuild process. Document how stale each read API may be.
Red Flags:
- Sends every read to a replica and ignores lag and read-your-writes behavior.
- Assumes that
readOnly = trueprevents all writes. - Acquires a connection before the routing context is available.
- Uses the same unrestricted database credentials for primary and replica routing.
- Adopts CQRS for simple CRUD with no clear benefit.
- Has no projection-lag metric, idempotency, replay, or rebuild plan.
Scoring Rubric
| Level | Pass bar |
|---|---|
| Junior | Uses repositories, parameters, projections, and paging correctly. |
| Senior | Diagnoses query and concurrency problems and fixes them with safe transaction boundaries. |
| Architect | Designs replica and CQRS strategies with explicit consistency and recovery rules. |