The Spring cache abstraction and providers, implementing caching correctly, how Boot simplifies the data layer, transaction best practices, eviction vs expiration, pagination (offset and keyset), soft deletes with Hibernate 6, and structuring a user-management REST API.
Published September 25, 2026
Data-layer questions at this level expect production awareness: cache invalidation, transaction boundaries, N+1 queries, and pagination that doesn't fall over on page 10,000. Mention one failure mode per answer, and you'll stand out.
Short answer: Spring's cache abstraction: annotations (@Cacheable, @CachePut, @CacheEvict, @Caching) applied through AOP proxies to bean methods, backed by a pluggable CacheManager. Boot auto-configures the provider it finds:
ConcurrentHashMap fallback, with no expiry, suitable for development only.Key points to cover:
Cache-Control, CDNs) is separate, and often even more effective.Learn it in depth → Caching Strategies
Short answer:
spring-boot-starter-cache plus a provider (Caffeine or Redis).@EnableCaching.@Cacheable.@CacheEvict/@CachePut on writes, plus a TTL as a safety net.@Service
class ProductService {
@Cacheable(cacheNames = "products", key = "#id", unless = "#result == null")
public ProductDto find(long id) { return mapper.toDto(repo.findById(id).orElse(null)); }
@CacheEvict(cacheNames = "products", key = "#id")
@Transactional
public void updatePrice(long id, BigDecimal price) { repo.findById(id).orElseThrow().setPrice(price); }
}
spring:
cache:
type: redis
redis:
time-to-live: 10m
cache-null-values: false
Common trap: caching mutable entities, or JPA proxies, instead of immutable DTOs. And calling a @Cacheable method from the same class, which bypasses the proxy, so there's no caching.
Short answer:
DataSource with a HikariCP pool, EntityManagerFactory, transaction manager and JPA settings, all from spring.datasource.* and spring.jpa.*.findByStatusAndCreatedAtAfter), @Query, projections and specifications, with no implementation code.DataAccessException.@DataJpaTest, with Testcontainers.Key points to cover:
org.hibernate.SQL=DEBUG), and watch for N+1 problems.Learn it in depth → N+1 Query Problem
Short answer:
@Transactional on service-layer methods (the business unit of work), not on controllers or repositories.readOnly = true on queries.rollbackFor).REQUIRES_NEW for audit logs that must persist even when the main transaction fails.@Version) for concurrent edits.spring.jpa.open-in-view=false).@Transactional(readOnly = true)
public OrderView view(long id) { … }
@Transactional(timeout = 5, rollbackFor = PaymentDeclinedException.class)
public Order checkout(Cart cart) throws PaymentDeclinedException { … }
Learn it in depth → @Transactional Deep Dive
Short answer: Eviction removes entries to respect a capacity limit, chosen by a policy (LRU, LFU, or W-TinyLFU in Caffeine), and is triggered by space pressure. Expiration removes entries because they're too old, based on time (TTL after write, or time-to-idle after the last access), and it bounds staleness.
Key points to cover:
@CacheEvict" means explicit invalidation, triggered by your code when the data changes. That's a third mechanism, and the one that actually keeps a cache correct.Short answer: Accept a Pageable (?page=0&size=20&sort=createdAt,desc), pass it to a Spring Data method, and return the content plus page metadata.
public interface OrderRepository extends JpaRepository<Order, Long> {
Page<Order> findByCustomerId(long customerId, Pageable pageable); // content + total count
Slice<Order> findByStatus(OrderStatus status, Pageable pageable); // no count query
@Query("select o from Order o where o.id < :cursor order by o.id desc")
List<Order> nextPage(@Param("cursor") long cursor, Limit limit); // keyset (Spring Data 3.2+)
}
@GetMapping("/orders")
PagedModel<OrderDto> list(@RequestParam long customerId,
@PageableDefault(size = 20, sort = "createdAt", direction = DESC) Pageable pageable) {
return new PagedModel<>(repo.findByCustomerId(customerId, pageable).map(mapper::toDto));
}
Key points to cover:
Page runs an extra COUNT query. Use Slice when you only need "has next page".WHERE id < :lastSeen), or Spring Data's ScrollPosition.size (spring.data.web.pageable.max-page-size), so clients can't request a million rows.Short answer: Add a deleted flag, or better, a deleted_at timestamp. Intercept deletes, so they become updates, and filter deleted rows out of every query:
@Entity
@SQLDelete(sql = "UPDATE customer SET deleted_at = now() WHERE id = ? AND version = ?") // repo.delete() → UPDATE
@SQLRestriction("deleted_at IS NULL") // Hibernate 6.3+ (replaces @Where)
public class Customer {
@Id Long id;
@Version Long version;
Instant deletedAt;
}
Key points to cover:
@SoftDelete annotation.WHERE deleted_at IS NULL, in PostgreSQL), or include deleted_at in the unique key.Short answer: A layered, feature-oriented structure:
UserController): HTTP mapping, request and response DTOs, @Valid validation, status codes (201 with Location, 204, 404). No business logic.UserService): business rules (a unique email, password hashing), transaction boundaries, and domain events (UserRegistered).UserRepository extends JpaRepository): persistence only.@RestControllerAdvice returning Problem Details, Spring Security (a user may only read or update themselves, while admins can manage everyone), pagination for listing, and OpenAPI documentation.com.shop.user
├── api/ UserController, CreateUserRequest, UserResponse
├── domain/ User, UserService, UserRegistered
├── persistence/UserRepository
└── config/ SecurityConfig
Key points to cover:
PATCH for partial updates, and idempotent PUT/DELETE.@WebMvcTest, unit tests, and @DataJpaTest.Q: What is a cache stampede, and how do you prevent it?
A: Many requests miss the same expired key at once, and all hit the database. Mitigations: @Cacheable(sync = true) (one loader per key per instance), early or background refresh (Caffeine refreshAfterWrite), random TTL jitter, and request coalescing.
Q: @CachePut vs @CacheEvict on update?
A: @CachePut always runs the method, and stores its return value, which keeps the cache warm. @CacheEvict removes the entry, so the next read reloads it. Eviction is safer when the updated value isn't what the read method would return.
Q: How do you batch inserts efficiently with JPA?
A: Set spring.jpa.properties.hibernate.jdbc.batch_size (for example 50), plus order_inserts/order_updates. Avoid IDENTITY ID generation, which disables insert batching, in favour of sequences. Flush and clear the persistence context periodically for very large batches.
Q: Why use DTO projections for read endpoints? A: They select only the needed columns, avoid lazy-loading and N+1 surprises, bypass dirty checking, and decouple the API from the entity model.