๐ Optimistic vs Pessimistic Locking in PostgreSQL: Using FOR UPDATE Effectively with Spring Bootโฆ
Concurrency control is one of the most critical aspects in enterprise systemsโโโespecially when multiple users or services modify sharedโฆ
๐ Concurrency Control in PostgreSQL 16 with Spring Boot JdbcTemplate โ A Practical Tutorial
Concurrency control is a crucial concern for enterprise apps, especially when using PostgreSQLโs FOR UPDATE locking with Spring Boot JdbcTemplate for safe, transactional updates.
PostgreSQL offers powerful row-level locking mechanisms like SELECT โฆ FOR UPDATE to help prevent dirty writes, lost updates, and race conditions. When combined with Spring Bootโs transaction management and **JdbcTemplate**, this becomes an elegant solution for safely handling concurrent updates.
๐ง 1. Understanding FOR UPDATE
The FOR UPDATE clause in PostgreSQL locks selected rows so that:
- Other transactions cannot update or delete these rows until the current transaction completes.
- Other transactions attempting to
SELECT โฆ FOR UPDATEon the same rows will wait (or fail ifNOWAITis used).
Syntax
SELECT * FROM account WHERE account_id = 1001 FOR UPDATE;
This locks the row for the current transaction until itโs committed or rolled back.
๐ฆ2. Common Use Cases
- Banking / Financial Transactions โ Prevent two transactions from updating the same account balance simultaneously.
- Inventory Management โ Ensure stock levels arenโt oversold.
- Crew / Flight Assignment Systems โ Prevent assigning the same crew or aircraft twice in parallel updates.

In Spring, the @Transactional annotation marks a method (or class) so that all database calls within it run in one transaction. What this gives you:
โ Atomicity โ either all the operations inside succeed, โ Rollback on error โ if an exception occurs anywhere inside, all changes are undone so the database stays consistent.
This works with JdbcTemplate too โ behind the scenes Spring wraps the method in JDBC transaction logic: it disables auto-commit, then commits on success or rolls back on failure.
When you lock rows withFOR UPDATE, validate and update them, you want that sequence to be atomic โ thatโs exactly what @Transactional gives you.
๐ FOR UPDATE โ Lock rows for safe updates
*SELECT ... FOR UPDATE tells PostgreSQL to lock the rows you select so no other transaction can modify them until you finish your transaction. That means:*
- If Transaction A runs
FOR UPDATEon a row, Transaction B must wait before it can update that row. - This prevents situations where two threads read stale data and then write conflicting updates.
- You typically use this before doing an update so that no one else can sneak in changes mid-transaction.
๐ Use case: Lock a flight record before assigning crew or updating aircraft details so that no two processes can step on each other.
SKIP LOCKEDandNOWAITgive you different ways to handle contention depending on whether you want to wait, skip, or fail fast.
๐ SKIP LOCKED โ Ideal for multi-threaded/distributed queues
SKIP LOCKED is an extension you add to FOR UPDATE like:
SELECT * FROM jobs WHERE status='PENDING' FOR UPDATE SKIP LOCKED;
What it does:
- If another transaction has already locked a row, this query skips that row instead of waiting.
- That lets multiple workers pull distinct tasks from a queue without blocking on the same locked rows.
๐ Use case: In a pool of workers that assign flights/crew, you can have each worker pick the next unlocked job instead of waiting on locked ones.
๐ 5) NOWAIT โ Fail fast if lock canโt be acquired
With NOWAIT, you tell PostgreSQL: โIf the row is locked, donโt wait โ just fail now.โ For example:
SELECT * FROM flight WHERE id=? FOR UPDATE NOWAIT;
- If another transaction has the lock, you get an immediate error instead of waiting.
- This is useful in UI real-time environments where waiting is unacceptable and you want to report lock contention immediately.
๐ Use case: A dispatcher tries to assign crew to a flight; if someone else is updating it, you immediately return a conflict message to the user.
โ๏ธ 4. Spring Boot + PostgreSQL Setup
Letโs simulate a bank account transfer scenario using **FOR UPDATE** for concurrency-safe debit/credit operations.
๐งพ Example Table
CREATE TABLE account (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
balance NUMERIC(15, 2)
);
Sample Data:
INSERT INTO account (name, balance) VALUES
('Alice', 1000.00),
('Bob', 500.00);
๐ง 5. Spring Boot Application Setup
Maven Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
Application Properties
spring.datasource.url=jdbc:postgresql://localhost:5432/demo
spring.datasource.username=postgres
spring.datasource.password=admin
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.open-in-view=false
๐ก 6. Transactional Service using FOR UPDATE
Hereโs where the magic happens.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.sql.ResultSet;
@Service
public class AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void transferMoney(long fromId, long toId, BigDecimal amount) {
// Lock both rows
String sql = "SELECT * FROM account WHERE id IN (?, ?) FOR UPDATE";
var accounts = jdbcTemplate.query(sql, new Object[]{fromId, toId},
(ResultSet rs, int rowNum) -> new Account(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("balance")
)
);
Account from = accounts.stream().filter(a -> a.getId() == fromId).findFirst().orElseThrow();
Account to = accounts.stream().filter(a -> a.getId() == toId).findFirst().orElseThrow();
if (from.getBalance().compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient balance");
}
BigDecimal newFromBalance = from.getBalance().subtract(amount);
BigDecimal newToBalance = to.getBalance().add(amount);
jdbcTemplate.update("UPDATE account SET balance=? WHERE id=?", newFromBalance, fromId);
jdbcTemplate.update("UPDATE account SET balance=? WHERE id=?", newToBalance, toId);
System.out.println("Transferred " + amount + " from " + from.getName() + " to " + to.getName());
}
}
POJO
public class Account {
private long id;
private String name;
private BigDecimal balance;
public Account(long id, String name, BigDecimal balance) {
this.id = id;
this.name = name;
this.balance = balance;
}
// getters and setters
}
๐ 7. What Happens Internally
- When
SELECT ... FOR UPDATEruns, PostgreSQL locks the rows for both accounts. - Another concurrent transfer involving either account will wait until this transaction commits or rolls back.
- Because the method is annotated with
@Transactional, the entire operation (read โ business logic โ update) is atomic.
This prevents lost updates even under high concurrency.
๐งฑ 8. Using NOWAIT or SKIP LOCKED for Job Queues
Letโs say we have a queue table, and multiple workers are polling for unprocessed jobs.
SELECT * FROM job_queue WHERE status = 'PENDING'
FOR UPDATE SKIP LOCKED LIMIT 1;
This allows one worker to pick an unlocked job, while others skip over locked ones โ a perfect pattern for distributed workers.
๐งฐ 9. Handling Lock Timeouts
PostgreSQL allows setting lock timeouts at runtime:
SET lock_timeout = '5s';
In Spring:
jdbcTemplate.execute("SET LOCAL lock_timeout = '5s'");
If a lock cannot be acquired within the timeout, PostgreSQL throws an error โ you can catch this to retry or log it.
โก 10. Testing Concurrency Behavior
You can test concurrency by running two threads:
Runnable task1 = () -> accountService.transferMoney(1L, 2L, new BigDecimal("100"));
Runnable task2 = () -> accountService.transferMoney(1L, 3L, new BigDecimal("100"));
new Thread(task1).start();
new Thread(task2).start();
Only one transaction can lock the account 1 at a time โ the second will wait until the first completes.
๐งฉ 11. Advanced Pattern: Partial Locking with FOR UPDATE OF
If you join multiple tables and want to lock only one of them:
SELECT a.*, c.*
FROM account a
JOIN customer c ON a.customer_id = c.id
FOR UPDATE OF a;
This locks rows only in account, not customer.
โ 12. Key Takeaways
ConceptUseFOR UPDATELock rows for safe updates@TransactionalEnsure atomicity and rollback on errorJdbcTemplateGives fine-grained control over SQL and lockingSKIP LOCKEDIdeal for multi-threaded or distributed worker queuesNOWAITFail fast if lock canโt be acquired
Conclusion
SELECT โฆ FOR UPDATE is a powerful mechanism for pessimistic locking in PostgreSQL. When used with Spring Bootโs transaction management and JdbcTemplate, it ensures:
- Data consistency under concurrent transactions
- Deadlock-free, safe updates
- Predictable transactional behavior
Itโs especially valuable for financial, inventory, or real-time crew/aircraft assignment systems โ where correctness is far more important than raw throughput.
๋ฉํ๋ฐ์ดํฐ
- post_id
- 75e9d5a050ae
- slug
- optimistic-vs-pessimistic-locking-in-postgresql-using-for-update-effectively-with-spring-boot-75e9d5a050ae
- url
- https://medium.com/@renjithkn-67435/optimistic-vs-pessimistic-locking-in-postgresql-using-for-update-effectively-with-spring-boot-75e9d5a050ae
- canonical_url
- https://medium.com/@renjithkn-67435/optimistic-vs-pessimistic-locking-in-postgresql-using-for-update-effectively-with-spring-boot-75e9d5a050ae
- author_url
- https://medium.com/@renjithkn-67435
- status
- ok
- fetched_at
- 2026-07-30 07:15:55