Scenario Questions
The "My Orders" API returns 20 orders but the logs show 41 queries. What do you do?
- Confirm with SQL logging or statistics: 1 query for orders + 20 for users + 20 for items = N+1 on two associations.
- Write a dedicated query:
join fetch o.userplus@EntityGraphor batch fetching foritems(avoid fetching two Lists in one query). - If the screen needs only a few fields, return a DTO projection instead.
- Add a test that asserts the query count so it does not regress.
Two customers buy the last unit of a product at the same moment and stock becomes -1. How do you fix it?
Classic lost update: both read stock = 1, both write 0 or -1. Options:
- Atomic conditional update (simplest, scales well): check that exactly 1 row changed.
- Optimistic locking with
@Versionand a retry on conflict. - Pessimistic lock (
PESSIMISTIC_WRITE) on the product row during checkout. - Plus a DB
CHECK (stock >= 0)constraint as a safety net.
@Modifying
@Query("update Product p set p.stock = p.stock - :qty where p.id = :id and p.stock >= :qty")
int reserveStock(@Param("id") Long id, @Param("qty") int qty);
if (productRepository.reserveStock(id, qty) == 0) throw new OutOfStockException(id);A service method changes a managed entity and the change is saved even though save() was never called. Why?
Dirty checking. Inside a transaction the entity is managed; Hibernate keeps a snapshot of its loaded state and at flush compares every managed entity to its snapshot, issuing UPDATEs for the changed ones. To avoid accidental writes, use readOnly = true, detach the entity, or work with DTOs. (See the simulation in the Coding Round.)
An entity added to a HashSet cannot be found after it is saved. Why?
Its equals/hashCode use the generated id. Before persist the id is null, so the object sits in the hash bucket for null; after the id is assigned its hashCode changes and the set looks in a different bucket. Fix: compare by id in equals (null ids never equal) and return a constant hashCode (e.g. getClass().hashCode()), or use a natural business key such as sku. Avoid Lombok @Data/@EqualsAndHashCode on entities.
Returning an entity from a REST controller causes a StackOverflowError or huge JSON. Why?
A bidirectional relation (Order.items ↔ OrderItem.order) makes Jackson (and Lombok toString) recurse forever, and lazy associations trigger extra queries or LazyInitializationException during serialization. Fix: return DTOs (records) from the API. Quick patches: @JsonManagedReference/@JsonBackReference or @JsonIgnore, and exclude associations from toString.
A nightly job importing 1 million rows runs out of memory and takes hours. How do you fix it?
- The persistence context keeps every persisted entity plus its snapshot: flush and clear every 50–1000 rows, or use a
StatelessSession. - Turn on JDBC batching and ordered inserts; switch ids from IDENTITY to SEQUENCE with a pooled optimizer.
- Commit in chunks (e.g. Spring Batch chunk-oriented steps) so one failure does not roll back everything.
- Read input as a stream; for pure loads consider database bulk tools (
COPY,LOAD DATA).
@Transactional on placeOrder() does not roll back when the payment step fails. What could be wrong?
Check, in order: the method is called from inside the same class (self-invocation, no proxy); the payment client throws a checked exception (add rollbackFor); the exception is caught and logged instead of rethrown; the method is private; or a REQUIRES_NEW inner method already committed its own transaction. Also remember a remote payment call cannot be rolled back by the DB: use a status (PENDING → PAID/FAILED) and compensation.