spring-boot · intermediate
Spring Data JPA and Transactions — Persistence Without Surprises
Start here
Spring Data JPA helps Java services talk to relational databases using:
- Entities — classes mapped to tables
- Repositories — interfaces for save/find/query
- JPA/Hibernate — the engine that generates SQL
You should care because most Spring Boot backends persist data this way, and the bugs are legendary: lazy-loading outside a session, transactions not starting, partial commits, and N+1 queries.
What you will learn
- Map entities and repositories at a conceptual level.
- Explain what
@Transactionalactually does. - See proxy self-invocation pitfalls.
- Handle read-only transactions and rollback rules.
- Work a complete place-order service example.
- Avoid LazyInitializationException and N+1.
- Know when to drop to explicit SQL.
What you should know first
| Topic | Why |
|---|---|
| ACID transactions | DB transaction basics |
| Spring dependency injection | Proxies wrap beans |
| Basic SQL | What JPA eventually runs |
Words you need before we begin
| Term | Plain English |
|---|---|
| JPA | Jakarta Persistence API—standard mapping interfaces. |
| Entity | Object mapped to a table row. |
| Repository | Abstraction for data access methods. |
| Persistence context | Session of managed entities JPA tracks. |
| Lazy loading | Load related data only when accessed. |
| Flush | Synchronize pending SQL to the database. |
| @Transactional | Declares a transactional boundary on a Spring bean method. |
| Proxy | Wrapper Spring uses to start/commit/rollback around calls. |
| N+1 queries | One query plus one per child row—accidental chatty SQL. |
| OSIV | Open Session In View—web pattern that keeps session open during view rendering (controversial). |
Simple story: bank teller batch
A teller posts two ledger lines as one unit: debit and credit. If the second line fails, both must undo. @Transactional is the tray that says “these steps are one unit,” and the proxy is the supervisor who opens and closes that tray when someone calls the official window—not when the teller whispers to themselves in the back (self-invocation).
The problem without clear transaction boundaries
Service method:
- Save order.
- Save order lines.
- Call payment.
- Save payment result.
Step-by-step explanation
Step 1 — Model entities carefully
Ids, columns, relationships (@ManyToOne, @OneToMany). Prefer clear aggregate boundaries.
Step 2 — Define repositories
JpaRepository<Order, Long> plus query methods or @Query.
Step 3 — Place @Transactional on service layer
Controllers stay thin. Services own business transactions.
Step 4 — Understand the proxy
External call → proxy starts transaction → method runs → commit/rollback. Self-calls inside the same class bypass the proxy unless you restructure.
Step 5 — Rollback rules
Runtime exceptions roll back by default; checked exceptions may not—configure rollbackFor when needed.
Step 6 — Read-only transactions
@Transactional(readOnly = true) for query paths—hints and connection optimizations; still not an excuse for N+1.
Step 7 — Keep transactions short
Do not hold transactions open during remote HTTP calls. Persist intent, commit, then call external systems with idempotency (often with outbox).
Visual mental model
sequenceDiagram
participant C as Controller
participant P as Spring proxy
participant S as OrderService
participant R as Repository
participant DB as Database
C->>P: placeOrder()
P->>DB: BEGIN
P->>S: placeOrder()
S->>R: save
R->>DB: SQL
S-->>P: return
P->>DB: COMMIT
P-->>C: result
Learning question: If placeOrder calls this.saveLines() as an internal method marked @Transactional, does a new transaction start?
Caption: Usually no—self-invocation skips the proxy.
Complete worked example: place order
Starting situation
OrderService.placeOrder must create order header and lines atomically, then publish an event after commit.
Decisions
| Step | Approach |
|---|---|
| Create order+lines | @Transactional service method |
| Validation failures | Throw before writes or mark rollback |
| Payment provider call | After commit via event/outbox—not inside long TX |
| Read order for response | Query in short TX or return managed DTO carefully |
Code shape (illustrative)
@Service
public class OrderService {
@Transactional
public OrderId place(OrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
// lines cascade or explicit saves
outboxRepository.save(OrderPlaced.of(order));
return order.getId();
}
}
Failure behavior
- Unique constraint on idempotency key: transaction rolls back; client retries safely.
- Lazy load of lines in controller after TX:
LazyInitializationException—map to DTO inside TX or fetch join.
How it works in production
- Connection pools (HikariCP)
- Migration tools (Flyway/Liquibase)
- Metrics: TX duration, rollbacks, pool waits
- p6spy/logging for slow SQL in staging
Failure modes
| Mode | Symptom | Fix |
|---|---|---|
| Self-invocation | Annotation ignored | Move method to another bean |
| Lazy load after TX | Runtime exception | Fetch joins / DTO mapping in TX |
| N+1 | Slow endpoints | Join fetch, entity graphs, batch size |
| Long TX with HTTP | Pool exhaustion | Split TX; outbox |
| Wrong rollback rules | Partial commits | Configure rollbackFor |
| OSIV hiding problems | Works in web, fails in async | Prefer explicit fetch strategies |
Trade-offs
| Choice | Benefit | Cost |
|---|---|---|
| JPA repositories | Speed of development | Hidden SQL complexity |
| Explicit SQL/jOOQ | Control | More code |
| Wide aggregates | Convenience | Heavy loads, contention |
| Fine-grained TX | Short locks | More round trips |
Compare with related concepts
| Concept | Difference |
|---|---|
| ACID at DB | Still the foundation; Spring demarcates boundaries |
| Distributed saga | Cross-service; not one JPA TX |
| JDBC template | Lower-level SQL without full ORM |
Common misunderstandings
- “@Transactional on private methods works.” Proxies typically need public methods on Spring beans called externally.
- “Save flushes immediately always.” Flush modes vary; commit flushes.
- “Read-only means no locks ever.” DB isolation still applies.
- “JPA eliminates N+1.” It can cause them if misused.
- “One big transaction is safer.” Long transactions increase contention and failure blast radius.
Check your understanding
- What does a Spring transaction proxy do?
- Why might
@Transactionalappear to do nothing? - When do LazyInitializationExceptions appear?
- Why avoid remote calls inside transactions?
- What is N+1?
Practice
- Refactor a service that calls payment inside
@Transactional. - Write a fetch-join query plan for order+lines.
- Demonstrate self-invocation bug with a small sketch.
- Choose isolation level notes for a reporting query (conceptual).
- List metrics for transaction health in production.
Deeper production notes
Testing transactions
@DataJpaTest and @SpringBootTest with rollback defaults can hide commit issues. Include tests that assert committed state visible to another transaction/entity manager.
Multi-datasource and ChainedTransactionManager
Complex; avoid until necessary. Prefer single primary relational store per service.
Specialist caution
Flush timing, second-level cache, and locking (@Lock) have sharp edges. Treat financial ledgers with extra review beyond default JPA patterns.
Revision summary
- Spring Data JPA maps objects to tables; repositories run queries.
@Transactionaldefines atomic boundaries via proxies.- Keep TX short; avoid lazy traps and N+1.
- Combine with outbox for reliable external side effects.
- Know when raw SQL is clearer.
Glossary
| Term | Definition |
|---|---|
| Entity | Persistent domain object mapped to a table. |
| Persistence context | Tracking set for entity states. |
| @Transactional | Declarative transaction boundary. |
| Lazy loading | Defer loading associations until accessed. |
Abbreviations and terminology
- JPA — Jakarta Persistence API
- ORM — Object-Relational Mapping
- AOP — Aspect-Oriented Programming (proxy advice)
- OSIV — Open Session In View
- DTO — Data Transfer Object
What to learn next
Additional teaching scenarios
Scenario A — busy day
Imagine traffic multiplies by ten for a marketing event. Re-read the failure modes section and mark which ones become likely first. Write the first mitigation you would take for each marked item. This exercise turns abstract lists into operational instincts.
Scenario B — partial deploy
Half of your instances run the new version and half run the old version. Which assumptions in this lesson break if the two versions disagree about protocols, message fields, or transaction boundaries? Prefer designs that tolerate mixed versions for at least one deploy window.
Scenario C — explain to a new teammate
In five sentences, teach the core idea of this lesson without acronyms. If you cannot, the mental model is not yet solid—revisit the simple story and worked example until the five sentences feel natural.
Scenario D — metric design
List three metrics and one alert threshold you would ship with this mechanism. Good metrics name the user impact or the resource that runs out, not only that a counter incremented.
Scenario E — deliberate non-goals
Write two problems this lesson's technique should not solve. Explicit non-goals prevent cargo-cult adoption where every service gets the same machinery whether it needs it or not.
Scenario F — capacity napkin math
Estimate requests or messages per second at peak, multiply by payload size, and ask whether your chosen design still holds. Napkin math catches fantasy architectures before they meet production invoices.
Scenario G — ownership checklist
Name the team that owns dashboards, the team that owns code changes, and the team that gets paged. If any is blank, fix ownership before enabling the feature broadly.
Additional teaching scenarios
Scenario A — busy day
Imagine traffic multiplies by ten for a marketing event. Re-read the failure modes section and mark which ones become likely first. Write the first mitigation you would take for each marked item. This exercise turns abstract lists into operational instincts.
Scenario B — partial deploy
Half of your instances run the new version and half run the old version. Which assumptions in this lesson break if the two versions disagree about protocols, message fields, or transaction boundaries? Prefer designs that tolerate mixed versions for at least one deploy window.
Scenario C — explain to a new teammate
In five sentences, teach the core idea of this lesson without acronyms. If you cannot, the mental model is not yet solid—revisit the simple story and worked example until the five sentences feel natural.
Scenario D — metric design
List three metrics and one alert threshold you would ship with this mechanism. Good metrics name the user impact or the resource that runs out, not only that a counter incremented.
Scenario E — deliberate non-goals
Write two problems this lesson's technique should not solve. Explicit non-goals prevent cargo-cult adoption where every service gets the same machinery whether it needs it or not.
FAQ from first-time learners
Q: Should controllers be transactional?
A: Prefer service-layer transactions so web concerns stay separate.
Q: Is Hibernate the same as JPA?
A: Hibernate is a common JPA implementation.
Q: Do I need @Transactional for single save?
A: Repository methods may be transactional themselves; service-level TX still helps multi-step use cases.
Track: Java Backend Engineering
Previous: Spring MVC REST APIs
Next: Spring Security OAuth2 Resource Server (JWT)
Series: Spring Boot Production
- Spring Core Dependency Injection
- Spring Bean Lifecycle
- Spring Boot Startup Lifecycle
- Spring MVC REST APIs
- Spring Data JPA and Transactions — Persistence Without Surprises (this guide)
- Spring Security OAuth2 Resource Server (JWT)
- Spring Boot Best Practices
By Shubham Jain