10. Scenario-Based JPA Questions
T
Tuan Nguyen

10. Scenario-Based JPA Questions

This section focuses on real JPA problems that often appear in backend interviews, including lazy loading, N+1 queries, transactions, cascading, concurrency, pagination, rollback issues, and production database behavior.

1. You call findById(), change the entity field, but do not call save(). Will the database update?

Yes, if the entity is managed and the change happens inside an active transaction.

Example:

@Transactional
public void updateUser(Long id) {
    User user = userRepository.findById(id).orElseThrow();
    user.setName("John");
}

Here, findById() loads the entity into the persistence context. Because the entity is managed, Hibernate tracks its state. When the transaction commits, Hibernate performs dirty checking and detects that name changed.

Then Hibernate sends an UPDATE query automatically.

You do not need to call save() again for a managed entity.

However, if the entity is detached or there is no transaction, the database may not be updated.


2. You get LazyInitializationException. What happened?

LazyInitializationException happens when Hibernate tries to load lazy data after the persistence context is already closed.

Example:

User user = userRepository.findById(id).orElseThrow();
return user.getOrders();

If orders is lazy and the transaction/session is closed, Hibernate cannot load it anymore.

This often happens when returning entities directly from controllers.

Fixes include:

Use DTO projection
Use fetch join
Use @EntityGraph
Access lazy data inside transaction
Avoid returning entities directly from API

Best production approach is usually DTO-based response design.


3. Your API returns infinite JSON recursion. Why?

This happens because of bidirectional entity relationships.

Example:

User has many Orders
Order has one User

When Jackson serializes User, it includes orders.

Then each Order includes user.

Then that User includes orders again.

Result:

User → Orders → User → Orders → User...

This causes infinite recursion.

The root problem is exposing entity relationships directly in JSON.

Best fix is to return DTOs instead of entities.


4. You have User -> Orders -> User -> Orders in JSON. How do you fix it?

Best solution: use DTOs.

Example:

public record UserResponse(
    Long id,
    String name,
    List<OrderResponse> orders
) {}

The DTO controls exactly what the API returns.

Other possible fixes:

@JsonIgnore
@JsonManagedReference
@JsonBackReference
@JsonIdentityInfo

But these are usually serialization-level fixes.

For clean backend design, DTOs are better because entities should model database relationships, while DTOs should model API responses.


5. Your endpoint is very slow. How do you check if JPA causes N+1?

Enable SQL logging and inspect generated queries.

Example configuration:

spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE

N+1 usually looks like this:

SELECT * FROM users;
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
SELECT * FROM orders WHERE user_id = 3;

One query loads users, then one extra query loads orders for each user.

For 100 users, you may get 101 queries.

Fixes:

Fetch join
@EntityGraph
Batch fetching
DTO projection

6. You use FetchType.EAGER everywhere. What problem can happen?

Using FetchType.EAGER everywhere can make your application slow and unpredictable.

EAGER means Hibernate loads related data immediately.

Example:

@ManyToOne(fetch = FetchType.EAGER)
private Department department;

Problems:

Too much data loaded
Large SQL joins
Poor performance
Unexpected queries
Memory usage increases
Difficult query control

EAGER can also create complex object graphs that are expensive to load.

Best practice:

Prefer LAZY by default.
Fetch only what you need per use case.

Use fetch join, EntityGraph, or DTO projection when related data is required.


7. You delete a user and accidentally delete all roles. Why?

This usually happens because cascading is configured incorrectly.

Example dangerous mapping:

@ManyToMany(cascade = CascadeType.ALL)
private Set<Role> roles;

If User and Role have many-to-many relationship, roles are usually shared by many users.

When deleting one user, CascadeType.ALL may also cascade remove operations to roles.

That can delete shared roles such as:

ADMIN
USER
MANAGER

This is dangerous.

For many-to-many relationships, avoid CascadeType.REMOVE and be very careful with CascadeType.ALL.


8. You use CascadeType.ALL on many-to-many. Why is it dangerous?

CascadeType.ALL includes:

PERSIST
MERGE
REMOVE
REFRESH
DETACH

The dangerous part is REMOVE.

In many-to-many relationships, both sides are often independent entities.

Example:

User A has Role ADMIN
User B also has Role ADMIN

If deleting User A cascades remove to Role ADMIN, then User B loses a shared role or the role row may be deleted.

Better approach:

Remove only the join table relationship.
Do not delete the shared entity.

Usually, many-to-many should not use CascadeType.ALL.


9. You update product stock and two users buy at same time. How do you prevent wrong stock?

This is a concurrency problem.

Example:

Stock = 1

User A reads stock = 1
User B reads stock = 1
Both buy
Stock becomes incorrect

Solutions:

Optimistic locking

Use version column:

@Version
private Long version;

If two transactions update the same row, one succeeds and the other fails with optimistic locking exception.

Pessimistic locking

Lock the row during transaction:

@Lock(LockModeType.PESSIMISTIC_WRITE)

This prevents another transaction from updating the same product stock at the same time.

Atomic database update

UPDATE product
SET stock = stock - 1
WHERE id = ? AND stock > 0;

Then check affected rows.

For inventory systems, atomic update or locking is commonly used.


10. You use pagination with fetch join and get wrong results. Why?

Pagination with collection fetch join can produce incorrect results because SQL joins multiply rows.

Example:

User 1 has 5 orders
User 2 has 3 orders

A query joining users and orders produces multiple rows per user.

When pagination is applied at SQL row level, the database paginates joined rows, not distinct parent entities.

This can cause:

Missing users
Duplicate users
Wrong page size
Memory pagination
Performance issues

Better solutions:

Use two-step query
First page parent IDs
Then fetch relationships by IDs
Use DTO projection
Use batch fetching

11. You use @Transactional but rollback does not happen. Why?

By default, Spring rolls back only for unchecked exceptions.

Rollback happens for:

RuntimeException
Error

But not automatically for checked exceptions.

Example:

@Transactional
public void process() throws IOException {
    throw new IOException();
}

This may not rollback unless configured:

@Transactional(rollbackFor = IOException.class)

Other reasons rollback may not happen:

Exception is caught and not rethrown
Method is not public
Self-invocation issue
Transactional proxy is bypassed
Database operation already committed

12. You call a transactional method inside the same class and it does not work. Why?

Spring @Transactional works through proxies.

Example:

public void methodA() {
    methodB();
}

@Transactional
public void methodB() {
}

If methodA() and methodB() are in the same class, the call does not go through the Spring proxy.

It is just a normal Java method call.

Therefore, transaction logic is not applied.

Fixes:

Move transactional method to another service
Call through Spring proxy
Redesign service boundaries

Best practical solution is usually moving the transactional operation into another Spring bean.


13. You use save() and Hibernate sends too many queries. Why?

This may happen because save() internally decides whether to persist or merge.

For detached entities, Spring Data JPA may call merge().

merge() may trigger extra SELECT queries because Hibernate needs to check existing database state and copy detached object data into a managed entity.

Other reasons:

Cascading relationships
EAGER fetching
Dirty checking many entities
N+1 queries
Unnecessary save() on managed entities

If an entity is already managed inside a transaction, changing fields is enough. Calling save() again is often unnecessary.


14. You load 10 users and each user has 10 orders. How many queries can happen?

If lazy loading causes N+1:

1 query loads 10 users
10 additional queries load orders for each user

Total:

11 queries

Example:

SELECT * FROM users;
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
...
SELECT * FROM orders WHERE user_id = 10;

Even though there are 100 orders, the number of queries is based on users loaded.

Fix with:

Fetch join
@EntityGraph
Batch fetching
DTO projection

15. You return entity directly from controller and get lazy loading error. Why?

The controller returns the entity after the service transaction has ended.

Example:

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

If Jackson tries to serialize a lazy field after the transaction is closed, Hibernate cannot load it.

Result:

LazyInitializationException

Better design:

Service loads required data inside transaction
Map entity to DTO
Controller returns DTO

Returning entities directly also risks exposing internal database structure.


16. You want to update only one field. Should you use PUT or PATCH?

Use PATCH when updating only part of a resource.

Example:

PATCH /users/1

Body:

{
  "name": "John"
}

Use PUT when replacing the whole resource.

Example:

PUT /users/1

Body should represent the full updated user.

In practice:

PATCH = partial update
PUT = full replacement

For one-field update, PATCH is usually better.


17. You want to prevent duplicate email. Should you only check in Java?

No.

Checking in Java is not enough.

Example:

if (userRepository.existsByEmail(email)) {
    throw new DuplicateEmailException();
}

This check can still fail under concurrency.

Two requests may check at the same time:

Request A: email does not exist
Request B: email does not exist
Both insert same email

The database must enforce uniqueness:

ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE(email);

Best design:

Check in application for user-friendly error
Enforce unique constraint in database for correctness
Handle database constraint violation

18. Your app works locally but fails in production database. Why can H2 hide problems?

H2 is not the same as PostgreSQL, MySQL, Oracle, or SQL Server.

H2 may behave differently with:

SQL syntax
Data types
Indexes
Constraints
Reserved keywords
Transaction behavior
Locking
JSON columns
Date/time handling

A query that works in H2 may fail in PostgreSQL.

Example:

Using a reserved keyword as table name

H2 may allow it, production DB may reject it.

Better approach:

Use Testcontainers with the same database engine as production
Run migration scripts in CI
Avoid relying only on H2 for integration tests

19. Your migration fails in production. What should you do?

First, do not blindly edit production manually.

A safe approach:

Stop the deployment if needed
Check migration logs
Identify failed migration
Check database state
Restore from backup if needed
Fix migration script
Test fix on staging
Re-run carefully

Good migration practices:

Use Flyway or Liquibase
Test migrations before production
Make migrations backward compatible
Avoid destructive changes without backup
Use transactions when supported
Have rollback plan

Migration failure is serious because database state may be partially changed.


20. Your API receives the same payment request twice. How do you prevent double charge?

Use an idempotency key.

Client sends:

Idempotency-Key: abc-123

Backend stores this key with the request result.

Flow:

First request:
Process payment
Store idempotency key and response

Second request with same key:
Return previous response
Do not charge again

Example table:

idempotency_records (
    idempotency_key VARCHAR UNIQUE,
    request_hash VARCHAR,
    response_status INT,
    response_body JSON,
    created_at TIMESTAMP
)

Also store payment provider transaction ID.

For payment systems, idempotency is essential because retries can happen due to network timeout, client retry, or user double-click.

Comments