Transactions, propagation and locking
Transactions are ACID: atomic, consistent, isolated and durable.
In Spring, @Transactional wraps a method in a transaction through a proxy.
Key attributes:
- propagation:
REQUIRED(the default: join the current transaction or start one),REQUIRES_NEW(always a new, independent transaction, for example audit logs),MANDATORY,NESTED. - isolation:
READ_COMMITTED(PostgreSQL's default),REPEATABLE_READ,SERIALIZABLE. readOnly = truefor queries, so Hibernate skips dirty checking.- rollback: by default only on unchecked exceptions.
Locking:
- Optimistic (
@Version): no database lock; a conflicting update fails withOptimisticLockException. A good default for web apps. - Pessimistic (
@Lock(PESSIMISTIC_WRITE), which issuesSELECT ... FOR UPDATE): locks the row. Use it for hot rows such as the last seats in a batch.
Example
@Service
public class EnrollmentService {
@Transactional
public void enroll(Long studentId, Long courseId) {
Course course = courseRepository.findByIdForUpdate(courseId); // row locked
if (course.getSeatsLeft() == 0) throw new SoldOutException(courseId);
course.decrementSeats();
enrollmentRepository.save(new Enrollment(studentId, courseId));
auditService.log("ENROLL", studentId); // REQUIRES_NEW: kept even if we roll back
}
}
public interface CourseRepository extends JpaRepository<Course, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select c from Course c where c.id = :id")
Course findByIdForUpdate(@Param("id") Long id);
}
@Service
class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String action, Long userId) { /* insert audit row */ }
}Common mistake
Calling this.saveAll() from a non-transactional method in the same bean and expecting @Transactional on saveAll to apply. The call never passes through the proxy.
Under the hood
Because @Transactional works through a proxy, self-invocation (calling a transactional method from another method of the same class) bypasses it, and so do private methods. Long transactions hold database connections and locks, so never call slow external APIs inside one. The classic anomalies are dirty reads, non-repeatable reads, phantom reads and lost updates; @Version prevents lost updates even at READ_COMMITTED.
Check yourself
By default, @Transactional rolls back onβ¦
How this connects
Know these first
Where this leads
Part of Job-ready backend developer, Crack the Java interview, Microservices and production.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.