This article explores how co-locating workflow metadata and application data within a single PostgreSQL database can simplify the implementation of durable workflows in distributed systems. By leveraging PostgreSQL's transactional guarantees, developers can achieve exactly-once execution semantics for workflow steps, eliminating the need for complex application-level idempotency logic and simplifying atomic updates across multiple systems through a transactional outbox pattern.
Read original on Hacker NewsThe article challenges the conventional wisdom of separating workflow state from application data, arguing that co-location in a single PostgreSQL database can be a "superpower" in distributed systems. This approach allows workflow metadata and application data to be updated within the same database transaction, thereby eliminating partial failures and simplifying the handling of complex edge cases, such as idempotency and atomicity.
A key challenge in distributed systems is ensuring idempotency when operations modify database state. Traditional durable workflows, which checkpoint after each step, can face issues if a workflow is interrupted after a step completes but before its checkpoint is recorded, leading to re-execution and potential duplicate side effects. The common solution involves application-level bookkeeping tables (e.g., `applied_payments`) to track executed operations.
Co-location Simplifies Idempotency
By co-locating workflow state and application data, a workflow engine can write the step checkpoint and perform the database update within the *same* PostgreSQL transaction. This guarantees exactly-once execution semantics: if the transaction commits, both the update and checkpoint are durably recorded; if it fails, the entire transaction rolls back, allowing safe re-execution without duplicates. This eliminates the need for application-level idempotency logic.
Another classic distributed systems problem is maintaining atomicity when updating records in a database and notifying other systems (e.g., updating an order and sending a warehouse notification). The standard solution is the transactional outbox pattern, where both the database record update and a message write to an 'outbox' table occur within a single transaction. A separate background process then polls the outbox table to deliver messages.
Simplifying the Transactional Outbox
The article proposes simplifying the transactional outbox by using PostgreSQL user-defined functions (UDFs) to enqueue a workflow directly within the same database transaction as the application update. This approach achieves the same atomicity guarantees as a traditional transactional outbox but reduces operational complexity by centralizing the workflow management within Postgres, rather than requiring separate polling infrastructure and reconciliation jobs.