java
57 lines · 7 steps
Optimistic locking with @Version in Spring
A JPA @Version field plus @Retryable lets concurrent withdrawals stay correct by retrying when writes collide.
Explained by
highlit
1@Entity
2@Table(name = "accounts")
3public class Account {
4
5 @Id
6 @GeneratedValue(strategy = GenerationType.IDENTITY)
7 private Long id;
8
9 @Column(nullable = false)
10 private String owner;
11
12 @Column(nullable = false)
13 private BigDecimal balance;
14
15 @Version
16 private long version;
17
18 public void withdraw(BigDecimal amount) {
19 if (balance.compareTo(amount) < 0) {
20 throw new InsufficientFundsException(id, amount);
21 }
22 this.balance = balance.subtract(amount);
23 }
24
25 public Long getId() {
26 return id;
27 }
28
29 public BigDecimal getBalance() {
30 return balance;
31 }
32
33 public long getVersion() {
34 return version;
35 }
36}
37
38@Service
39public class AccountService {
40
41 private final AccountRepository accounts;
42
43 public AccountService(AccountRepository accounts) {
44 this.accounts = accounts;
45 }
46
47 @Retryable(
48 retryFor = ObjectOptimisticLockingFailureException.class,
49 maxAttempts = 3,
50 backoff = @Backoff(delay = 50))
51 @Transactional
52 public void withdraw(Long accountId, BigDecimal amount) {
53 Account account = accounts.findById(accountId)
54 .orElseThrow(() -> new AccountNotFoundException(accountId));
55 account.withdraw(amount);
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A @Version column lets the database reject stale writes instead of locking rows up front.
- 2Optimistic locking trades pessimistic blocking for occasional retries, which suits low-contention workloads.
- 3Wrapping the transaction in @Retryable turns a lost-update conflict into a transparent second attempt.
Related explainers
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/optimistic-locking-with-version-in-spring-explained-java-64d6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.