Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

Transaction Isolation and Data Consistency

Recommended resources

Hello! Welcome to the next lesson in our "Spring Data & Transaction Management" module.

In our last lesson, we focused on defining the scope and propagation of transactions with @Transactional. We learned how Spring uses proxies to manage transaction boundaries and how propagation levels like REQUIRED and REQUIRES_NEW control the behavior when transactional methods call each other.

Today, we address the "I" in ACID: Isolation. While propagation defines how transactions relate to each other logically, isolation controls how they behave when running concurrently and accessing the same data. Our goal is to explain transaction isolation levels and their impact on data consistency issues like dirty reads and phantom reads. This is a fundamental concept in database engineering and a common topic in senior-level interviews, as it directly relates to building robust, high-concurrency systems.

1. The Challenge of Concurrency: Transactional Anomalies

In a system with multiple users, it's inevitable that multiple transactions will try to read and write the same data simultaneously. Without proper controls, this can lead to data becoming inconsistent. These inconsistencies are often called transactional anomalies.

Before we explore the solutions, let's define the three main problems we need to solve.

Understanding Isolation Levels in Transactions with Java ...

The article 'Understanding Isolation Levels in Transactions with Java' provides clear and concise definitions of these core anomalies. Understanding them is key to the rest of this lesson.

Read the section 'Common Transactional Anomalies' to get a formal definition of Dirty Read, Non-repeatable Read, and Phantom Read.

To summarize those definitions:

  • Dirty Read: A transaction reads data that has been modified by another transaction but has not yet been committed. If the modifying transaction rolls back, the first transaction is left with "dirty," invalid data.
  • Non-Repeatable Read: A transaction reads a specific row of data. Before the transaction finishes, a second transaction modifies that same row and commits. If the first transaction reads the same row again, it gets a different value. The read is "non-repeatable."
  • Phantom Read: A transaction executes a query that retrieves a set of rows (e.g., SELECT * FROM products WHERE category = 'laptops'). A second transaction then inserts a new row that matches the query's criteria and commits. If the first transaction re-executes its query, it will see a "phantom" row that wasn't there before.

Transaction isolation levels are the mechanism databases provide to prevent these anomalies, offering a trade-off between data consistency and system performance.


2. The Four Standard Isolation Levels

Spring allows you to specify an isolation level on the @Transactional annotation, like so: @Transactional(isolation = Isolation.READ_COMMITTED). Let's explore what each level does, what problems it solves, and see it in action.

The following video provides an excellent hands-on demonstration of each isolation level using a multithreaded Spring Boot application. We'll walk through it step-by-step.

Spring Boot Transaction Isolation Explained | Real-Time Hands-On Example | ‪@Javatechie

This video from the Java Techie channel will be our main guide. It simulates two concurrent transactions (as two threads) to clearly demonstrate the behavior of each isolation level.

Watch the introductory segments from 00:41 to 02:30. This will set the stage by defining isolation and listing the levels available in Spring.

A. READ_UNCOMMITTED: The Wild West

  • Behavior: The lowest isolation level. It offers no protection from any of the anomalies. Transactions can read uncommitted changes from other transactions.
  • Problems Solved: None.
  • Performance: Highest concurrency, as it uses minimal locking.
  • Use Case: Very rare. Might be used for high-throughput, non-critical tasks like logging, where eventual consistency is acceptable and performance is paramount.

Let's see the "dirty read" problem in action.

Spring Boot Transaction Isolation Explained | Real-Time Hands-On Example | ‪@Javatechie

The video now demonstrates a dirty read. Transaction A updates a stock value but hasn't committed. Transaction B reads this uncommitted value. Then, Transaction A rolls back, leaving Transaction B with incorrect data.

Watch from 04:21 to 21:46. This is a long segment, but it's crucial. The first part (until 10:46) explains the concept with a stock update and a movie ticket analogy. The second part (from 10:46) is a detailed code walkthrough showing the dirty read and a subsequent rollback scenario. Pay close attention to the console output showing Transaction B reading the uncommitted value '5'.

B. READ_COMMITTED: The Sensible Default

  • Behavior: Ensures a transaction can only read data that has been committed.
  • Problems Solved: Dirty Reads.
  • Problems Remaining: Non-Repeatable Reads, Phantom Reads.
  • Performance: Good balance of consistency and performance. This is the default isolation level for many databases, including PostgreSQL and Oracle.
  • Use Case: Most general-purpose application logic. A classic example is a bank transfer, where you must not see an intermediate balance before the entire transfer is committed.

Now, let's see how READ_COMMITTED fixes the dirty read problem.

Spring Boot Transaction Isolation Explained | Real-Time Hands-On Example | ‪@Javatechie

The video continues by changing the isolation level to READ_COMMITTED and re-running the same scenario.

Watch from 21:46 to 29:35. Notice how Transaction B now reads the last committed value ('50'), not the uncommitted value ('5') from Transaction A. This successfully prevents the dirty read.

C. REPEATABLE_READ: Ensuring Consistency Within a Transaction

  • Behavior: Guarantees that if a transaction reads a row multiple times, it will see the exact same data each time. The database achieves this by placing locks on the rows that are read.
  • Problems Solved: Dirty Reads, Non-Repeatable Reads.
  • Problems Remaining: Phantom Reads.
  • Performance: Lower concurrency than READ_COMMITTED due to more extensive locking. This is the default in MySQL.
  • Use Case: Useful when a transaction needs to read a value, perform logic, and then write a new value based on the original read. For example, checking an inventory stock level and then decrementing it.

The next segment demonstrates how this level prevents a non-repeatable read.

Spring Boot Transaction Isolation Explained | Real-Time Hands-On Example | ‪@Javatechie

Here, the demonstration is updated. Transaction B will now read the stock value twice. In between these two reads, Transaction A updates the stock and commits. Let's see what Transaction B reads the second time.

Watch from 29:35 to 39:12. The key insight is that even though Transaction A updated the value to '5' and committed, Transaction B's second read still gets '40'—the same value it got on its first read. The read is repeatable.


3. A Deeper Look: The Phantom Read Anomaly

The phantom read is the most subtle of the three anomalies. REPEATABLE_READ protects you from updates to rows you've read, but it doesn't stop other transactions from inserting new rows that would match your query criteria.

This can lead to inconsistencies. For example, a transaction could count the number of line items in an order, get 3, and then another transaction adds a new line item. If the first transaction re-runs the count, it gets 4. Or, worse, it might fetch the items to display and show 4, while a "total items" field calculated earlier still says 3.

Understanding Phantom Reads Problem with hands on examples

To understand this tricky concept, let's watch a dedicated video from Arpit Bhayani. He provides a fantastic, practical example of how a phantom read can cause data inconsistency in a real-world scenario.

Watch from the beginning to 10:25. Pay close attention to the scenario: a transaction counts posts (gets 4), another transaction inserts a new post and commits, and then the first transaction fetches the posts to return to the user (gets 5). The count (4) and the actual data (5 rows) are now inconsistent. This is a phantom read.


4. SERIALIZABLE: The Fortress of Consistency

  • Behavior: The highest level of isolation. It effectively executes transactions one after another (serially).
  • Problems Solved: Dirty Reads, Non-Repeatable Reads, and Phantom Reads.
  • Performance: Lowest concurrency. This can cause significant performance bottlenecks in high-traffic systems because it essentially eliminates parallel execution of conflicting transactions.
  • Use Case: Critical operations where absolute data consistency is non-negotiable and performance can be sacrificed, such as booking a unique flight seat or preventing over-subscription to a limited-offer product.

Let's see how SERIALIZABLE forces sequential execution.

Spring Boot Transaction Isolation Explained | Real-Time Hands-On Example | ‪@Javatechie

The final demonstration in the Java Techie video shows the effect of using SERIALIZABLE.

Watch from 39:12 to 45:32. The console output is the key takeaway: Transaction A fully completes its execution before Transaction B even begins. There is no concurrent execution, thus no possibility of anomalies.


5. Summary and Interview Focus

Here is a table summarizing what we've learned. It shows which anomalies are possible at each isolation level.

Transaction Isolation Levels and Concurrency Phenomena
This table shows which transactional anomalies (Dirty Read, Non-repeatable Read, Phantom Read) are prevented by each standard isolation level. 'Yes' means the anomaly can occur; 'No' means it is prevented. Notice that as consistency increases (from bottom to top), performance generally decreases.
Test your understanding!

An interviewer asks: "You are designing a ticket booking system for a popular concert. Multiple users might try to book the last few remaining seats at the same time. Which isolation level would you recommend for the 'book seat' operation, and what are the trade-offs?"

Show answer

This is a great question to showcase nuanced thinking.

  1. Start with the Safest: The most critical requirement is to prevent overbooking. SERIALIZABLE provides the strongest guarantee. You would explain that by using SERIALIZABLE, the database will ensure that only one 'book seat' transaction can execute at a time for the same event, completely eliminating the risk of two people booking the same seat.
  2. Acknowledge the Trade-off: The major trade-off is performance. You'd state that SERIALIZABLE will create a bottleneck, as concurrent booking requests will be queued and processed one by one. This might lead to a poor user experience (long loading times) during peak demand.
  3. Discuss Alternatives (Bonus Points): You could then mention alternative strategies. While REPEATABLE_READ would prevent a non-repeatable read on the seat's status, it wouldn't prevent phantom reads if you were, for example, counting available seats in a section. A more advanced solution would be to use a lower isolation level (like READ_COMMITTED) combined with optimistic locking (using a @Version column in your entity). This allows for higher concurrency and handles conflicts by throwing an exception if two users try to update the same seat simultaneously. We will cover optimistic locking in a future lesson.

This multi-layered answer demonstrates a deep understanding of not just the definitions, but the practical trade-offs involved in system design.


Conclusion

You have now mastered one of the most important—and often misunderstood—aspects of transaction management. Understanding isolation levels is crucial for writing correct, robust code for concurrent environments.

Key Takeaways:

  • Transaction isolation is a trade-off between data consistency and system performance/concurrency.
  • READ_UNCOMMITTED is the fastest but offers no protection.
  • READ_COMMITTED is a balanced default, preventing dirty reads.
  • REPEATABLE_READ adds protection against non-repeatable reads but is vulnerable to phantom reads.
  • SERIALIZABLE provides complete protection by executing transactions serially, at a significant cost to concurrency.
  • The choice of isolation level is not global; it should be determined by the specific business requirements of the transactional method.

Preview of the Next Lesson:
We've spent a lot of time on correctness and concurrency control. Now, let's switch gears back to performance. In our next lesson, we will tackle one of the most common performance pitfalls in applications using JPA/Hibernate: the N+1 query problem. You will learn how to diagnose it and solve it efficiently using JOIN FETCH and @EntityGraph.