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
An ORM is a library that maps classes to tables. It writes the SQL for common operations, turns rows into objects and objects into rows, and inside a transaction it notices changed objects and updates their rows. In Java the standard API is JPA and Hibernate is the usual implementation.
Lesson: The object-relational mismatchThe SQL is hidden, so one method call can send many queries without anyone noticing, like the N+1 problem. It has its own rules to learn, such as lazy loading and when changes are flushed. I keep SQL logging on in development so I always know what my code sends.
Lesson: The object-relational mismatchI would replace it with one SQL query that groups by month and sums quantity * unit_price inside MySQL, returning a small list of rows into a record. That moves the work to the database, sends one query and loads no entities.
JPA is a specification: the jakarta.persistence annotations, the EntityManager API and JPQL. Hibernate is an implementation of that specification that actually generates and runs the SQL. I code against JPA and Hibernate does the work underneath; it also has extra features of its own.
Java EE moved to the Eclipse Foundation and became Jakarta EE, and from Jakarta EE 9 the packages had to be renamed to jakarta. Spring Boot 3 and later and Hibernate 6 and later use only the new names, so old javax.persistence code does not compile.
The Spring Data proxy hands the call to SimpleJpaRepository, which calls EntityManager.find. Hibernate builds the SELECT from the entity mapping, takes a connection from HikariCP, and runs a JDBC prepared statement through the MySQL driver. The row comes back and Hibernate builds the Category object.
The spring-boot-starter-data-jpa dependency, the mysql-connector-j driver with runtime scope, and spring.datasource.url, username and password. Boot then auto-configures a HikariCP pool, Hibernate and Spring Data repositories; I add an entity and a repository interface.
I write password: ${DB_PASSWORD} in application.yml and set the environment variable on each machine or in the deployment. The YAML file can then be committed safely, and each environment has its own value.
With ddl-auto=validate the app stops at startup: wrong column type, found int but expecting bigint. I change the field to Integer, because the schema owns the table; Java types must follow the column types.
I set logging.level.org.hibernate.SQL to DEBUG, add hibernate.format_sql for readability, and set org.hibernate.orm.jdbc.bind to TRACE to see the parameter values. I put these in the dev profile only.
Hibernate uses prepared statements, sending the SQL and the values separately. That protects against SQL injection and lets the database reuse the statement. To see the values I turn on the bind logger at TRACE.
Lesson: Seeing the SQL Hibernate generatesNo. It costs time on every query, produces huge amounts of log data, and bind logging writes personal data like e-mails into the logs. For production issues I use metrics and targeted, temporary logging instead.
Lesson: Seeing the SQL Hibernate generatesJPA is the specification in jakarta.persistence: annotations, the EntityManager and JPQL. Hibernate implements it and does the real work of writing and running SQL; Spring Boot 4.1 uses Hibernate 7. Spring Data JPA sits on top and generates repository classes from interfaces like JpaRepository<Category, Integer>.
I look for the APPLICATION FAILED TO START box first, then Hibernate's HHH000247 WARN lines, then the last Caused by line. Those tell me whether it is a missing URL, a missing driver, a network, login or database-name problem, or a schema validation mismatch.
The YAML only has placeholders like ${DB_PASSWORD}; the values come from environment variables or the deployment platform. Dev can have harmless defaults for the URL and user, but never the password.
Hibernate creates the object for a loaded row through the no-arg constructor and then fills the fields by reflection. I make it protected so Hibernate can still use it, but other code is guided to the real constructor that sets every required field.
GenerationType.IDENTITY, because MySQL creates keys with AUTO_INCREMENT. The default AUTO makes Hibernate use a sequence stored in a separate table such as products_seq, which our schema does not have, so validate fails. One side effect of IDENTITY is that Hibernate must run the INSERT immediately to learn the key.
I read the last Caused by: line to see which class it names. Then I check two things: that the class has @Entity, and that its package is the main class's package or below it, because Spring Boot scans only there. Usually the main class or the entity is in the wrong package.
@Column names the column behind a field and describes it: nullable, length, unique, precision and scale. It is not required when the snake case of the field name matches the column, but I add it on ShopLite entities to copy the real column facts next to the field.
@Transient removes the field from the mapping completely: it is never read or written. insertable = false, updatable = false keeps the column in every SELECT but leaves it out of INSERT and UPDATE, which fits a column the database fills, like created_at with a default.
The service first checks whether the e-mail is taken and throws a conflict exception that becomes 409. Because two requests can pass that check at the same time, the MySQL unique constraint is the final guard, and I map its DataIntegrityViolationException to the same 409.
ORDINAL stores the position of the constant, so adding or reordering constants silently changes what old rows mean: a cancelled order can read as delivered. STRING stores the name, which survives new constants and reordering, and the data is readable in SQL. The only thing that breaks it is renaming a constant, so I treat enum names like column names.
I look at what the column holds. ShopLite's DATETIME columns hold India wall-clock time, so they map to LocalDateTime. An Instant is a UTC moment, and Hibernate 7 on MySQL reads and writes it as UTC, so it fits only columns defined to hold UTC. Mixing them shifts times by the zone offset without any error.
When a field should be a small value type with its own rules, like a Pincode or a PhoneNumber, but the column is a plain CHAR or VARCHAR. The converter turns the value into the column value and back, and the type's constructor validates once. I handle null in both methods and use autoApply = true so every field of that type is covered.
An entity has an identity, its id, and stays the same thing when its data changes, like a customer who moves house. A value object is defined only by its values and has no id, like a postal address or an amount; two with the same values are equal, and you replace it instead of changing it.
Lesson: Embedded value objects with @EmbeddableWhen the embedded fields must map to other column names than the defaults, and always when one entity embeds the same type twice, like a pickup and a drop place. Without overrides both uses claim the same columns and Hibernate fails with "Column is duplicated in mapping".
Lesson: Embedded value objects with @EmbeddableHibernate loads the embedded field as null, not as an empty object. That causes NullPointerException in code that worked in memory. I avoid it with NOT NULL columns, or I expose the value as Optional so callers must handle the missing case.
With Spring Data auditing: @EnableJpaAuditing once, @EntityListeners(AuditingEntityListener.class) on the entity or a mapped superclass, and @CreatedDate / @LastModifiedDate on the fields. The listener fills them before each INSERT and UPDATE, and I add updatable = false to the created column.
The default fills the row, but Hibernate does not read it back, so the saved object holds null until the next load; and the time then depends on the database session's zone. Setting it in the app gives the value immediately and under my control. I still keep the default for rows inserted by SQL scripts.
With @CreatedBy on a field and an AuditorAware bean. Today it returns a fixed "system"; after Spring Security in Section 17, getCurrentAuditor() reads the authenticated user from the security context, so entities and services do not change.
The id is null until the entity is saved, so the hash changes at save() and the entity gets lost in any HashSet it was in. Also, Objects.equals(id, other.id) makes all unsaved entities equal to each other, so a set of new entities keeps only one.
Yes, all elements share one bucket, so lookups become linear. Entity sets are usually small, like the items of one order, so correctness wins. For big lookups I use a Map<Integer, Product> keyed by id instead of a set of entities.
Only the id and a few simple columns. toString runs in logs and debuggers, often outside a transaction, and touching a lazy relationship there throws LazyInitializationException or runs extra queries. So relationships and collections never go in it.
I go column by column from the CREATE TABLE: table name, key with IDENTITY and the right wrapper type, then each column's Java type and any name that is not snake case, enums as STRING and value groups as embeddables. Then I start with ddl-auto=validate and fix each reported mismatch on the Java side until it starts cleanly.
STRING. ORDINAL stores the position, so adding a constant in the middle or reordering silently changes the meaning of old rows. STRING survives that and is readable in SQL; only renaming a constant needs a data migration.
Lesson: Practice: entities and mappingWith Spring Data auditing: @EnableJpaAuditing, the AuditingEntityListener, and @CreatedDate (with updatable = false) and @LastModifiedDate. I supply the time through a DateTimeProvider using a fixed-zone Clock, so the stored time matches the database's zone rule and tests can fix the clock.
CrudRepository has the basic save, find, count and delete methods. JpaRepository extends ListCrudRepository and ListPagingAndSortingRepository and adds JPA-specific methods like flush, saveAndFlush, deleteAllInBatch and getReferenceById. I usually extend JpaRepository, or the bare Repository for a read-only interface.
When I only need the id to point at a row, such as setting a @ManyToOne customer on a new order. It returns a lazy proxy with no SELECT. If I need the data or must check the row exists, I use findById, because a wrong id only fails later with EntityNotFoundException.
Business rules would end up spread over controllers, transactions would be unclear, and entities would leak into JSON. In ShopLite the controller calls a service, the service uses repositories and returns DTO records, and the repositories are package-private.
Lesson: JpaRepository and the built-in CRUD methodsAt startup it splits the name into a subject (find...By, count...By, exists...By, delete...By) and a predicate of property names joined by keywords such as And, Between or OrderBy. It checks every property against the entity and builds a JPA criteria query, which Hibernate turns into SQL.
findFirst and findTopN fix the limit in the method name and add limit ? to the SQL. A Limit parameter (Spring Data 3.2+) lets the caller choose the number at runtime. For page numbers and totals I use a Pageable instead.
When the name stops being readable, roughly after three conditions, or when I need joins, functions, aggregates or projections. Then I write @Query with JPQL and give the method a business name like findBuyable.
SQL works on tables and columns of one database. JPQL works on entities and their fields and is translated by Hibernate into the database's SQL. JPQL is checked against the mapping at startup and is portable, but it lacks database-only features like window functions.
Lesson: @Query with JPQLYes, as long as I use parameters. Values are bound separately from the query text. It becomes unsafe only if I build the query string by concatenating user input, which @Query with a constant string does not allow anyway.
That the JPQL uses field names, not column names. is_active is the column; the entity field is active, so the query must say p.active. The message tells me the entity, so I compare it with the entity class.
A JPQL query on active products with stock < :threshold, ordered by stock and then id so the most urgent come first in a stable order. The service maps the entities to DTOs before returning them.
Not if I bind every value with :name or ?1; the SQL text stays fixed. They become vulnerable when someone concatenates input into the SQL, for example with createNativeQuery("..." + input). Identifiers like sort columns cannot be bound, so I map them through an allow-list.
The Java type follows the SQL type: count(*) and rank() are BIGINT in MySQL, so they come back as Long. DECIMAL columns come back as BigDecimal. The projection's getters must use those types.
A result type that holds only part of an entity, such as an interface with a few getters or a record. Spring Data selects only those columns, so less data travels and Hibernate does not track the results for changes.
Lesson: Projections and DTO queriesEntities always select every mapped column, and later may trigger extra queries for lazy links. A closed projection selects only the fields it declares, and a constructor expression can join related data into the same statement.
Lesson: Projections and DTO queriesThe values in a JPQL select new ...(...) do not match any constructor of that class by count, order or type. I compare the select list with the record components in order.
When the same query serves several screens with different shapes, for example a full entity for editing and a small record for a dropdown. The trade-off is that the signature no longer says what comes back.
Lesson: Projections and DTO queriesA bulk query changes rows in the database but not the entities Hibernate already holds, so later reads in the same transaction return stale objects. clearAutomatically clears the persistence context after the query; flushAutomatically sends pending changes before it so clearing does not lose them.
Everything that runs on entities: @Version checks and increments, @PreUpdate and auditing callbacks like @LastModifiedDate, Bean Validation on the entity, and the cached objects in the persistence context. It is plain SQL.
On the service method. It represents the business operation, so the update and any other steps commit or roll back together. A transaction on the repository method would make each call its own unit of work.
Lesson: @Modifying update and delete queriesOutside a transaction that already holds it, save merges: it SELECTs the row and then sends an UPDATE. Inside a transaction where the entity is already managed, I do not need save at all; Hibernate flushes the change at commit.
They select only the fields the response needs and can join related data into one query, so there is less data, no 1 + N lookups and nothing for Hibernate to track. A record projection can even be the response DTO.
Lesson: Practice: repositories and queriesIt skips entity callbacks, auditing and @Version, and it leaves already-loaded entities stale. I run it in a @Transactional service with flushAutomatically and clearAutomatically, and update the version column myself if the table uses optimistic locking.
Because the default is EAGER, and EAGER cannot be turned off per query. A simple findAll() then loads every linked parent, often with one extra SELECT per parent, which is the N+1 problem. With LAZY I load the parent only where I need it, for example with a fetch join in one specific query.
No. The @ManyToOne side is enough to save and read the link. I add the @OneToMany only when I really work with the children through the parent, like order items, and when the list stays small. For big lists such as all products of a category I use a paged repository query.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson