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 axum::{extract::{Path, State}, http::StatusCode, Json}; use dashmap::DashMap; use serde::Serialize; use std::sync::Arc;
Request coalescing in an Axum handler
caching
concurrency
request-coalescing
Advanced
8 steps
java
public interface GitHubClient { @GetExchange("/users/{username}") GitHubUser getUser(@PathVariable String username);
Declarative HTTP clients in Spring
http-client
declarative-api
proxy
Intermediate
8 steps
php
<?php namespace App\Console\Commands;
Releasing stale document locks in Laravel
artisan-command
transactions
row-locking
Intermediate
6 steps
java
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;
Building a trie for autocomplete in Java
trie
prefix-tree
recursion
Intermediate
8 steps
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, catchError, concatMap, finalize } from 'rxjs'; import { DataSource, QueryRunner } from 'typeorm';
Wrapping requests in a transaction with NestJS
interceptors
transactions
rxjs
Advanced
7 steps
go
package config import ( "fmt"
A thread-safe config singleton in Go
singleton
concurrency
environment-variables
Intermediate
7 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.