Prompt
How do I keep my app search index in sync with Postgres changes?
Latest observation
A solid way to keep an app search index in sync with Postgres is to treat Postgres as the source of truth and stream changes out to your search system asynchronously.
Recommended pattern: transactional outbox + async indexer
1. Write to Postgres first
When your app creates/updates/deletes data, do it in the same Postgres transaction as a small “outbox” record.
Example:
productstable updatedoutbox_eventstable gets an event likeproduct.updated
This avoids the classic problem where the DB write succeeds but the search update fails.
2. Have a worker consume outbox events
A background process reads the outbox table and applies the change to your search index:
- insert/update document
- delete document
- reindex if needed
After success, mark the event processed.
3. Make indexing idempotent
Search updates should be safe to retry:
- use stable document IDs
- apply “upsert” semantics
- ignore duplicate events if possible
4. Handle deletions explicitly
Don’t rely on inferred deletes. Emit delete events or soft-delete flags, then remove from the index.
5. Rebuild periodically
Even with sync logic, run periodic reconciliation:
- compare counts / checksums
- reindex changed rows
- full rebuild if drift is detected
Alternatives
CDC / logical replication
If you want near-real-time sync with less app code:
- use Postgres logical decoding / CDC
- stream changes into Kafka, Debezium, or a custom consumer
- index from the change stream
Good for larger systems, but more operational complexity.
Trigger-based sync
You can use Postgres triggers to write to an outbox table automatically.
Good if you want every write captured at the DB layer.
Dual write directly from app
App writes to Postgres and search index in the request path.
Usually not recommended because failures create inconsistency unless you add retries, compensation, and careful ordering.
Practical advice
- Never use the search index as source of truth
- Expect eventual consistency
- Use retries and dead-letter handling
- Track event versions / updated_at timestamps to avoid stale overwrites
- Backfill with a full reindex job for initial load and repairs
Simple architecture
- API updates Postgres
- Same transaction inserts outbox event
- Worker polls outbox or consumes CDC
- Worker updates search index
- Worker marks event processed
- Nightly reconciliation job checks for drift
If you want, I can sketch:
- a PostgreSQL schema for the outbox pattern,
- sample code in your stack,
- or a CDC-based design using Debezium/Logical Replication.