Stage 9: JDBC, JPA and Hibernate, lesson 6 of 6

Transactions, propagation and locking

Advanced3 min read@since 17Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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 = true for queries, so Hibernate skips dirty checking.
  • rollback: by default only on unchecked exceptions.

Locking:

  • Optimistic (@Version): no database lock; a conflicting update fails with OptimisticLockException. A good default for web apps.
  • Pessimistic (@Lock(PESSIMISTIC_WRITE), which issues SELECT ... FOR UPDATE): locks the row. Use it for hot rows such as the last seats in a batch.

Example

Java
@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

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.