Loading...
Loading...
Backend interview guide
Entities, relationships, repositories, queries and transactions. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
I default to List with @OrderBy, initialised to an empty ArrayList. A Set needs reliable equals and hashCode on the entities, which is tricky because ids are assigned when they are saved. Set is the better choice for @ManyToMany, because of how Hibernate updates a List there.
It is the field whose table holds the foreign key, the side without mappedBy. Hibernate reads only that field when it writes the column; the mappedBy side is ignored for saving.
Yes, with a unidirectional @OneToMany and either a join table or @JoinColumn. But it is costly: a join table nobody needs, or an INSERT followed by an extra UPDATE per child, which fails on NOT NULL columns. I prefer @ManyToOne on the child and mappedBy on the list.
At startup. Hibernate checks every mappedBy when it builds the EntityManagerFactory, so the app fails before serving a request, with a message naming the collection and the missing property. It is one of the safer mistakes because it can never reach production silently.
Because the rest of the transaction uses the Java objects, not the database. Totals, validations and the response are computed from the stale list, so a user can get an invoice or a confirmation with wrong numbers even though the rows are correct.
Lesson: Keeping both sides in sync with helper methodsUsually toString, equals or hashCode that include a linked entity on both sides of a bidirectional link, so they call each other forever. Lombok's @Data or @ToString on entities does exactly that. I include only plain fields and ids.
No. The INSERT or UPDATE comes from the owning side, which the helper sets anyway. The helper only keeps the in-memory objects consistent, which is what the rest of the business logic reads.
Lesson: Keeping both sides in sync with helper methodsAs soon as the link has data of its own, such as a quantity, a price or a date. Then I map the link table as a join entity with two @ManyToOnes, like OrderItem between orders and products. In practice that is most many-to-many links, so I start with a join entity when the data is likely to come.
It tells Hibernate that a @ManyToOne or @OneToOne fills (part of) the primary key, so the same column is both a foreign key and the id. With an @EmbeddedId, @MapsId("studentId") names which part of the id the link fills.
With @OneToOne(fetch = LAZY) and @JoinColumn(unique = true) on the entity whose table has the foreign key, backed by a UNIQUE constraint in the database. If the child's primary key is the parent's id, I use @MapsId instead. I add the mappedBy side only when I really need it.
It makes a @OneToOne or @ManyToOne fill the entity's primary key, so the child shares the parent's id. I use it when the child cannot exist without the parent, like a loyalty card or a profile: one column is both key and foreign key, and I read it with findById(parentId).
With a UNIQUE constraint on the foreign key column, or by making the foreign key the primary key. The JPA annotation unique = true only describes it; MySQL's constraint is what blocks a second wallet, even from two requests at once.
PERSIST, MERGE, REMOVE, REFRESH, DETACH and ALL. I use ALL, usually with orphanRemoval, only on a parent's collection of children it owns, like order items. On links to shared entities I use no cascade.
Lesson: Cascade types and orphanRemovalA saved entity has a link to an object created with new that Hibernate never saved, so there is no id to write into the foreign key. In Hibernate 7 it is TransientPropertyValueException with "references an unsaved transient instance". I fix it by loading the existing entity with findById or getReferenceById, not by adding a cascade on the child side.
No. The other side holds shared rows, like courses taken by many students, so deleting one student would try to delete shared courses: either a foreign key error or silent data loss. On @ManyToMany I use no cascade, or PERSIST and MERGE only.
Whenever the link has data of its own. order_items connects orders and products but also stores quantity and the unit price on the order day, so it is an OrderItem entity with two @ManyToOnes. @ManyToMany is only for pure links such as tags, and then with a Set.
Because the default is EAGER and cannot be switched off per query, so every load of a product would also load its category, often with one extra SELECT per category. With LAZY I load links only where a use case needs them, with a fetch join or a DTO query.
Lesson: Practice: relationshipsEntities never leave the service. Each service method runs in a read-only transaction and maps entities to response records with exactly the contract's fields and no back-links, so Jackson never follows a bidirectional link and nothing lazy is touched after the session closes.
Lesson: Practice: relationshipsTransient (new, not tracked), managed (tracked), detached (has a row, not tracked) and removed (to be deleted). persist and find make objects managed, detach, clear or the end of the transaction detach them, merge returns a managed copy of a detached object, and remove marks a managed object for deletion at commit.
One SELECT, and the same object both times, because the second call is answered from the persistence context. Without a surrounding transaction each repository call has its own context, so you get two SELECTs and two different objects that are equal by id but not ==.
Yes. Spring injects a shared proxy, not a real EntityManager. Each call goes to the EntityManager bound to the current thread's transaction, so concurrent requests never share one.
Lesson: Transient, managed, detached and removedIt is the flush step where Hibernate compares every managed entity with the snapshot of values it took at load time and sends an UPDATE for each entity that changed. Because of it, a service updates a row just by changing a managed object inside a transaction.
Lesson: Dirty checking: updates without save()No, it compares values. Setting a field to its current value, or changing it and back, gives no UPDATE. That is also why direct field changes inside the entity's own methods are detected.
Lesson: Dirty checking: updates without save()For a Hibernate-backed transaction Spring makes the session read-only and stops automatic flushing, so no snapshots are kept and no UPDATE is sent. I put it on every method that only reads: it saves memory and blocks accidental writes from mappers.
Lesson: Dirty checking: updates without save()Flush sends the pending SQL to the database inside the current transaction; commit ends the transaction and makes the changes permanent and visible. After a flush the changes can still be rolled back. In Spring, a commit always flushes first.
Lesson: Flush: when SQL is actually sentBecause of write-behind: an UPDATE (or a DELETE) for a managed entity is only sent at the flush, which normally happens at commit. If the error must be handled in the service, flush inside the method and catch DataIntegrityViolationException there.
Not for IDENTITY ids: each INSERT must run alone to return its generated key, so hibernate.jdbc.batch_size only helps updates and deletes. For bulk inserts I would use JDBC batch inserts, or sequence-based ids on a database that has sequences.
It checks whether the entity is new. If so it calls EntityManager.persist and returns the same object; otherwise it calls merge and returns the managed copy merge gives back. By default "new" means the id is null, unless the entity has a wrapper @Version field or implements Persistable.
When the id is assigned by the application, such as a coupon code, the id is never null, so Spring Data treats the entity as not new and merges it. merge must SELECT to see if the row exists. Implementing Persistable or adding a @Version field fixes it.
Spring Boot 4 uses Hibernate 7, which removed save, update and saveOrUpdate from Session. I replace them with persist for new objects and merge for detached ones, or better, with repository save for new entities and load-and-change for updates.
The controller takes a request DTO with nullable fields. A @Transactional service loads the entity by id (404 if missing), validates rules, sets only the fields present, and returns a response DTO built inside the transaction. Dirty checking sends the UPDATE at commit, so there is no save() call.
EAGER applies to every query that loads the entity and cannot be turned off per query. It loads data nobody asked for and often causes N+1 selects on lists. Keeping associations LAZY and fetching per use case gives control.
Lesson: LazyInitializationException and why it happensIt lets lazy loading outside a transaction work by opening a temporary session for each access. It hides the exception but adds one query and connection per access, outside any transaction, so I never use it.
Lesson: LazyInitializationException and why it happensBoth load associations in the same query. @EntityGraph keeps derived query methods and uses left joins; JPQL join fetch gives full control but is an inner join unless I write left join fetch. I pick per method, and I am careful with collection fetches combined with pagination.
No. I load the entity inside a @Transactional method and change it; dirty checking writes it at commit. I call save for new entities, and saveAndFlush only when I need a constraint error at a known point.
The UPDATE was not sent inside the try block: write-behind delayed it to the flush at commit, after the method returned. Flushing inside the try block, with flush() or saveAndFlush, moves the error there.
@Transactional, and why not on the controller?On service methods, because a service method is one business operation and every caller (controller, job, test) gets the same boundary. On a controller it would hold a database connection during HTTP work and leave other callers without a transaction.
Lesson: @Transactional on service methodsSpring Boot auto-configures a JpaTransactionManager when the Data JPA starter is present. It opens an EntityManager, starts the JDBC transaction through Hibernate and binds both to the current thread, so repository calls inside the method share them.
save() after changing an entity inside a transactional method?The entity is managed by the persistence context that lives as long as the transaction. At commit Hibernate flushes: it compares each entity with its snapshot and sends an UPDATE for what changed.
Lesson: @Transactional on service methodsRuntimeException and its subclasses, and Error. Checked exceptions commit by default; the rule is inherited from EJB, where a checked exception counted as an expected application outcome.
UnexpectedRollbackException and when do you see it?It is thrown when a method returns normally but its transaction can only roll back. Typically an inner @Transactional method that joined the transaction threw, which marked the transaction rollback-only, and the outer method caught the exception and carried on.
They roll back automatically, they do not force throws clauses through controllers and services, and one global handler maps each type to a ProblemDetail status. Checked exceptions from libraries are translated into ShopLite exceptions at the edge.
@Transactional(readOnly = true) actually do?It starts a transaction flagged read-only. With Hibernate, the session uses flush mode MANUAL and read-only entities, so no snapshots are kept and nothing is flushed. Spring also marks the JDBC connection read-only, so the MySQL driver refuses data-changing statements.
Lesson: Read-only transactionsYes. readOnly applies only to a transaction the method starts. If a read-write transaction calls it, it joins that transaction and any entity it changes is flushed at commit. So read methods should never change entities at all.
InnoDB uses REPEATABLE READ. Inside one transaction, repeated plain reads see the same snapshot taken at the first read, even if others commit changes meanwhile. READ COMMITTED would show each statement the latest committed data instead.
Lesson: Read-only transactionsWith an AbstractRoutingDataSource whose lookup key comes from TransactionSynchronizationManager.isCurrentTransactionReadOnly(), wrapped in a LazyConnectionDataSourceProxy so the connection is chosen after the transaction starts. Read-only transactions then go to the replica and the rest to the primary, keeping in mind that replicas can lag.
REQUIRED joins the caller's transaction, so everything commits or rolls back together. REQUIRES_NEW suspends the caller's transaction and runs in its own, so its commit is final even if the caller later rolls back.
Lesson: Propagation basics: REQUIRED and REQUIRES_NEWNo. Only the inner transaction rolls back, and the outer one is not marked rollback-only, because they are separate transactions. That is different from REQUIRED, where catching leads to UnexpectedRollbackException.
On a method that must never run alone, such as a stock adjustment that only makes sense as part of a bigger operation. If someone calls it without a transaction, it fails fast with IllegalTransactionStateException instead of committing half a change.
Best is to move the transactional method into another bean and inject it, so every call crosses a proxy. Or I move the transaction to the outer method if the whole thing should be one unit, or use TransactionTemplate for a transaction per item.
@Transactional method?The proxy cannot override it, so the call runs on the proxy object itself. That object's fields were never injected, so you typically get a NullPointerException on a repository field, not just a missing transaction.
@Transactional?When the boundary is decided at runtime: a separate transaction per item in a batch, a retry loop around a transaction, or code that is not a Spring bean method. For normal service methods the annotation is clearer.
Lesson: Self-invocation and other reasons @Transactional is ignoredOptimistic locking lets everyone read freely and detects a clash when writing, using a version column in the UPDATE's WHERE. Pessimistic locking locks the row when reading (SELECT ... FOR UPDATE), so other writers wait. Optimistic suits rare clashes; pessimistic suits hot rows where retries would be constant.
In MySQL's REPEATABLE READ a normal SELECT takes no lock, so two transactions can read the same value and both write. The transaction makes each write atomic, but the later write still overwrites the earlier one.
Lesson: Optimistic locking with @Version vs pessimistic locksTwo transactions each hold a lock the other needs, so neither can go on. MySQL detects it and rolls one back. I avoid it by locking rows in a fixed order everywhere, keeping transactions short, and retrying once on CannotAcquireLockException.
@Version without a version column in the table?No, the version must be stored. With ddl-auto=validate the app would not even start without the column. I add it with a migration, version INT NOT NULL DEFAULT 0, so existing rows start at 0.
@Transactional method not roll back after an error?Either the exception was checked, which commits by default, or there was no transaction at all because the call did not go through the proxy: self-invocation, a private or final method, or an object made with new. I check the exception type and the transaction log.
readOnly = true give you with Hibernate?Hibernate sets flush mode MANUAL and loads entities read-only, so it keeps no snapshots and does no dirty checking. The connection is set read-only, and in my tests MySQL's driver refused an UPDATE on it. It is a performance and safety hint, not a security check, and it is ignored when the method joins a read-write transaction.
Lesson: Practice: transactions in SpringUnexpectedRollbackException?It means the method returned normally but the transaction could only roll back, because an inner transactional call that joined it failed and marked it rollback-only. Usually someone caught that inner exception. The fix is to not use exceptions for normal cases, to isolate optional work with REQUIRES_NEW, or to let the operation fail.
Lesson: Practice: transactions in Spring0-based: PageRequest.of(0, 20) is the first page. The offset is page × size. Web clients that want 1-based numbers must be converted, or the web support can be switched with spring.data.web.pageable.one-indexed-parameters=true, but mixing the two styles is a common off-by-one bug.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson