Most engineers know Postgres handles ACID transactions. What fewer engineers know is how deep that capability goes. Isolation levels, advisory locks, LISTEN/NOTIFY, and serializable snapshot isolation combine into a coordination layer that can replace a surprising number of distributed systems patterns without adding a new service to maintain.
This is not an argument to use Postgres for everything. Kafka is excellent at event streaming. Redis is excellent at caching. But teams often reach for these tools to solve coordination problems that Postgres already handles, and the overhead of a new system is not always worth it.
The Problem With Distributed Coordination
Distributed systems fail in non-obvious ways. A service crashes after writing to a database but before sending a message to the queue. A read happens before a write is fully visible. Two services process the same work twice because neither saw the other's state change.
These are coordination failures. They happen because distributed systems have multiple independent sources of truth, and keeping those sources in sync is genuinely hard. The standard solution is to add more infrastructure: a message queue, a distributed lock service, a saga coordinator. Each addition increases operational complexity and failure surface area.
Postgres offers a different approach. Instead of spreading coordination across multiple systems, you can centralize it in the database you already operate.
Advisory Locks Are Underused
Postgres advisory locks let you acquire a lock on an arbitrary integer key without any table involvement. The lock exists only in Postgres's shared memory and survives transaction boundaries if you use pg_advisory_xact_lock() for transaction-scoped locks or pg_advisory_lock() for session-scoped locks.
A practical use case: preventing duplicate job processing. If you have a queue of work items and want to make sure two workers never process the same item simultaneously, you can attempt to acquire an advisory lock on the job ID before processing. If the lock succeeds, process the job. If it fails, skip to the next one.
SELECT pg_try_advisory_xact_lock(job_id);
-- Returns true if lock acquired, false if another transaction holds itNo Redis needed. No distributed lock service. The lock is enforced by Postgres, which is already your source of truth for the job state.
LISTEN and NOTIFY for Event-Driven Patterns
Every Postgres installation includes a pub/sub mechanism that most teams never use. LISTEN channel_name sets up a session to receive notifications. NOTIFY channel_name, message broadcasts to all sessions listening on that channel.
The message is ephemeral. LISTEN/NOTIFY is not a message queue. But it is enough for a surprising range of patterns:
- Invalidating a cache entry when the underlying data changes
- Triggering a background job when a new row is inserted
- Notifying connected web clients through a server-side event channel
For cache invalidation specifically, this is cleaner than most alternatives. When an order is updated, a database trigger or application code fires NOTIFY cache_invalidate, order_12345. A small background process listens on that channel and deletes the corresponding cache key. The invalidation happens within the same transaction that modified the data, so there is no window where the cache holds stale data.
Serialization and Conflict Detection
Serializable snapshot isolation (SSI) is Postgres's strongest isolation level. Unlike READ COMMITTED or REPEATABLE READ, SSI guarantees that the result of any concurrent transaction set is equivalent to some serial execution of those transactions. No dirty reads, no non-repeatable reads, no phantom reads. And crucially, no explicit locking required.
The performance cost of SSI is real. Workloads with high write contention can see significant slowdowns. But for workflows where correctness matters more than throughput, SSI lets you write code that is straightforward to reason about. Two concurrent transactions that modify overlapping rows will not silently produce wrong results. One will be retried or rejected with a serialization failure.
This matters for financial calculations, inventory management, booking systems. Any workflow where the cost of a race condition is high.
When to Reach for Postgres and When Not To
Postgres coordination primitives are not the right answer for everything. Do not use Postgres as a message queue for high-throughput event streaming workloads. LISTEN/NOTIFY drops messages if no one is listening. Advisory locks do not work across Postgres instances in a distributed setup.
For inter-service communication between independently deployed services, a message broker is still the right choice. But for coordination within a bounded context, where one service owns the data and related background tasks, Postgres can often replace the extra layer.
The practical advice: look at what coordination infrastructure you are running. If you have Redis just for distributed locks, check whether advisory locks in Postgres would work. If you have a message queue handling cache invalidation for a single-service domain, check whether LISTEN/NOTIFY would suffice. Each tool you remove reduces operational surface area and makes debugging simpler.
Postgres is not just a database. For many applications, it is also a perfectly capable coordination layer hiding in plain sight.

