← Back to Articles
6/6/2026Admin Post

springboot jpa part6 interview

Part 6: Interview Questions and Architect Guide

Navigation: Index | Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6


Table of Contents

  1. How to Use This Guide
  2. Category 1: JPA and Hibernate Fundamentals (Most Asked)
  3. Category 2: Relationships and Mapping (Most Asked)
  4. Category 3: Performance and N+1 (Frequently Asked)
  5. Category 4: Transactions (Frequently Asked)
  6. Category 5: Database Design and IDs (Important)
  7. Category 6: Production Patterns (Senior Level)
  8. Category 7: Tricky and Edge Case Questions
  9. Category 8: Architect-Level Discussions
  10. Category 9: Last 2 Years - Trending Topics (2024-2026)
  11. Follow-Up Question Chains
  12. What Interviewers Actually Evaluate

1. How to Use This Guide

Questions are ordered by frequency of appearance in real interviews. Most-asked questions appear first within each category.

For each question, you will find:

  • The direct answer (what to say first)
  • The depth (what a senior/architect adds)
  • Common follow-up questions
  • Traps to avoid

For Technical Architect interviews: Interviewers expect you to talk about trade-offs, not just facts. Every answer should end with "it depends on..." and explain what it depends on.


2. JPA Fundamentals

Q1: What is the difference between JPA, Hibernate, and Spring Data JPA?

Direct answer:
JPA (Jakarta Persistence API) is a specification - a set of interfaces and rules, not an implementation. Hibernate is the most popular implementation of JPA - it is the actual library that does the ORM work. Spring Data JPA is a higher-level abstraction built on top of JPA that provides JpaRepository, derived query methods, and pagination support.

Depth:

JPA Specification  ->  Defines: @Entity, EntityManager, JPQL
Hibernate          ->  Implements: Session, HQL, SQL generation, caching
Spring Data JPA    ->  Adds: JpaRepository, @Query, Specifications, @EntityGraph

If JPA is the "blueprint," Hibernate is the "builder," and Spring Data JPA is the "contractor who manages builders for you."

Follow-up: "Can you use Hibernate without JPA?" Yes, Hibernate has its own native API (Session instead of EntityManager). But using the JPA API keeps your code portable to other providers like EclipseLink.


Q2: What is the Persistence Context and the First-Level Cache?

Direct answer:
The Persistence Context is a unit of work. It is a container that tracks all entities that have been loaded or saved within the current transaction. Hibernate uses it as a first-level cache - if you load the same entity twice by ID in one transaction, only one SQL query is issued. The second call returns the cached instance.

Example:

@Transactional
public void example() {
    User u1 = userRepository.findById(1L).orElseThrow();  // SELECT issued
    User u2 = userRepository.findById(1L).orElseThrow();  // NO SELECT - from cache
 
    assertThat(u1 == u2).isTrue();  // Same instance - not just equal, identical reference
}

Depth - The Dirty Checking mechanism:
The Persistence Context also tracks the original state of all entities when they were first loaded. Before committing, Hibernate compares the current state to the original state (dirty checking). Any changed field triggers an automatic UPDATE - you do not need to call save() explicitly on managed entities.

@Transactional
public void updateName(Long id, String newName) {
    User user = userRepository.findById(id).orElseThrow();
    user.setName(newName);
    // No save() needed! Hibernate detects the change and flushes on commit.
}

Trap: This is called "automatic dirty checking." Never call save() inside a @Transactional method on an already-managed entity - it is redundant (though not harmful).


Q3: What are the different entity states in JPA?

Direct answer:

StateDescription
TransientCreated with new, not tracked, not in DB
Managed (Persistent)In DB and tracked by Persistence Context
DetachedWas managed, session closed, no longer tracked
RemovedScheduled for DELETE on next flush

Code example:

User user = new User();               // TRANSIENT
entityManager.persist(user);          // MANAGED
entityManager.detach(user);           // DETACHED
user.setName("new");                  // Change ignored by JPA
entityManager.merge(user);            // Back to MANAGED, changes tracked
entityManager.remove(user);           // REMOVED

Q4: What is the difference between get()/find() and getReference()/getReferenceById()?

Direct answer:

  • findById(id) issues a SELECT immediately and returns an Optional (null if not found)
  • getReferenceById(id) returns a proxy (a hollow object with just the ID) without issuing a SELECT. The SELECT is deferred until a property other than ID is accessed.

When to use getReferenceById:

// Creating an OrderItem that references a Product by ID
// We do NOT need to load the entire Product - just the FK value
Product productProxy = productRepository.getReferenceById(productId);
OrderItem item = new OrderItem();
item.setProduct(productProxy);  // Only the ID is used for the FK - no SELECT needed
orderItemRepository.save(item);
// Result: Only one INSERT, no SELECT for Product

Trap: If the entity does not exist and you access a property, EntityNotFoundException is thrown - not at getReferenceById() call time, but when you first access the proxy.


Q5: What is the difference between save(), persist(), merge(), and saveAndFlush()?

Direct answer:

MethodSourceBehavior
persist()JPATransitions Transient -> Managed. Fails if entity already has ID
merge()JPAMerges a Detached entity. Returns new managed instance
save()Spring DataUses persist for new entities, merge for detached
saveAndFlush()Spring DataCalls save() then immediately flushes to DB

Depth:

// persist(): Fails if entity has an ID set (thinks it's detached)
User user = new User();
user.setId(99L);
entityManager.persist(user);  // Throws PersistenceException - ID already set
 
// merge(): Returns a NEW managed instance, original is still detached
User detached = new User();
detached.setId(1L);
detached.setName("Alice");
User managed = entityManager.merge(detached);  // Returns managed copy
// detached is still detached, managed is the tracked copy

Q6: What is flush() and how does Hibernate decide when to flush?

Direct answer:
Flushing synchronizes the in-memory Persistence Context state with the database. It sends pending INSERT, UPDATE, DELETE statements to the database (within the current transaction).

Flush modes:

FlushModeWhen Flush Occurs
AUTO (default)Before query execution if pending changes affect the query, and at transaction commit
COMMITOnly at transaction commit
ALWAYSBefore every query
MANUALOnly when explicitly called

Production tip: Use saveAndFlush() when you need the generated ID immediately (e.g., to use the ID in a subsequent operation within the same method, without committing the transaction).


3. Relationships and Mapping

Q7: What is the owning side of a JPA relationship and why does it matter?

Direct answer:
The owning side is the entity whose table holds the foreign key column. JPA reads and writes the relationship's FK based only on the owning side. The inverse side (marked with mappedBy) is read-only from JPA's perspective.

Why it matters - the classic mistake:

// Order.items has mappedBy = "order" -> INVERSE side
// OrderItem.order has @JoinColumn -> OWNING side
 
Order order = new Order();
OrderItem item = new OrderItem();
 
// WRONG: Setting only the inverse side
order.getItems().add(item);
orderRepository.save(order);
// Result: item.order_id is NULL! JPA ignores the inverse side when writing.
 
// CORRECT: Must set the owning side
item.setOrder(order);             // Sets the FK
order.getItems().add(item);       // For in-memory consistency only
orderRepository.save(order);      // item.order_id is now correctly set

Q8: What does mappedBy mean?

Direct answer:
mappedBy = "fieldName" on the @OneToMany or @ManyToMany side declares:

  1. This is the INVERSE side of the relationship
  2. The FK is managed by the fieldName attribute on the OTHER entity
  3. JPA must not create its own join table or FK column for this side
@OneToMany(mappedBy = "order")  // "order" is the field name in OrderItem
private List<OrderItem> items;
 
// This tells JPA: "Look at OrderItem.order for the FK. I am just the inverse view."

Q9: When should you use CascadeType.ALL vs individual cascade types?

Direct answer:
Use CascadeType.ALL only when the child entity has NO meaning without the parent (true composition). For most relationships, be selective.

Decision framework:

Is the child entity a composition of the parent?
(child cannot exist independently, child is part of parent)
  YES -> CascadeType.ALL + orphanRemoval = true
  NO  -> Use specific cascades or no cascade

Examples:
  Order -> OrderItems:   CascadeType.ALL, orphanRemoval=true (items are part of order)
  User -> Orders:        CascadeType.PERSIST, MERGE (orders exist independently)
  User -> Roles:         NO cascade (roles are completely independent)

Trap: CascadeType.ALL includes CascadeType.REMOVE. On @ManyToMany, this means deleting a User could cascade-delete the ADMIN Role, wiping all users' role assignments.


Q10: What is orphanRemoval and how is it different from CascadeType.REMOVE?

Direct answer:

  • CascadeType.REMOVE: When the parent entity is DELETED, delete its children.
  • orphanRemoval = true: When a child is REMOVED from the parent's collection, delete that child from the database.
// CascadeType.REMOVE example:
orderRepository.delete(order);     // Deletes order AND all its items
 
// orphanRemoval example:
order.getItems().remove(item);     // Deletes ONLY item from DB (no parent deletion)
// Without orphanRemoval, this would just set item.order_id = NULL

Follow-up: "What happens if you call items.clear()?"
With orphanRemoval = true, clearing the collection triggers a DELETE for every item in the collection. This is a common cause of accidental data deletion.


Q11: What is the default fetch type for each relationship annotation?

Direct answer:

AnnotationDefault Fetch
@OneToOneEAGER
@ManyToOneEAGER
@OneToManyLAZY
@ManyToManyLAZY

Critical follow-up: Always override @OneToOne and @ManyToOne to LAZY. The EAGER defaults for these were a historical mistake in the JPA specification that the community now universally recommends against.


Q12: Why should you use Set instead of List for @ManyToMany?

Direct answer:
When using a List for @ManyToMany, Hibernate's delete strategy is destructive: on any modification, it deletes ALL rows from the join table for this entity and reinserts all remaining ones.

With a Set, Hibernate can target individual rows: one INSERT for adding, one DELETE for removing.

Example:

// User has 5 roles. Adding a 6th role with List:
// DELETE FROM user_roles WHERE user_id = 1        (deletes all 5 existing rows!)
// INSERT INTO user_roles VALUES (1, role_1)        (reinserts 5 existing rows)
// INSERT INTO user_roles VALUES (1, role_2)
// INSERT INTO user_roles VALUES (1, role_3)
// INSERT INTO user_roles VALUES (1, role_4)
// INSERT INTO user_roles VALUES (1, role_5)
// INSERT INTO user_roles VALUES (1, role_6)        (new one)
 
// Same operation with Set:
// INSERT INTO user_roles VALUES (1, role_6)        (only the new one)

4. Performance and N+1

Q13: What is the N+1 problem?

Direct answer:
When loading N parent entities triggers N additional queries to load each parent's children. The total query count is N+1 instead of 1.

Example:

List<Order> orders = orderRepository.findAll();  // 1 query -> 100 orders
for (Order o : orders) {
    o.getItems().size();  // 1 query per order -> 100 queries
}
// Total: 101 queries instead of 1 or 2

Follow-up: "How do you detect N+1 in production?"

  1. Enable Hibernate SQL logging and count SELECT statements for a single request
  2. Use datasource-proxy library in tests to assert query counts
  3. Monitor hikaricp.connections.active and SELECT rate in AWS CloudWatch
  4. Enable hibernate.generate_statistics=true and monitor query execution counts

Q14: What are all the solutions for N+1? Compare them.

Solutions ranked by applicability:

1. JOIN FETCH (JPQL)

@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.customer.id = :id")
List<Order> findByCustomerWithItems(@Param("id") Long id);

Best for: Single entity or fixed list fetches. Cannot be used with pagination for collections.

2. @EntityGraph

@EntityGraph(attributePaths = {"items", "items.product"})
Page<Order> findByCustomerId(Long customerId, Pageable pageable);

Best for: Works safely with pagination. Concise annotation-based approach.

3. default_batch_fetch_size

hibernate.default_batch_fetch_size: 25

Best for: Transparent baseline improvement. No code changes needed. Works everywhere.

4. DTO Projection

@Query("SELECT new OrderSummaryDto(o.id, o.total, o.status) FROM Order o WHERE o.id = :id")
OrderSummaryDto findSummaryById(Long id);

Best for: Read-only responses. Fastest. Does not load entity lifecycle overhead.

Architect's note: Use batch_fetch_size as a global baseline, JOIN FETCH or EntityGraph for critical code paths, and DTOs for read-only API endpoints.


Q15: What is the Cartesian product problem with EAGER fetching?

Direct answer:
If an entity has two EAGER collections (say items and tags), MySQL JOINs both in one query. The result set contains every combination: if order has 10 items and 5 tags, the result has 50 rows, not 15. Hibernate must deduplicate these. This wastes I/O and memory exponentially.

// BAD:
@OneToMany(fetch = FetchType.EAGER)  // 10 rows
private List<OrderItem> items;
 
@ManyToMany(fetch = FetchType.EAGER)  // 5 tags
private Set<Tag> tags;
 
// Result of findById: 10 * 5 = 50 rows fetched for 15 unique records
// Hibernate's HHH90003004 warning: MultipleBagFetchException
// With 3 EAGER collections and 20 records each: 8000 rows for 60 records

Rule: Never have more than one EAGER collection on an entity. Always use LAZY.


5. Transactions

Q16: What does @Transactional do and how does Spring implement it?

Direct answer:
@Transactional tells Spring to wrap the method in a database transaction: begin transaction before the method, commit on success, rollback on exception.

Spring implements it via AOP (Aspect-Oriented Programming) proxy. When you @Autowired a @Service, Spring injects a proxy that intercepts method calls and wraps them in transaction management logic.

Depth - The proxy mechanism:

// What Spring actually creates:
class OrderServiceProxy extends OrderService {
    @Override
    public Order createOrder(CreateOrderRequest req) {
        transactionManager.begin();
        try {
            Order result = super.createOrder(req);  // Actual method
            transactionManager.commit();
            return result;
        } catch (RuntimeException e) {
            transactionManager.rollback();
            throw e;
        }
    }
}

Follow-up: "Why does self-invocation break @Transactional?"
When a method calls another method on this, it calls the real object directly, bypassing the proxy. The interceptor never runs, so no transaction is created.


Q17: What are @Transactional propagation types and when do you use them?

Most important ones in production:

REQUIRED (default): Join existing or create new. 99% of use cases.

REQUIRES_NEW: Suspend existing transaction, start a fresh one. Use for audit logging, sending notifications, or any operation that must commit independently.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(AuditEvent event) {
    auditRepository.save(event);
    // Commits independently - even if the parent transaction rolls back,
    // the audit log is preserved. Crucial for compliance.
}

NESTED: Uses a savepoint. If the nested transaction rolls back, the outer continues from the savepoint. Not supported by all databases.

SUPPORTS: Use only if a transaction exists. Non-transactional otherwise. Used for optional caching helpers.


Q18: What is @Transactional(readOnly = true) and when should you use it?

Direct answer:
Sets the transaction as read-only. This tells Hibernate:

  1. Skip dirty checking (no need to compare entity state to original snapshot)
  2. Do not flush to the database on commit
  3. Allow database driver/replication layer to route to a read replica

Use for ALL read operations:

@Transactional(readOnly = true)  // Every service method that only reads
public List<ProductResponse> getProducts() { ... }
 
@Transactional  // Only methods that write
public Product createProduct(CreateProductRequest req) { ... }

Performance impact: On a transaction loading 10,000 entities, skipping dirty checking provides measurable speedup. Rule: if the method does not modify data, use readOnly = true.


Q19: What happens if a RuntimeException is thrown inside a @Transactional method?

Direct answer:
The transaction is rolled back. ALL database changes made within that transaction are reverted. The exception propagates to the caller.

@Transactional
public void createOrderAndDeductStock(Long productId, int qty) {
    orderRepository.save(buildOrder());    // INSERT committed?
    productService.deductStock(productId, qty);  // Throws RuntimeException
 
    // RESULT: Both the order INSERT and any stock UPDATE are rolled back.
    // Database is in the same state as before the method was called.
}

Trap for checked exceptions:

@Transactional
public void processFile() throws IOException {
    fileRepository.save(metadata);
    Files.readAllBytes(path);  // Throws IOException (CHECKED)
    // DEFAULT BEHAVIOR: IOException does NOT trigger rollback!
    // The fileRepository.save() is COMMITTED despite the exception!
}
 
// Fix: Use rollbackFor
@Transactional(rollbackFor = Exception.class)
public void processFile() throws IOException { ... }

6. Database Design and IDs

Q20: When should you use UUID vs Long AUTO_INCREMENT as a primary key?

Direct answer:

Use Long AUTO_INCREMENT when:

  • Single-database application
  • Sequential, predictable inserts
  • Internal tables not exposed to external API
  • Insert performance is critical (sequential clustered index)

Use UUID when:

  • Distributed systems that generate IDs independently
  • Data is merged from multiple databases
  • IDs are exposed in URLs (prevents enumeration attacks)
  • Event sourcing or CQRS architectures

MySQL-specific concern:
UUID v4 is random, which causes B-tree index fragmentation in MySQL. Each new UUID inserts into a random position in the clustered index, causing page splits. Use UUID v7 or ULID for UUID keys in MySQL to maintain near-sequential inserts.


Q21: What is the difference between @EmbeddedId and @IdClass for composite keys?

Aspect@EmbeddedId@IdClass
Key class type@Embeddable value objectPlain Serializable class
JPQL accessWHERE e.id.field1 = :valWHERE e.field1 = :val
Code duplicationNone - key fields in key class onlyFields duplicated in entity
@MapsId supportFull supportVerbose
PreferredYesLegacy preference

Q22: What is the difference between SEQUENCE and IDENTITY ID generation in Hibernate?

Direct answer:

StrategyMySQL SupportBatch InsertID Before Insert
IDENTITYYes (AUTO_INCREMENT)NoNo
SEQUENCESimulated (TABLE)Yes (allocationSize)Yes

Key insight: With IDENTITY, Hibernate must do the INSERT first, then use LAST_INSERT_ID() to get the ID. This means Hibernate cannot batch inserts with IDENTITY strategy. With SEQUENCE, Hibernate pre-allocates a range of IDs (allocationSize = 50 means 50 IDs per DB call), enabling efficient batching.

In MySQL, IDENTITY is standard. If batch insert performance is critical, consider UUID (ID known before insert) or switch to PostgreSQL for SEQUENCE support.


7. Production Patterns

Q23: How do you implement optimistic locking? When do you choose it over pessimistic?

Direct answer:
Add @Version Long version to the entity. Hibernate adds WHERE version = ? to all UPDATE statements. If the version does not match (another transaction updated it), OptimisticLockException is thrown.

When to choose:

Optimistic locking: Low contention, high read-to-write ratio, long user sessions (user spends 5 minutes editing, then saves). Conflict is rare, so optimistic is more efficient.

Pessimistic locking: High contention, financial operations, flash sales with limited stock. When you cannot afford retries and conflict is likely.

Follow-up: "What do you do when OptimisticLockException is thrown?"
Retry the operation (usually 3 times). Spring Retry's @Retryable with exponential backoff handles this cleanly.


Q24: How do you implement soft deletes in JPA?

Direct answer:
Three annotations work together:

  1. @SQLDelete overrides the DELETE SQL with an UPDATE that sets deleted_at
  2. @SQLRestriction (Hibernate 6) or @Where adds a global filter to all queries
  3. Add a deleted_at DATETIME column to the entity
@Entity
@SQLDelete(sql = "UPDATE products SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class Product {
    @Column(name = "deleted_at")
    private Instant deletedAt;
}

Follow-up: "What are the downsides of soft deletes?"

  • Tables grow indefinitely (need archival strategy)
  • JOIN queries can return stale data if not all entities use soft deletes
  • Unique constraints on soft-deleted records block reuse (e.g., unique email that was "deleted")
  • @SQLRestriction adds AND deleted_at IS NULL to EVERY query, including those that do not need it

Q25: How does HikariCP connection pooling work and how do you size it?

Direct answer:
HikariCP maintains a pool of pre-established database connections. When your code needs a DB connection, it borrows one from the pool. When done, it returns it instead of closing it.

Sizing formula:

pool_size = (core_count * 2) + effective_spindle_count

For AWS RDS with SSD on a 4-core instance: (4 * 2) + 1 = 9, round to 10.

Critical production parameters:

  • max-lifetime: Must be less than MySQL wait_timeout (default 28800s). Set to 1800000ms (30 min).
  • connection-timeout: How long to wait for a connection from the pool. Set to 30000ms.
  • leak-detection-threshold: Warn if connection is held longer than this.

Follow-up: "Why does a larger pool sometimes make things worse?"
More connections means more threads. Context switching overhead on the database server increases. MySQL itself becomes the bottleneck because it handles each connection with a dedicated thread. Connection pool design papers (HikariCP authors) show that smaller pools at high efficiency outperform large pools with contention.


8. Tricky Questions

Q26: What happens when you call items.clear() on a @OneToMany with orphanRemoval = true?

Answer:
All items in the collection are deleted from the database. Hibernate issues individual DELETE statements for each item, or a single batch DELETE depending on batch settings.

This is the correct behavior, but it surprises many developers. Never call clear() unless you intentionally want to delete all children.

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
 
// In a transaction:
order.getItems().clear();  // Schedules DELETE for ALL order items
orderRepository.save(order); // Or just transaction commit - all items deleted

Q27: Can you have a @OneToMany without mappedBy? What happens?

Answer:
Yes, but Hibernate creates a separate JOIN TABLE to manage the relationship (similar to @ManyToMany). This is almost never what you want for @OneToMany.

// BAD: Unidirectional @OneToMany without mappedBy
@Entity
public class Order {
    @OneToMany  // No mappedBy!
    private List<OrderItem> items;
}
 
// Hibernate creates: orders_items (order_id, items_id)
// But order_items already has order_id! Now you have 2 ways to express the same relationship.
// Wastes space, confuses developers, causes double write operations.

Rule: Always use bidirectional @OneToMany with mappedBy, or unidirectional @ManyToOne only on the child. Never unidirectional @OneToMany alone.


Q28: What is a MultipleBagFetchException and when does it occur?

Answer:
Thrown when you try to JOIN FETCH two List (Bag) collections at the same time.

// This throws MultipleBagFetchException:
@Query("SELECT o FROM Order o JOIN FETCH o.items JOIN FETCH o.tags WHERE o.id = :id")
Optional<Order> findByIdWithItemsAndTags(@Param("id") Long id);

Why: A single SQL query with two JOINs produces a Cartesian product. Hibernate cannot reconstruct two separate lists from a Cartesian product correctly when both are Bags (Lists without ordering).

Fixes:

  1. Change one or both collections to Set - Hibernate can de-duplicate Sets
  2. Use separate queries: first fetch with items, then fetch tags separately
  3. Use @EntityGraph which handles this via separate SELECT statements internally

Q29: If you mark a method @Transactional(readOnly = true) but then call a write method inside it, what happens?

Answer:
It depends on the write method's propagation.

If the write method has @Transactional (default REQUIRED), it joins the existing read-only transaction. In MySQL, a read-only transaction allows DML statements, but Hibernate throws an exception before even attempting it because it skips dirty checking and flush in read-only mode.

@Transactional(readOnly = true)
public void badMethod() {
    Order order = orderRepository.findById(1L).orElseThrow();
    order.setStatus(SHIPPED);
    orderRepository.save(order);
    // Hibernate: TransactionException - flush during readOnly transaction
}

Q30: What is the LazyInitializationException and what are all the ways to fix it?

Answer:
Occurs when a lazy-loaded association is accessed AFTER the Hibernate session (transaction) has closed.

// Example:
Order order = orderService.getOrder(1L);  // Transaction ends here
order.getItems().size();  // Session closed -> LazyInitializationException

Fixes (in order of preference):

  1. DTO at service layer (best): Map to DTO inside the transaction, return DTO
  2. JOIN FETCH: Load everything needed within the transaction
  3. @EntityGraph: Declare what to load, applied at query time
  4. @Transactional on controller (not recommended): Extends transaction to controller
  5. OpenSessionInView (worst): Keeps session open for entire HTTP request - anti-pattern

Q31: Why can two entities with the same ID in a Set be considered duplicates even with getClass().hashCode()?

Answer:
They cannot - and that is the point. With getClass().hashCode(), all instances of the same type return the same hash code, so they go into the same hash bucket. Then equals() differentiates them by ID.

The invariant is maintained: two objects that are equals() must have the same hashCode(). But the reverse is not required. Using getClass().hashCode() is a conservative choice: always the same hash, so equals() is always called and makes the final decision.

The risk is O(N) lookup in large collections due to collisions. But for typical collection sizes in JPA entities (< 100 items), this is irrelevant.


Q32: What is the difference between JPQL and SQL?

AspectJPQLSQL
Operates onEntity objects and fieldsTables and columns
PortabilityDatabase-agnosticDatabase-specific
Type safetyField names checked by HibernateRaw string
JOINsBased on relationshipsBased on FK columns
ExampleFROM Order o WHERE o.customer.email = :emailFROM orders o JOIN users u ON o.customer_id = u.id WHERE u.email = ?

Key difference example:

// JPQL: navigate the object graph
"SELECT o FROM Order o WHERE o.customer.email = :email"
 
// SQL equivalent:
"SELECT o.* FROM orders o JOIN users u ON o.customer_id = u.id WHERE u.email = ?"

9. Architect-Level Discussions

Q33: How would you design the data layer for a multi-tenant SaaS application with Spring Boot and MySQL?

Answer:
Three strategies, each with different trade-offs:

Strategy 1: Separate Database per Tenant (Strongest Isolation)

@Component
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContextHolder.getCurrentTenant();  // Thread-local tenant ID
    }
}
// Each tenant gets their own MySQL database/schema.
// Complete isolation. Higher cost (N databases). Flyway must migrate each DB.

Strategy 2: Shared Database, Separate Schema

// Use MySQL schema (database) per tenant, single MySQL instance
// Flyway: apply migrations to all tenant schemas
// HikariCP: separate pool per tenant

Strategy 3: Shared Schema with Tenant Column (Most Cost Efficient)

@Entity
@Table(name = "orders")
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Order {
    @Column(name = "tenant_id", nullable = false, updatable = false)
    private String tenantId;
}
// Hibernate @Filter applied at session level
// Every query automatically includes WHERE tenant_id = ?
// Risk: Missing WHERE clause exposes all tenants' data

Architect answer: Choose Separate DB for compliance (HIPAA, GDPR - data physically separated), Shared Schema for startup MVP (cost), Separate Schema for middle ground.


Q34: How do you handle database migrations in a zero-downtime deployment on AWS?

Answer:
Key principle: Backwards-compatible migrations only. Each deployment must handle both old and new code versions reading the schema simultaneously (during rolling deployment).

Steps for a zero-downtime column rename:

Deployment 1:
  - Add new column phone_number (nullable)
  - Flyway V20: ALTER TABLE users ADD COLUMN phone_number VARCHAR(20)
  - Application code reads from phone, writes to BOTH phone and phone_number
  - Deploy with blue-green or rolling update

Deployment 2:
  - Backfill old data: UPDATE users SET phone_number = phone WHERE phone_number IS NULL
  - Application code reads from phone_number (now complete), writes to both

Deployment 3:
  - Flyway V22: ALTER TABLE users DROP COLUMN phone
  - Application code reads and writes only phone_number

AWS tooling: Use AWS CodeDeploy with blue-green deployment. Flyway runs before application startup. If migration fails, deployment fails before any traffic is routed.


Q35: How do you handle the situation where your JPA application needs to process 10 million records?

Answer:
Never load 10 million entities into memory. Use one of these patterns:

Pattern 1: JPA Stream API

@Transactional(readOnly = true)
@Query("SELECT p FROM Product p WHERE p.status = 'ACTIVE'")
Stream<Product> streamAllActiveProducts();
 
// Usage:
try (Stream<Product> products = productRepository.streamAllActiveProducts()) {
    products.forEach(product -> {
        processProduct(product);
        entityManager.detach(product);  // Immediately detach to release memory
    });
}

Pattern 2: Chunk Processing with Spring Batch

// Spring Batch: Reads 1000 records, processes, writes, then reads next 1000
@Bean
public Step processProductsStep() {
    return stepBuilder.<Product, ProductResult>chunk(1000, transactionManager)
        .reader(jdbcCursorItemReader())   // Streams from MySQL cursor
        .processor(productProcessor())
        .writer(resultWriter())
        .build();
}

Pattern 3: Keyset Pagination

Long lastId = 0L;
List<Product> batch;
do {
    batch = productRepository.findNextBatch(lastId, PageRequest.of(0, 1000));
    processBatch(batch);
    if (!batch.isEmpty()) {
        lastId = batch.get(batch.size() - 1).getId();
    }
} while (!batch.isEmpty());

Q36: How do you decide between a monolithic JPA model and a microservices approach?

Answer:
JPA's @ManyToOne and @OneToMany crossing service boundaries creates coupling. In microservices, you replace JPA relationships across services with:

// MONOLITH JPA: Entity relationship
@ManyToOne(fetch = FetchType.LAZY)
private Product product;
 
// MICROSERVICES: ID reference only
@Column(name = "product_id")
private Long productId;  // No JPA relationship - just the ID
 
// To get product details: call Product Service API asynchronously

Decision factors:

FactorMonolith JPAMicroservices
Team size< 10 developers> 20 developers
DeploymentSingle deploymentIndependent deployments
Data consistencyStrong (ACID)Eventual (Saga pattern)
Development speedFast initiallyFaster at scale
Operational complexityLowHigh

Architect answer: Start monolith, apply domain boundaries via packages (Part 1's hybrid package structure). Extract services when you have clear operational boundaries, independent teams, and different scaling needs per domain.


Q37: What is Hibernate 6's @SoftDelete annotation?

Answer:
Hibernate 6.4+ introduced a native @SoftDelete annotation that replaces the @SQLDelete + @SQLRestriction approach.

// Old approach (still works):
@SQLDelete(sql = "UPDATE products SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class Product { }
 
// New Hibernate 6.4+ approach:
@Entity
@SoftDelete(columnName = "deleted", strategy = SoftDeleteType.ACTIVE)
// OR
@SoftDelete(columnName = "deleted_at", strategy = SoftDeleteType.DELETED)
public class Product { }

Q38: What is the Virtual Threads (Project Loom) impact on JPA connection pooling?

Answer (Java 21+):
Spring Boot 3.2+ supports Virtual Threads. With virtual threads, blocking operations (DB calls) park the virtual thread instead of blocking a platform thread. This means you can handle more concurrent requests with fewer platform threads.

Impact on HikariCP:
Virtual threads still need an actual JDBC connection. HikariCP pool size should still be sized for the database's capacity, not the number of virtual threads.

spring:
  threads:
    virtual:
      enabled: true # Enable Virtual Threads in Spring Boot 3.2+
  datasource:
    hikari:
      maximum-pool-size: 10 # Still size for DB capacity, not request concurrency

Q39: What are the challenges with JPA in a reactive Spring WebFlux application?

Answer:
Standard JPA (blocking JDBC) is incompatible with reactive programming. You cannot use JPA directly in WebFlux without tying up reactor threads.

Solution: Spring Data R2DBC

// R2DBC: Reactive, non-blocking database access
public interface ProductRepository extends ReactiveCrudRepository<Product, Long> {
    Flux<Product> findByStatus(String status);  // Returns Flux, not List
    Mono<Product> findBySkuAndStatus(String sku, String status);
}
 
// Trade-off: R2DBC does NOT support JPA relationships or Hibernate ORM features.
// You write SQL manually or use JPQL-like query methods.
// For complex domains with many relationships, JOOQ is a better fit.

11. Follow-Up Question Chains

Chain 1: Starting with "What is the N+1 problem?"

Q: What is the N+1 problem?
A: (Explain one query + N queries per parent)

Follow-up: How do you detect it?
A: Hibernate SQL logging, datasource-proxy query counter in tests, statistics

Follow-up: How do you fix it?
A: JOIN FETCH, EntityGraph, batch fetch size

Follow-up: Which solution works with pagination?
A: EntityGraph and batch fetch. JOIN FETCH for collections causes in-memory pagination.

Follow-up: Show me how to safely paginate orders with their items.
A: (Two-query approach: paginate IDs, then fetch by IDs with JOIN FETCH)

Architect follow-up: At what point would you consider a different architecture for this query?
A: If the query is a reporting use case, use a DTO projection or a read model (CQRS pattern).
   If accessed from multiple places, consider a materialized view in MySQL.

Chain 2: Starting with "Explain @Transactional"

Q: What does @Transactional do?
A: (AOP proxy, begin/commit/rollback)

Follow-up: What is the default rollback behavior?
A: Rolls back for RuntimeException and Error. NOT for checked exceptions.

Follow-up: How do you rollback on checked exceptions?
A: @Transactional(rollbackFor = Exception.class)

Follow-up: What is REQUIRES_NEW?
A: Suspends current transaction, opens a new one. Use for audit logging.

Follow-up: What happens with self-invocation?
A: @Transactional is ignored. The proxy is bypassed. Use a separate bean.

Architect follow-up: How do you handle distributed transactions across two microservices?
A: Cannot use JPA @Transactional. Use Saga pattern (choreography or orchestration).
   Each service handles its own local transaction. Compensation transactions handle rollback.

Chain 3: Starting with "What is optimistic locking?"

Q: What is optimistic locking?
A: @Version field, WHERE version = ? in UPDATE, OptimisticLockException if mismatch

Follow-up: How do you handle OptimisticLockException?
A: Retry the operation (Spring Retry @Retryable)

Follow-up: When would you use pessimistic locking instead?
A: High contention, financial operations (cannot afford retries), flash sales

Follow-up: What is the difference between PESSIMISTIC_READ and PESSIMISTIC_WRITE?
A: READ = shared lock (SELECT LOCK IN SHARE MODE), WRITE = exclusive lock (SELECT FOR UPDATE)

Architect follow-up: How does optimistic locking behave in a cluster of 5 application instances?
A: Works correctly. All instances connect to the same MySQL primary. The DB-level version check
   is atomic. At most one instance wins the UPDATE per version increment.

12. What Interviewers Actually Evaluate

For Senior Developer Roles

Interviewers want to see:

  1. Practical problem solving: Can you debug a LazyInitializationException or identify an N+1 problem from a log snippet?

  2. Trade-off awareness: When you say "use JOIN FETCH," do you know it does not work with pagination?

  3. Production mindset: Do you mention connection pool sizing, indexes, and Flyway migrations unprompted?

  4. Code quality: Do you write helper methods for bidirectional relationships? Do you avoid @Data on entities?

For Technical Architect Roles

Architects are evaluated on:

  1. System-level thinking: Can you design a multi-tenant data model? Can you explain zero-downtime migrations?

  2. Cross-concern integration: How does the data layer interact with security, caching, messaging?

  3. Evolution path: Can you start with a monolith JPA model and explain when and how to evolve it?

  4. Failure mode analysis: What happens when the primary RDS fails? When migrations fail in production? When connection pool is exhausted?

  5. AWS integration depth: RDS parameter groups, read replica routing, Secrets Manager, CloudWatch metrics for HikariCP.

Red Flags That Eliminate Candidates

  • "I always use CascadeType.ALL on everything"
  • "I use FetchType.EAGER because it is simpler"
  • "I modify the migration script after it has been applied - just change the SQL"
  • "I use spring.jpa.hibernate.ddl-auto=update in production"
  • "Open Session In View is fine - it is the default"
  • Inability to explain the owning side of a relationship
  • Using @Data on JPA entities

Green Flags That Impress Interviewers

  • Mentioning readOnly = true for all read operations without being asked
  • Knowing about the self-invocation problem with @Transactional
  • Discussing the difference between List and Set for @ManyToMany
  • Explaining why getClass().hashCode() is correct for entities in Sets
  • Bringing up zero-downtime migration patterns for schema changes
  • Mentioning TestContainers as the preferred integration testing approach
  • Discussing HikariCP sizing formula and why bigger pools are not always better

Back to Index