java
45 lines · 8 steps
Optimistic locking with a version column in JDBC
A repository detects concurrent writes by matching a version number on update, rejecting stale changes.
Explained by
highlit
1public class AccountRepository {
2
3 private final DataSource dataSource;
4
5 public AccountRepository(DataSource dataSource) {
6 this.dataSource = dataSource;
7 }
8
9 public Account findById(long id) throws SQLException {
10 String sql = "SELECT id, balance, version FROM accounts WHERE id = ?";
11 try (Connection conn = dataSource.getConnection();
12 PreparedStatement ps = conn.prepareStatement(sql)) {
13 ps.setLong(1, id);
14 try (ResultSet rs = ps.executeQuery()) {
15 if (!rs.next()) {
16 throw new NoSuchElementException("Account not found: " + id);
17 }
18 return new Account(
19 rs.getLong("id"),
20 rs.getBigDecimal("balance"),
21 rs.getLong("version")
22 );
23 }
24 }
25 }
26
27 public void update(Account account) throws SQLException {
28 String sql = "UPDATE accounts SET balance = ?, version = version + 1 "
29 + "WHERE id = ? AND version = ?";
30 try (Connection conn = dataSource.getConnection();
31 PreparedStatement ps = conn.prepareStatement(sql)) {
32 ps.setBigDecimal(1, account.getBalance());
33 ps.setLong(2, account.getId());
34 ps.setLong(3, account.getVersion());
35
36 int affected = ps.executeUpdate();
37 if (affected == 0) {
38 throw new OptimisticLockException(
39 "Account " + account.getId() + " was modified concurrently; "
40 + "expected version " + account.getVersion());
41 }
42 account.setVersion(account.getVersion() + 1);
43 }
44 }
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A version column lets you detect concurrent edits without holding a lock for the whole read-modify-write cycle.
- 2When an update's WHERE clause fails to match, an affected-row count of zero is your signal that the row changed underneath you.
- 3Try-with-resources guarantees connections, statements, and result sets close even when queries throw.
Related explainers
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
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
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-a-version-column-in-jdbc-explained-java-3109/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.