Distributed Locking With PostgreSQL and Spring Boot
Locks, PostgreSQL, Code , Examples and more.
Introduction
In modern microservices and high-traffic applications, multiple instances of your service often run concurrently. They may try to access the same resource—like refreshing a cache, updating a shared counter, or processing the same job—at the same time. Without coordination, this can lead to race conditions, redundant work, or even database overload.
This is where distributed locking comes in. A distributed lock ensures that only one service instance at a time can perform a critical operation, even if multiple servers are running in parallel.
We may think that we need a fancy distributed system like Redis, ZooKeeper, DynamoDB, or etcd to achieve this. Surprisingly, we can implement effective distributed locks using a single PostgreSQL instance that all our services share. Combined with Spring Boot, this provides a simple, robust solution without extra infrastructure.
Why Use Distributed Locks?
Consider a scenario: Spring Boot app caches user profiles from the database. When the cache entry expires, multiple requests hit at the same time. Without a lock:
All app instances may simultaneously fetch the same data from the DB
Database load spikes unexpectedly
Cache stampedes happen, slowing down all users
With a distributed lock:
Only one instance refreshes the cache
Others either wait or continue serving stale data
Database load is controlled, and performance stays consistent
PostgreSQL Advisory Locks
PostgreSQL provides advisory locks, which are lightweight application-level locks. Key properties:
Session-level: held until connection closes, can be blocking (
pg_advisory_lock) or non-blocking (pg_try_advisory_lock)Transaction-level: released at the end of the transaction, can be blocking (pg_advisory_xact_lock), can be non-blocking (pg_try_advisory_xact_lock)
These locks are global to all clients connecting to the same database, so multiple Spring Boot instances can coordinate using them.
Example SQL:
Blocking:
Non-blocking:
📢 📢 📢 Get actionable Java insights every week — from practical code tips to expert interview questions you can use today.
Join 3400+ subscribers and level up your Spring & backend skills with hand-crafted content — no fluff.
I want to offer an additional 40% discount ($ 18 per year) until January 31st.
Not convinced? Check out the details of the past work
Spring Boot Implementation
With Spring Boot, we can create a service class that provides lock and unlock method wrappers on top of SQL functions we saw earlier.
If we use a native query:
Using @Transactional ensures same connection, which is crucial for advisory locks.
We can also use Repository, with a native query:
About lockId:
PostgreSQL advisory locks don’t know anything about your application.
They just lock a 64-bit integer (BIGINT) or two 32-bit integers.
You can choose any value, as long as all sessions that want the same lock use the same value.
PostgreSQL also allows:
SELECT pg_try_advisory_lock(key1, key2);
Useful if you want to combine two values (e.g., app ID + user ID) without generating a big number manually.
Example:
int appId = 123;
int userId = 456;
em.createNativeQuery("SELECT pg_try_advisory_lock(:key1, :key2)")
.setParameter("key1", appId)
.setParameter("key2", userId)
.getSingleResult();Service Layer
Benefits
No extra infrastructure needed: uses your existing PostgreSQL database
Safe across multiple Spring Boot instances
Prevents cache stampedes or duplicate work
Simple to implement and monitor
Caveats
The lock is tied to a single DB instance; if the database goes down, locking fails
Locks are connection-dependent: if the connection drops unexpectedly, the lock is released
Not suitable for multi-database or geo-distributed setups without extra coordination.
Conclusion
Distributed locks are essential for preventing race conditions in high-concurrency environments. Using PostgreSQL advisory locks with Spring Boot is a simple, effective, and production-ready approach. It’s especially handy for cache refreshes, job processing, or any shared resource where only one instance should act at a time.
With this pattern, Spring Boot apps stay fast, safe, and predictable, without introducing extra distributed systems into your stack.
📢 📢 📢 Get actionable Java insights every week — from practical code tips to expert interview questions you can use today.
Join 3400+ subscribers and level up your Spring & backend skills with hand-crafted content — no fluff.
I want to offer an additional 40% discount ($ 18 per year) until January 31st.
Not convinced? Check out the details of the past work
Helpful Resources (7)









The elegance here is in leveraging pg_advisory_lock as a coordination primitive without the operational overhead of consensus protocols. One edge case to watch: if you're using connection pooling (like PgBouncer in transaction mode), session-level locks won't persist across pool reuse cycles, which can lead to subtle race conditions. Worth documenting whether your implementation assumes sticky connections or if you're explicitly managing lock lifetimes within bounded transactions. Also curious how this performs under high contention - have you benchmarked lock acquisition times with hundreds of concurrent requests?
This approach to using PostgreSQL advisory locks instead of spinning up Redis or ZooKeeper is genius - we implemented somthing similiar last quarter and cut our infrastructure costs noticeably. The explanation of session-level vs transaction-level locks is really clear, and the cache stampede use case hits home since we were dealing with exactly that problem. One thing worth mentionning is how well this scales when you already have a robust PostgreSQL setup.