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
rust
use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::thread;
Aggregating metrics across threads in Rust
concurrency
shared-state
mutex
Intermediate
7 steps
java
public class ThumbnailProcessor { private static final int MAX_CONCURRENCY = 4;
Bounded parallel thumbnail rendering in Java
concurrency
thread-pool
futures
Intermediate
7 steps
typescript
type RetryOptions = { retries?: number; timeoutMs?: number; baseDelayMs?: number;
Retry with timeout and backoff in TypeScript
promises
retry
exponential-backoff
Intermediate
10 steps
rust
use std::sync::{mpsc, Arc, Mutex}; use std::thread; use std::time::Duration;
Building a thread pool in Rust
concurrency
channels
thread-pool
Advanced
9 steps
java
public class SortedListMerger { public static int[] merge(int[] a, int[] b) { int[] result = new int[a.length + b.length];
Merging two sorted arrays in Java
two-pointers
merging
arrays
Beginner
6 steps
go
package cache import ( "container/list"
Building a generic LRU cache in Go
lru-cache
generics
linked-list
Intermediate
8 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.