Scenario Questions

The "My Orders" API returns 20 orders but the logs show 41 queries. What do you do?

  1. Confirm with SQL logging or statistics: 1 query for orders + 20 for users + 20 for items = N+1 on two associations.
  2. Write a dedicated query: join fetch o.user plus @EntityGraph or batch fetching for items (avoid fetching two Lists in one query).
  3. If the screen needs only a few fields, return a DTO projection instead.
  4. 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 @Version and 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.