Three separate topics that are always examined together: how Spring talks to a database, how it manages transactions, and how JPA and Spring Data sit on top of both.
Exceptions
What is the difference between a checked and an unchecked exception?
A checked exception extends Exception and the compiler forces you to catch or declare
it. An unchecked exception extends RuntimeException and it does not.
Why does Spring prefer unchecked exceptions?
Because a checked exception you cannot do anything about is pure noise. SQLException
is the classic case: your service has no way to recover from "the connection dropped", so the
catch block either logs and rethrows or, worse, swallows it. Making it unchecked lets
the code that can handle it do so, and lets everything in between stay clean.
What is the data access exception hierarchy?
Spring translates vendor-specific errors — SQLException with a MySQL error code,
Hibernate's own exceptions — into one consistent tree rooted at
DataAccessException, which is unchecked.
DataAccessException
├── DataIntegrityViolationException constraint violated
│ └── DuplicateKeyException unique index hit
├── EmptyResultDataAccessException queryForObject found nothing
├── IncorrectResultSizeDataAccessException
├── OptimisticLockingFailureException
├── CannotAcquireLockException
└── ...The point is portability: DuplicateKeyException means the same thing whether the
database was MySQL, PostgreSQL or Oracle, so your service layer does not have to know which. The
translation is switched on by @Repository — one more reason the stereotype
annotations are not interchangeable.
DataSource
How do you configure a DataSource?
In Boot, by setting properties. It auto-configures a connection pool — HikariCP — from them.
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/pizza?useSSL=false&connectionTimeZone=LOCAL
spring.datasource.username=root
spring.datasource.password=For tests and demos, EmbeddedDatabaseBuilder builds an in-memory H2 or HSQL database
and runs schema and data scripts into it. Useful, but be aware you are then testing against a
different database than you deploy on — Testcontainers is the modern answer to that.
JdbcTemplate
What is the template pattern, and what is the JdbcTemplate?
The template pattern puts the fixed part of an algorithm in one place and lets callers supply the
varying part. For JDBC the fixed part is: get a connection, create a statement, bind parameters,
execute, iterate the results, close everything in the right order even on failure, translate the
exception. The varying part is the SQL and the row mapping. JdbcTemplate owns the fixed
part; you supply the rest.
What are the callback interfaces used with queries?
| Callback | Called | Use for |
|---|---|---|
RowMapper<T> | once per row, returns an object | the normal case |
ResultSetExtractor<T> | once for the whole ResultSet | building one object from many rows |
RowCallbackHandler | once per row, returns nothing | streaming — writing a file, accumulating state |
@Component
public class TopProductRowMapper implements RowMapper<ReportDTOs.TopProduct> {
@Override
public ReportDTOs.TopProduct mapRow(ResultSet rs, int rowNum) throws SQLException {
return new ReportDTOs.TopProduct(
rs.getString("product_name"),
rs.getLong("units_sold"),
RowValues.money(rs.getBigDecimal("revenue")));
}
}Can you run plain SQL, and how does it return objects?
// A list of mapped objects
List<ReportDTOs.RevenueByDay> rows =
jdbcTemplate.query(sql, Map.of("from", from), revenueByDayRowMapper);
// Exactly one - throws EmptyResultDataAccessException if there are none,
// IncorrectResultSizeDataAccessException if there are two
ReportDTOs.Summary summary =
jdbcTemplate.queryForObject(sql, Map.of("from", from), reportSummaryRowMapper);
// A single scalar
Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM customer_order", Map.of(), Long.class);
// Anything that is not a query
jdbcTemplate.update("UPDATE product SET active = :active WHERE id = :id", params);When does the JdbcTemplate acquire and release its connection?
Per method call — it borrows from the pool, runs, and returns it. Unless a transaction is
already in progress, in which case it uses the one bound to the current thread and does
not close it. That is what allows a JdbcTemplate call and a JPA call inside the
same @Transactional method to be part of the same transaction.
Always use named or indexed parameters, never string concatenation.
:from and :limit are bound by the driver, so nothing a caller passes can
change the shape of the statement. Concatenating a value into SQL is how injection happens.
When is JdbcTemplate the right choice over JPA? Reporting and aggregation. JPA's
job is to map rows onto an object graph and track changes to it; a GROUP BY that
returns nine rows of totals wants neither. In the pizza API the reports are plain SQL with explicit
RowMappers for exactly that reason.
Transactions
What is a transaction?
A unit of work that is ACID — Atomic, Consistent, Isolated, Durable. All of it commits or none of it does.
Local versus global?
A local transaction involves one resource — a single database — and is managed by that resource. A global (distributed, XA) transaction spans several resources and needs a transaction manager to coordinate a two-phase commit. Global transactions are expensive and rare; almost everything you write is local.
Is a transaction a cross-cutting concern? How does Spring implement it?
Yes — and Spring implements it with AOP. @Transactional puts a proxy around the bean;
the proxy starts a transaction before the method, commits after it returns, rolls back if it throws.
Everything in the AOP lesson applies here,
self-invocation included.
What is the PlatformTransactionManager?
The abstraction that actually does the work, with one implementation per technology —
DataSourceTransactionManager for plain JDBC, JpaTransactionManager for
JPA, JtaTransactionManager for global transactions. Boot picks the right one; the
annotation stays the same regardless, which is the whole point of the abstraction.
What does @EnableTransactionManagement do?
Registers the infrastructure that finds @Transactional and creates the proxies.
Spring Boot switches it on for you.
Declaring a transaction
@Service
public class ProductServiceImpl implements ProductService {
// readOnly lets Hibernate skip dirty checking and tells the driver this will not
// write - cheaper, and an accidental write fails loudly.
@Override
@Transactional(readOnly = true)
public List<ProductDTO> getMenu() {
return mapper.mapProductsToProductDTOs(productDAO.findActiveMenu());
}
@Override
@Transactional
public ProductDTO createProduct(ProductCreateDTO dto) { ... }
}What does declarative transaction management mean? That you declare the boundary
with an annotation and the framework applies it, instead of writing
begin/commit/rollback by hand. The programmatic alternative —
TransactionTemplate — is still there for the cases where the boundary is not a whole
method.
Where can @Transactional go? On a method or on a class. On a class
it applies to every public method, with a method-level annotation overriding it. Put it on the
service layer: that is where a business operation begins and ends. A transaction per
repository call gives you one transaction per statement, which is no transaction at all.
Use the Spring annotation
(org.springframework.transaction.annotation.Transactional), not
jakarta.transaction.Transactional. Both compile. Only the Spring one supports
readOnly, isolation, timeout and the rollback rules.
Propagation
What does propagation mean? What happens when a transactional method is called by another transactional method.
| Propagation | Existing transaction | No transaction |
|---|---|---|
REQUIRED (default) | join it | start one |
REQUIRES_NEW | suspend it, start a new one | start one |
SUPPORTS | join it | run without |
NOT_SUPPORTED | suspend it, run without | run without |
MANDATORY | join it | throw |
NEVER | throw | run without |
NESTED | savepoint inside it | start one |
REQUIRES_NEW is the one with a real use case and a real trap. The use case: writing
an audit record that must survive the caller rolling back. The trap: it holds two connections at
once, so a pool with fewer connections than nesting depth deadlocks.
Isolation
What is an isolation level? How much one in-flight transaction can see of another's uncommitted work. Weaker is faster and admits more anomalies.
| Level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
READ_UNCOMMITTED | possible | possible | possible |
READ_COMMITTED | no | possible | possible |
REPEATABLE_READ | no | no | possible |
SERIALIZABLE | no | no | no |
- Dirty read — you read a row another transaction has not committed yet.
- Non-repeatable read — you read the same row twice and get different values.
- Phantom read — you run the same query twice and get a different set of rows.
Spring adds DEFAULT, meaning "whatever the database's own default is" — which is
REPEATABLE_READ on MySQL/InnoDB and READ_COMMITTED on PostgreSQL and
Oracle. Worth knowing, because the same code behaves differently on the two.
Rollback
What is the default rollback policy?
Roll back on RuntimeException and Error. Commit on a
checked exception.
That surprises people every time, and it is worth being able to say why: Spring treats an unchecked exception as an unexpected failure and a checked one as an anticipated outcome the caller is expected to handle. Override it explicitly when your domain disagrees:
@Transactional(rollbackFor = Exception.class) // roll back on checked too
@Transactional(noRollbackFor = ProductNotFoundException.class)And if you catch the exception yourself, there is no rollback — the proxy never sees it. Catching an exception inside a transactional method and returning normally commits everything done so far. If you catch it and still want the rollback, mark it yourself:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();What happens when a @Transactional method calls another
@Transactional method on the same object?
The second annotation is ignored entirely — including its propagation. Self-invocation does not go
through the proxy. A REQUIRES_NEW called that way just runs in the caller's
transaction, which is precisely the opposite of what was asked for. Move it to another bean.
Why is "unit of work" important, and why does JDBC autocommit violate it?
Because a business operation is usually several statements and is only correct if all of them happen. With autocommit on, every statement commits by itself, so a failure halfway leaves the database in a state that no business rule permits — an order row with no line items. Spring turns autocommit off for the duration of a transaction.
JPA
What do JPA and ORM stand for?
Jakarta Persistence API — a specification for object-relational mapping; Hibernate is the usual implementation. ORM is object-relational mapping: representing rows as objects and associations as references.
Benefits and drawbacks?
You write far less boilerplate, get dirty checking, caching, lazy loading and portability across
databases. In exchange you get a large abstraction whose failure modes — N+1 queries,
LazyInitializationException, surprising flush ordering — require you to know the SQL it
generates. ORM does not save you from understanding your database; it saves you from typing it.
What is the persistence context, and what is the EntityManager?
The persistence context is a first-level cache of managed entities plus their loaded state. The
EntityManager is the API you use to interact with it. One persistence context is
normally scoped to one transaction.
What it does for you: entities are unique within it (find twice returns the same
instance), and at flush time it compares each managed entity against its loaded state and issues
UPDATEs for what changed. That is dirty checking — and it is why this works with no
save() call at all:
@Override
@PreAuthorize("hasRole('ADMIN')")
@Transactional
public AdminUserDTO changeRole(String actingAdminEmail, UUID userId, UserRole role) {
User target = requireUser(userId); // managed from here on
target.setRole(role); // dirty checking will flush this
return toDto(userDAO.save(target)); // explicit save, but not strictly required
}The four entity states: new/transient (never persisted, not managed), managed (in the persistence context, changes tracked), detached (was managed, the context has closed), removed (scheduled for deletion at flush).
Touching a lazy association on a detached entity throws
LazyInitializationException. That single sentence explains the majority of JPA
questions people arrive with.
Why is @Entity needed and where does it go? On the class, to mark it
as mapped. It needs a no-arg constructor and an @Id. Field access is inferred from where
@Id is placed — annotate the fields, or annotate the getters, but never mix the two in
one class.
@Entity
@Table(name = "product")
@SQLRestriction("deleted = false") // Hibernate adds this to every query for this entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Two identifiers on purpose: id is the FK target and never leaves the server,
// publicId is what the API exposes. Sequential ids let anyone walk /api/products/1,2,3.
@JdbcTypeCode(SqlTypes.CHAR)
@Column(name = "public_id", nullable = false, unique = true, length = 36)
private UUID publicId;
@ToString.Exclude
@EqualsAndHashCode.Exclude
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.LAZY)
private List<ProductSize> sizes = new ArrayList<>();
}The two exclusions are load-bearing. Lombok's @Data would otherwise walk the
collection in toString and equals — forcing a lazy load on every log line,
and recursing forever through the child's back-reference. It is the most common way Lombok and JPA
go wrong together.
What do you need to configure to use JPA with Spring, and how does Boot make it easier?
By hand: a DataSource, a LocalContainerEntityManagerFactoryBean, a
JpaVendorAdapter, a JpaTransactionManager and a
persistence.xml or a packages-to-scan list. With
spring-boot-starter-data-jpa: a URL, a username and a password.
# Liquibase owns the schema, so Hibernate must not touch it. `validate` makes the app
# refuse to start if the entities and the tables have drifted apart - a loud failure at
# boot beats a silent mismatch discovered in production.
spring.jpa.hibernate.ddl-auto=validate
# Turn this OFF. Left on (the default), the persistence context stays open for the whole
# request, so lazy loads succeed in the view layer and hide N+1 queries until production.
spring.jpa.open-in-view=falseWhat does @PersistenceContext do? Injects a container-managed
EntityManager — actually a proxy that resolves to the one bound to the current
transaction, which is what makes it safe to hold in a singleton. With Spring Data you rarely need
it.
Spring Data repositories
What is an "instant repository", and why is it an interface?
An interface you declare and never implement — Spring Data generates the implementation at runtime as a proxy. It is an interface precisely so that it can be proxied: there is no class to write, so there is no class to get wrong.
public interface ProductRepository extends JpaRepository<Product, Long> {
// Derived from the method name: no body, no @Query.
List<Product> findByActiveTrueOrderByTypeAscDisplayOrderAsc();
Optional<Product> findByPublicId(UUID publicId);
boolean existsByNameIgnoreCase(String name);
// @EntityGraph fetches the sizes in the SAME query. Without it, listing 14 products
// is 1 query for the products plus 14 more for their sizes - the N+1 problem.
@EntityGraph(attributePaths = "sizes")
List<Product> findByTypeAndActiveTrueOrderByDisplayOrderAsc(ProductType type);
@EntityGraph(attributePaths = "sizes")
@Query("select p from Product p")
List<Product> findAllWithSizes();
}What is the naming convention for finder methods?
A prefix — findBy, readBy, getBy, countBy,
existsBy, deleteBy — then property names joined by And or
Or, then optional keywords: Between, LessThan,
Like, Containing, IgnoreCase, OrderBy…Asc/Desc,
True, False, In, IsNull. The property names must
match the entity's fields — a typo is a startup failure, not a runtime one, which is
a feature.
How are they implemented at runtime?
Spring Data creates a JDK dynamic proxy per repository interface, backed by
SimpleJpaRepository. Each call is routed to a query derived from the method name, a
declared @Query, or a named query — in that order of precedence.
What is @Query for? Writing the query yourself when the method name
would be unreadable or the query is not expressible by derivation. JPQL by default;
nativeQuery = true for real SQL.
Return Optional<T> for single results. A repository method
returning T returns null when there is no row, and the
NullPointerException then surfaces somewhere unrelated.
⚠️ One trap worth the whole lesson
@SQLRestriction("deleted = false") on the entity filters soft-deleted rows out of
every JPA query automatically. It does not apply to SQL you write yourself —
Hibernate adds it when it builds a query from the entity model, and JdbcTemplate never
goes near the entity model.
In the pizza API, omitting AND deleted = 0 from the reporting SQL counted
cancelled-and-deleted orders as revenue. Nothing failed. The reports stayed entirely plausible and
were simply wrong, which is why it took a while to notice.
What to remember
- Spring's data exceptions are unchecked and vendor-neutral;
@Repositoryenables the translation. @Transactionalgoes on the service layer, is implemented with a proxy, and rolls back on unchecked exceptions only.- Catching the exception yourself, or calling the method from a sibling, means no rollback and no transaction.
- Seven propagation modes;
REQUIREDis the default andREQUIRES_NEWcosts a second connection. - Lazy loading on a detached entity is
LazyInitializationException. Setopen-in-view=falseand fetch what you need with@EntityGraph. - Repository method names are checked at startup.