What Postgres gives you for free — the interview version
Every engine-level feature your projects lean on, named and claimed.
Know your own system
Yesterday's engine theory, converted into your talking points. Walk through what you use and what it cost you to not build:
| You use | Postgres machinery | What you didn't build |
|---|---|---|
| conditional updates (CAS) | row locks + predicate re-eval (EvalPlanQual) | distributed lock service |
| SKIP LOCKED claims | row-lock skipping in the planner | leader election / job scheduler |
| unique constraints | B-tree unique indexes + error codes | application-level dedup coordinators |
pg_advisory_lock at boot | lock manager keyed by bigint | migration orchestration service |
| transactions spanning business+outbox | WAL + 2-phase within the commit | dual-write reconciliation |
| recursive CTE | iterative execution engine | graph database |
| JSONB policies | TOASTed binary JSON + operators | config service |
| pgvector + HNSW | ANN graph index | vector database |
| sequences | MVCC-safe counters | ticket server |
| PITR (traverse) | WAL archiving | event sourcing backbone |
The pitch line: "My architecture is 'lean on the database until it measurably hurts.' Every layer people bolt on — locks, queues, schedulers, dedup, vector search — Postgres already provides in transactional form. The discipline is knowing which one becomes the bottleneck first: connections (pool), write throughput (partitions), or query shape (indexes) — and I can rank them for my workloads."
The costs you accepted knowingly (say these too — nothing is free):
- Single-writer bottleneck: all writes serialize through one primary's WAL — the ceiling for both projects. The remedy ladder: batching, partitioning (doesn't remove the WAL), then replication/sharding (moves the ceiling).
- VACUUM dependence: your write-heavy tables (submissions, outbox, notification_delivery with attempts churn) produce dead tuples; autovacuum tuning is the first operational knob under write growth. The notification table's UPDATE-heavy claim pattern (
attempts+1, next_attempt_atper drain) is exactly the update-churn that makes autovacuum visible — worth monitoringn_dead_tupthere. - Connection process model: each connection = a backend process (~10 MB); 3 services × pools must stay modest — that's why pool sizes of 10–25, not fashion.
- Operational surface: backups/replication/monitoring are yours — traverse answered with S3 WAL archive + Aiven; zoth's gap is known (Day 33/34).
One more claimable detail — the FOR UPDATE SKIP LOCKED engine view: the lock manager keeps a wait queue per row; SKIP LOCKED opts out of queueing (returns "not eligible") instead of blocking — that's why the drain scales linearly with workers: no lock convoy, just disjoint set operations.
The zoth-vs-traverse engine pressure comparison
- Zoth: tiny data, human-paced writes, transactional correctness first — engine headroom is enormous; the audit chain (append-only + read-last) is the one pattern that stresses its single-writer serialization if scaled (Day 50's fix).
- Traverse: 100+ users writing submissions + workers updating derived state + notification churn — the realistic first engine pressure is autovacuum/connections at maybe 10–100× current load. The remedy ladder is already scoped (partitions by month, pool caps, read replicas for stats).
Bulletproof your resume
Say these answers aloud before moving on
These are the follow-ups an interviewer will actually ask about your projects.
Which Postgres feature would you say you exploited most?
Conditional updates as compare-and-swap. Every state machine transition (JIT lifecycle), every idempotent claim (set-once topics, ring closure), and every race resolution rides on the engine re-evaluating the WHERE clause against the current row version after lock acquisition. It replaced a lock service, a leader elector, and a dedup coordinator in one idiom — and it's testable with plain SQL concurrency tests.
Where does Postgres become your bottleneck, concretely?
The single primary's WAL is the global serialization point — fine at my write rates (events per minute), with a clear ladder: batch writes (broadcast fanout already batches), partition hot tables for retention and pruning, replicas for read load, and only then shard. The nearer-term engine issues are autovacuum on my update-churn tables (notification_delivery attempts) and connection count as replicas multiply — both monitoring knobs, both already on my list.
Why did you skip Redis for zoth entirely?
Because every job Redis would do was already done transactionally: caching would serve ~1 rps reads (nothing hot), a cache adds an invalidation problem I don't have, and queueing is RabbitMQ's job in that stack. The honest framework: add a component when it removes a measured bottleneck. Traverse needed Redis because its job pipeline and cache serve a real latency contract; zoth's constraints were correctness and cost — both better served by one Postgres.