When you annotate a method or class with:
@Transactional
public void myMethod() {
// some DB logic
}…it works without you writing any boilerplate like opening/committing/rolling back transactions.
Spring wraps your bean in a proxy at runtime if it sees @Transactional.
There are 2 types of proxies:
- JDK Dynamic Proxy – if your class implements an interface
- CGLIB Proxy – if your class does not implement an interface
🔁 These proxies wrap your bean method calls and allow Spring to inject extra logic, like transaction handling, before/after your actual method runs.
When you call a transactional method like:
userService.createUser();This is what happens behind the scenes:
-
Proxy intercepts the call
-
Checks for
@Transactional -
Delegates to TransactionInterceptor
-
TransactionInterceptor interacts with PlatformTransactionManager
-
The transaction manager (e.g.,
DataSourceTransactionManagerfor JDBC) does:- Starts transaction (get connection, set autocommit false)
- Executes your method
- Commits if success, rolls back if exception
| Component | Role |
|---|---|
@Transactional |
Metadata that marks a method for transaction |
TransactionInterceptor |
Core logic to handle begin, commit, rollback |
PlatformTransactionManager |
SPI for managing transactions (like JDBC, JPA) |
TransactionAttributeSource |
Parses the attributes from @Transactional |
TransactionManager |
Concrete class (e.g., DataSourceTransactionManager, JpaTransactionManager) |
Caller --> Proxy --> TransactionInterceptor
--> TransactionManager (begin)
--> Actual Method Call
--> TransactionManager (commit or rollback)
If you call repository.save() without @Transactional, JPA (Hibernate) defers DB writing until flush time — but:
- If you're inside a Spring-managed method with no
@Transactional, the default is non-transactional. - Some operations may still work because Hibernate can work with its session-level transaction (but it’s risky).
- Best practice: use
@Transactionalat the service layer.
@Service
public class UserService {
@Transactional
public void registerUser(User user) {
userRepository.save(user); // triggers DB insert
// any exception here will rollback the transaction
}
}Here, Spring:
- Starts transaction at method entry
- Commits at method exit (if no exception)
- Rolls back if runtime exception is thrown
- If you call a
@Transactionalmethod from within the same class, the proxy is bypassed. So transaction will not work as expected.
public void outerMethod() {
innerTransactionalMethod(); // doesn't go through proxy, so @Transactional is ignored
}| Concept | How It Works |
|---|---|
| Proxy | Spring creates a proxy to intercept method calls |
| @Transactional | Parsed by AOP and injected into proxy logic |
| TransactionInterceptor | Applies begin, commit, rollback logic |
| PlatformTransactionManager | Concrete logic for JDBC, JPA, etc. |
| No manual code | You just use @Transactional and Spring does the rest |
Spring handles transactions using AOP proxies. That means:
- Your bean is wrapped in a proxy object
- When an external class calls a method like
userService.saveUser(), it goes through the proxy, which handles the transaction logic.
BUT...
If a method within the same class calls another @Transactional method, it’s a direct call, like this.someTransactionalMethod(), which bypasses the proxy. So the transaction logic is never triggered.
@Service
public class UserService {
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}// External call goes through proxy — transaction starts
userService.saveUser(user);✔ Transaction works because Spring's proxy intercepts the method call.
@Service
public class UserService {
public void registerUser(User user) {
// calling transactional method inside same class
saveUser(user); // ❌ proxy is bypassed
}
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}⛔ Here, registerUser() calls saveUser() directly inside the same class. So the call doesn't go through the proxy, and @Transactional is ignored.
| Call Type | Goes Through Proxy | Transaction Applies |
|---|---|---|
| Called from another bean | ✅ Yes | ✅ Yes |
| Called from same class | ❌ No | ❌ No |
Called via this.method() |
❌ No | ❌ No |
Put transactional methods in a separate service class, so they're always called through a proxy.
public void save(User user) {
userRepository.save(user);
}You asked why this works without @Transactional.
-
When you call
repository.save()(e.g., JPA), Hibernate will queue the insert in its persistence context. -
If no transaction exists, the actual SQL might:
- Run in auto-commit mode (one statement = one transaction)
- Or may be deferred until flush
But:
With @Transactional |
Without @Transactional |
|---|---|
| All operations in one unit | Each statement might run in auto-commit |
| Rollback possible on exception | No rollback; partial updates may happen |
| Safe for multiple operations | Risky if multiple steps depend on one another |
Without @Transactional, if you're:
- Saving multiple entities
- Modifying multiple tables
- Doing multiple operations that depend on each other
…then failure in one step will not rollback the others!
| Concept | Explanation |
|---|---|
Spring uses AOP proxies to handle @Transactional |
|
| Internal method calls bypass proxies — transactions don’t apply | |
Without @Transactional, DB may auto-commit after each statement |
|
Always annotate service-layer methods with @Transactional for safety |
|
| Use separate service classes if needed to avoid internal call issue |
- Most JPA providers (like Hibernate) can run read queries without needing a transaction.
- They auto-open a session and execute the query.
For example:
User user = userRepository.findById(1L).orElseThrow();This works without @Transactional, and it fetches data just fine.
Even though it's optional for reads, you may want to add:
@Transactional(readOnly = true)-
Performance Boost
- Tells Hibernate: "This is a read-only operation"
- So it skips dirty checking, caching, etc.
-
Consistency
- For databases that require transactions even for reads (like PostgreSQL with repeatable reads), it ensures consistent reads.
If you fetch an entity and try to access a lazy-loaded relation (e.g., user.getRoles()) outside of a transaction, you'll get:
LazyInitializationException
Because Hibernate needs a transactional context to load the relation.
So for complex reads involving lazy relations, wrap in:
@Transactional(readOnly = true)
public User getUserWithRoles(Long id) {
User user = userRepository.findById(id).orElseThrow();
user.getRoles().size(); // triggers lazy load
return user;
}| Operation | Needs @Transactional? |
Notes |
|---|---|---|
findById(), findAll() |
❌ No | Works fine without it |
| Complex reads with lazy loading | ✅ Recommended (readOnly = true) |
Prevents LazyInitializationException |
Write operations (save, delete) |
✅ Required | Ensures rollback & atomicity |
| Scenario | Notes |
|---|---|
| ✅ Simple Reads | No @Transactional needed |
| ✅ Writes/Deletes | Use @Transactional to ensure commit/rollback |
| 🚫 Internal method call (same class) | Proxy bypassed → @Transactional won't work |
✅ @Transactional(readOnly = true) |
Optimized for performance on reads |
| ✅ Batch save | Use @Transactional to group all saves/rollbacks |
| Interface | Use Case |
|---|---|
Repository |
Marker interface |
CrudRepository |
Basic CRUD |
PagingAndSortingRepository |
CRUD + Pagination/Sorting |
JpaRepository |
Full JPA support + Specifications |
MongoRepository |
For MongoDB-specific ops |
ListCrudRepository |
Same as CrudRepository but returns List instead of Iterable |
| Concept | Purpose |
|---|---|
@NoRepositoryBean |
Prevents base repo instantiation |
@RepositoryDefinition |
Use if not extending any Spring Data interfaces |
SimpleJpaRepository |
Default internal implementation behind proxies |
| Use Case |
|---|
| Fine-grained control over persistence context (flush, detach, etc.) |
| Custom complex queries |
| Batch inserts/updates with manual transaction control |
| Feature | Example |
|---|---|
| Derived query method | findByEmailAndStatus() |
| Custom query | @Query("SELECT u FROM User u WHERE ...") |
| Proxy-based transaction | Created by Spring AOP for methods with @Transactional |
- Lazy load outside transaction →
LazyInitializationException - Calling
@Transactionalmethod from same class → no transaction - Avoid mixing transactional and non-transactional logic in same method