This capstone integrates the entire course into one deliverable: design and operate a production-grade PostgreSQL cluster that is highly available, secure, performant, observable, and recoverable. You will bring together schema and index design, partitioning, transactions and MVCC-aware maintenance, replication and automatic failover, connection pooling, backups with point-in-time recovery, security with roles and RLS, and deployment — the full operational picture of running PostgreSQL well.
The scenario is the production database tier for a multi-tenant SaaS application: it must stay available through node failures, isolate tenants' data, sustain high read/write concurrency, recover from mistakes, and be observable enough to operate confidently. The point is integration — making replication, pooling, backups, security, and maintenance work together as one coherent, resilient system rather than as isolated features.
Learning Objectives
- Integrate the whole course: schema/indexing, partitioning, HA, pooling, backups, security, and observability.
- Deliver high availability with streaming replication and automatic failover that prevents split-brain.
- Enforce security: least-privilege roles, multi-tenant Row-Level Security, and encrypted, verified connections.
- Protect data with point-in-time recovery (base backups + WAL archiving), proven by a tested restore.
- Scale connections with a pooler and route reads to replicas and writes to the primary.
- Operate with observability and healthy maintenance: monitoring, autovacuum tuning, and partition lifecycle.
Technical Requirements
- A primary plus at least two replicas with automatic failover (Patroni + a distributed consensus store, or a Kubernetes operator).
- PgBouncer (transaction pooling) in front, with reads routed to replicas and writes to the primary.
- Continuous physical backups with WAL archiving for PITR (pgBackRest/Barman/WAL-G), stored off-host.
- Security: non-superuser app roles, separate DDL/DML roles, FORCE Row-Level Security for tenant isolation, and TLS (verify-full).
- A well-indexed, partitioned schema for the largest tables, with autovacuum tuned per hot table.
- Monitoring (a Prometheus exporter or equivalent) with alerts on replication lag, bloat, disk, and backup success.
Architecture & Design
The cluster is a primary with two streaming replicas, orchestrated for automatic failover by Patroni backed by an odd-sized etcd quorum (or an equivalent Kubernetes operator). PgBouncer fronts the cluster in transaction mode, sending writes to the current primary and reads to replicas via the routing the orchestrator maintains. Continuous WAL archiving plus periodic base backups to off-host object storage provide point-in-time recovery independent of the replicas.
Security is layered throughout: applications connect over TLS as least-privilege, non-owner roles; a separate role runs migrations; and FORCE Row-Level Security enforces tenant isolation in the database itself. The schema partitions the largest tables by time, indexes match the hot queries, autovacuum is tuned per busy table, and a metrics exporter feeds dashboards and alerts. Every module's concern has a defined place in this one design.
-- Topology (conceptual): one writable primary, two read replicas, automatic failover
--
-- app --> PgBouncer(tx pooling) --> [ Patroni-managed cluster ]
-- primary (writes) <-- etcd quorum elects leader
-- replica1 (reads)
-- replica2 (reads)
-- --> WAL archive + base backups (off-host, PITR)
--
-- Security & isolation applied across it:
ALTER TABLE tenant_data ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_data FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_iso ON tenant_data
USING (tenant_id = current_setting('app.tenant')::bigint)
WITH CHECK (tenant_id = current_setting('app.tenant')::bigint);Phase 1 — HA Cluster, Pooling, and Routing
Build the availability foundation first: a primary and two streaming replicas with replication slots, orchestrated by Patroni and an odd-sized etcd quorum (or a Kubernetes operator) so a primary failure triggers automatic, split-brain-safe promotion. Put PgBouncer in front in transaction mode, routing writes to the leader and reads to replicas.
Verify the core HA loop by drilling failover: kill the primary, confirm a replica is promoted within your RTO, and confirm clients — via the pooler and the orchestrator's leader routing — follow to the new primary automatically. This phase delivers a cluster that survives node loss and absorbs connection load before any data or security work is layered on.
# Patroni cluster (3 nodes) + etcd quorum; PgBouncer routes by role.
# Drill the failover loop and confirm clients follow:
patronictl -c /etc/patroni.yml list # confirm 1 leader, 2 replicas
docker stop pg-primary # or: patronictl switchover (planned)
patronictl -c /etc/patroni.yml list # a replica is now Leader
# PgBouncer 'app_rw' points at the leader, 'app_ro' at replicas:
# [databases]
# app_rw = host=patroni-leader port=5432 dbname=app
# app_ro = host=patroni-replicas port=5432 dbname=app pool_mode=transactionPhase 2 — Schema, Security, and Performance
On the HA base, build the multi-tenant schema: partition the largest tables by time, design indexes (composite, partial, covering) to match the application's hot queries validated with EXPLAIN ANALYZE, and tune autovacuum per busy table so MVCC bloat is controlled. This is where the indexing, partitioning, and maintenance modules become concrete in one schema.
Apply security in depth: applications connect over TLS (verify-full) as least-privilege, non-owner roles, with a separate role for migrations; revoke broad PUBLIC defaults; and enforce tenant isolation with FORCE Row-Level Security so no application bug can leak across tenants. Set the tenant per request via a session setting that the RLS policy reads.
-- Partitioned, indexed, isolated tenant table; autovacuum tuned for churn
CREATE TABLE tenant_data (
id bigint GENERATED ALWAYS AS IDENTITY, tenant_id bigint NOT NULL,
created_at timestamptz NOT NULL, payload jsonb,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE INDEX ON tenant_data (tenant_id, created_at DESC); -- hot-query index
ALTER TABLE tenant_data SET (autovacuum_vacuum_scale_factor = 0.02); -- control bloat
-- Least privilege + isolation
CREATE ROLE app_runtime LOGIN PASSWORD '...'; -- DML only, not owner, not superuser
REVOKE ALL ON SCHEMA public FROM PUBLIC;
-- (RLS enabled + FORCED with the tenant policy from Architecture above)Phase 3 — Backups, Observability, and Recovery Drills
Protect the data independently of the replicas: configure continuous WAL archiving and periodic base backups to off-host object storage with a tool like pgBackRest, enabling point-in-time recovery. Then prove it — perform a PITR restore to a scratch environment targeting a moment before a simulated bad change, and confirm the data comes back correctly.
Add observability: a Prometheus exporter feeding dashboards and alerts on replication lag, dead-tuple bloat, disk usage, backup success, and wraparound age, so problems are caught early. Document the runbook — failover, restore, and routine maintenance (partition lifecycle via pg_cron, autovacuum monitoring) — so the cluster is genuinely operable, not just configured.
# Continuous backup + PITR with pgBackRest, then PROVE recovery
pgbackrest --stanza=main backup --type=full # periodic base backup
# (WAL archived continuously via archive_command)
pgbackrest --stanza=main --type=time \
--target='2026-06-16 14:32:00' --delta restore # PITR to a scratch host
-- Observability: alert-worthy signals to export and watch
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag FROM pg_stat_replication;
SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;
SELECT last_archived_time, failed_count FROM pg_stat_archiver; -- backups succeeding?Evaluation Rubric
- High availability (25%): automatic, split-brain-safe failover; clients follow the new primary; failover drilled successfully.
- Security (20%): least-privilege non-owner roles, separate DDL/DML, FORCE RLS tenant isolation, and TLS verify-full.
- Backup & recovery (20%): continuous PITR configured off-host and proven by a successful tested restore.
- Performance & maintenance (15%): partitioned, well-indexed schema validated by EXPLAIN; autovacuum tuned per hot table.
- Connection management (10%): pooler in transaction mode with correct read/write routing.
- Observability & operability (10%): monitoring and alerts on lag, bloat, disk, and backups, plus a documented runbook.
Extension Challenges: Add synchronous replication for a zero-RPO subset of critical transactions and measure the latency cost; introduce logical replication to feed a separate reporting database without loading the primary; deploy the whole cluster via a Kubernetes operator (CloudNativePG) and compare the operational effort to the hand-built Patroni setup; and run a full game-day exercise — simultaneously simulating a node failure and a PITR restore — to validate that your runbook, monitoring, and team response actually work under pressure.