DEV Community

Machine coding Master
Machine coding Master

Posted on

Stop Polling Your Outbox: Lightweight Event Streaming with Postgres LISTEN/NOTIFY and Java Virtual Threads

Stop Polling Your Outbox: Lightweight Event Streaming with Postgres LISTEN/NOTIFY and Java Virtual Threads

For years, we’ve tolerated the operational headache of spinning up heavy Kafka Connect or Debezium clusters just to sync our transactional outbox tables. But in 2026, with Java's virtual threads fully mature and mainstream, blocking a database connection to wait on events is no longer an architectural sin—it's a massive simplification.

Why Most Developers Get This Wrong

  • The Polling Tax: Constantly querying SELECT * FROM outbox WHERE status = 'PENDING' LIMIT 100 shreds your database indexes, bloats transaction logs, and spikes CPU for no reason.
  • Over-Engineering with CDC: Bootstrapping a complete Change Data Capture pipeline for a simple microservice boundary is operational overkill that introduces unnecessary network hops.
  • Thread Starvation Fears: Developers still avoid blocking JDBC drivers like PostgreSQL's notification listener because they mistakenly think it will choke their thread pools.

The Right Way

Leverage PostgreSQL's native LISTEN/NOTIFY system bound directly to a dedicated Java virtual thread that blocks cheaply and reacts instantly.

  • Virtual Thread Per Listener: Spawn an unpinned virtual thread using Thread.ofVirtual().start() to run a blocking getNotifications() loop.
  • Database Triggers: Use a lightweight Postgres trigger on your outbox table to automatically execute NOTIFY outbox_channel, payload on insert.
  • Zero-Overhead Parsing: Read the notification payload directly in Java, deserialize it, and dispatch it to your event broker instantly.

Show Me The Code

// Executed inside Thread.ofVirtual().start(...)
try (var conn = dataSource.getConnection()) {
    var pgConn = conn.unwrap(PGConnection.class);
    conn.createStatement().execute("LISTEN outbox_channel");
    while (!Thread.currentThread().isInterrupted()) {
        // Blocks cheaply on a virtual thread, yielding the carrier thread
        var notifications = pgConn.getNotifications(10000);
        if (notifications != null) {
            for (var notification : notifications) {
                eventPublisher.publish(notification.getParameter());
            }
        }
    }
} catch (SQLException e) { log.error("Listener failed", e); }
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Drop CDC Overhead: You don't need Debezium or Kafka Connect for simple transactional outbox patterns anymore.
  • Zero Polling Latency: Events are pushed immediately from Postgres to your Java application via TCP, cutting latency to sub-millisecond.
  • Infinite Scale on JVM: Because Virtual Threads are virtually free, you can run hundreds of dedicated listeners without exhausting the OS thread pool.

Want to go deeper? javalld.com — machine coding interview problems with working Java code and full execution traces.

Top comments (0)