A support ticket arrived with a screenshot of a product page showing one price and a checkout page showing another. The price had been changed that morning, and the cache had been serving the old value for nearly three hours.
On paper, our invalidation was textbook, prices were read through a cache-aside pattern: check Redis, fall back to the database on a miss, store the result with a three-hour TTL. When a price changed, the service updated the database and then evicted the key. Update, then evict, it is the sequence most articles recommend, and the code reviewers had approved it for exactly that reason.
The flaw was where the eviction ran. It sat inside the same transaction as the update, which meant it executed before the commit. For a short window, Redis was empty while the database still held the old price. If a reader arrived in that window, it missed the cache, read the old price from the database, and wrote it back to Redis with a fresh three-hour TTL. Then the writer committed. The database now held the new price, the cache held the old one, and nothing was scheduled to correct it until the TTL expired.
The window was only a few milliseconds wide, which is why it took a busy morning and a bulk price change on popular products to expose it. A product page that receives dozens of requests a second will put a reader into a few-millisecond window surprisingly often, and we ship price changes in bulk.
Two changes closed it. The eviction now runs after the transaction commits, so a reader that misses the cache can only ever load a value that is already final. And the TTL dropped from three hours to five minutes as a backstop. The after-commit eviction handles the normal case, and the TTL bounds the damage when something bypasses it, such as a direct database update during a data fix, or an eviction that fails because Redis was briefly unreachable.
The order "update, then evict" is only half of a correct strategy. The other half is when the eviction becomes visible relative to the commit, and a long TTL turns any mistake in that timing into a long-lived one. Treat the TTL as the safety net for your invalidation logic, not as a freshness policy, and size it by how long you can tolerate being wrong.
More like this?
I write short production war stories like this one, plus a deep-dive architecture series. Subscribe here if you want both in your inbox.
Recommended Resources for Java & Spring Boot Engineers
If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot & Architecture skills, check out these highly-rated resources from the Javarevisited publication (Use promo code friends20 for an exclusive 20% discount automatically applied at checkout):
- Grokking the Java Interview Prepare for core Java, concurrency, JVM internals, and design pattern questions. Get Full Book (Paid) | Download Free Sample Copy
- Grokking the Spring Boot Interview Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices. Get Full Book (Paid) | Download Free Sample Copy
- Grokking the SQL Interview Deep-dive into query optimization, indexing, joins, and complex SQL window functions. Get Full Book (Paid) | Download Free Sample Copy
- The Complete Java + Spring + SQL Interview Bundle Get all three interview guides in a single heavily discounted package. Get the Ultimate Interview Bundle
- Spring Professional Certification Practice Questions (250+ Questions) Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification. Get the Spring Professional Questions | Download Free Sample Copy


Top comments (0)