PostgreSQL is a general-purpose relational database for applications that need transactions, strong constraints, expressive SQL, and an extensible type and index system. It is a good default for many transactional systems, but it is not automatically the right answer for every workload. The useful question is whether its consistency model, operating model, query capabilities, and extension ecosystem match the application you need to run.
Start with workload fit
PostgreSQL fits especially well when related data must remain consistent across several tables, when queries evolve beyond key lookups, or when the database should enforce business invariants. Foreign keys, unique constraints, exclusion constraints, check constraints, transactions, window functions, recursive queries, and rich data types let a team keep important rules close to the data. A mature SQL interface also makes it possible to serve application traffic, operational reporting, and carefully bounded analytical queries from the same system.
That flexibility does not make PostgreSQL a universal storage layer. A high-volume append-only analytics platform may be better served by a columnar engine. A globally disconnected multi-writer design needs an explicit conflict model that ordinary PostgreSQL replication does not provide. Very large binary objects may belong in object storage with database metadata and integrity checks. A cache, search engine, queue, or vector service can still be the right companion when its specialized behavior is required. Choose from access patterns and failure requirements, not from a feature checklist.
| Requirement | PostgreSQL starting point | Design question |
|---|---|---|
| Transactional application data | Normalized tables, constraints, and short transactions | Which invariants must the database enforce? |
| Semi-structured attributes | jsonb beside typed relational columns | Which paths are queried, sorted, joined, or constrained often enough to become columns? |
| Read scale and recovery | Physical streaming replicas | What lag and failover behavior can each read tolerate? |
| Selective data movement | Logical publications and subscriptions | Who owns schema changes, sequences, and conflict prevention? |
| Specialized capabilities | Reviewed extensions | Are packages, upgrades, backups, and failover tested for every extension? |
Understand the process and transaction model
A PostgreSQL server uses a process-based architecture. A client connection is normally served by a backend process, while background processes handle WAL, checkpoints, vacuuming, statistics, and other work. This is operationally important: opening thousands of idle application sessions is not free. Set a connection budget from memory and concurrency measurements, keep transactions short, and use a compatible pooler when application fan-out exceeds the number of useful database backends. Pooling is an external deployment choice, not a postgresql.conf parameter.
Multiversion concurrency control, or MVCC, lets statements see snapshots while concurrent transactions create new row versions. It does not mean reads and writes are universally lock-free. Data-changing statements acquire row and table locks; explicit locking reads, DDL, unique checks, and foreign-key checks can wait or deadlock. Under the default Read Committed isolation level, two statements in one transaction can see different committed snapshots. Repeatable Read and Serializable provide stronger behavior, with retryable serialization failures possible. Pick the isolation level from the invariant, then make retries idempotent.
Updates and deletes leave obsolete row versions until vacuum can reclaim their space. Autovacuum also refreshes planner statistics and prevents transaction ID wraparound. Disabling it to avoid I/O simply transfers risk to table growth, poor estimates, and emergency maintenance. Monitor per-table change rate, dead-row estimates, vacuum progress, oldest transaction age, and sessions holding old snapshots. Tune hot tables from observed behavior; a single global scale factor or vacuum schedule rarely matches every table.
Model relational data first and add JSONB deliberately
Keep identifiers, ownership, lifecycle state, money, timestamps, and common join or filter keys in typed columns. Use jsonb for attributes whose shape varies but still benefits from transactional storage and PostgreSQL operators. This gives the application flexibility without hiding every query behind document extraction.
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id bigint NOT NULL,
occurred_at timestamptz NOT NULL DEFAULT now(),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object')
);
CREATE INDEX events_tenant_time_idx
ON events (tenant_id, occurred_at DESC);
CREATE INDEX events_payload_containment_idx
ON events USING gin (payload jsonb_path_ops);
SELECT id, occurred_at
FROM events
WHERE tenant_id = 42
AND payload @> '{"type":"payment"}'::jsonb
ORDER BY occurred_at DESC
LIMIT 50;The B-tree supports the tenant and time access path. The GIN index with jsonb_path_ops is a focused choice for containment and JSON-path operators; the default jsonb_ops supports additional key-existence operators. An expression index can be smaller and more targeted when one path dominates, but the query expression must match it. Validate every index with representative data and EXPLAIN (ANALYZE, BUFFERS); an index adds write, vacuum, storage, and recovery work even when a sample query becomes faster.
Promote a JSON path to a typed column when it needs a foreign key, stable type, frequent ordering, uniqueness, or routine joins. Add validation before migration, backfill in bounded batches, dual-read during verification, and only then make the new column authoritative. JSONB is a useful boundary for variable attributes, not permission to abandon data modeling.
Use extensions as dependencies, not decorations
PostgreSQL can add types, operators, functions, index methods, and procedural languages through extensions. Core and contributed modules such as pg_stat_statements follow PostgreSQL packaging, while projects including PostGIS, pgvector, TimescaleDB, and pg_partman have their own maintainers, packages, version matrices, and upgrade procedures. Availability on one laptop does not prove availability in a managed service or recovery region.
For each extension, record the owner, purpose, package source, approved versions, required preload settings, privileges, backup behavior, logical-replication behavior, and removal plan. Install the same version in staging and every failover target. Run extension update scripts during tested maintenance, then verify functions, indexes, background workers, and restored backups. pg_stat_statements, for example, normally needs to be loaded through shared_preload_libraries before CREATE EXTENSION exposes its view in a database.
PostGIS is appropriate for geospatial types and operators. pgvector adds vector types and approximate or exact nearest-neighbor access paths. pg_partman can help manage time- or serial-based partitions. TimescaleDB adds its own time-series abstractions. These capabilities solve different problems; enabling all of them increases the platform's compatibility surface. For vector-specific trade-offs, see the PostgreSQL vector database guide.
Separate availability, read scaling, and data distribution
Physical streaming replication sends WAL to a byte-compatible standby and is the usual foundation for read replicas and failover. Synchronous replication can require selected standbys to acknowledge commits, trading failure exposure for commit latency and availability behavior. Asynchronous replication avoids that acknowledgement dependency but can lose transactions that were not replayed before an unplanned promotion. Neither mode automatically redirects clients, fences the old primary, or proves the application recovered.
Logical replication sends selected table changes through publications and subscriptions. It can move subsets of data and can bridge supported major-version combinations, but schema DDL, sequence state, and large objects require separate handling. It is useful for migrations and distribution workflows when those boundaries are owned explicitly. The PostgreSQL replication guide compares physical and logical designs, while the dedicated logical-replication runbook covers setup and cutover.
Replication is not a backup. A replica can faithfully reproduce an accidental delete or corrupt application change. Maintain independent backups and WAL archives where point-in-time recovery is required, restore them into an isolated environment, and measure recovery from the application perspective. A failover test is incomplete until writes reach exactly one primary, connection pools reconnect, scheduled jobs are fenced, critical data is validated, and a new resilient topology is established.
Tune from evidence, not copied constants
Start with defaults, a workload model, and observability. Size max_connections from useful concurrency and per-backend cost; more sessions can increase queueing and memory pressure. Remember that work_mem may be used by multiple sort or hash nodes in each operation and by many concurrent sessions, so multiplying it by the connection limit still understates some peaks. Treat shared_buffers, WAL and checkpoint settings, autovacuum capacity, and planner cost settings as hypotheses to test.
- Define service objectives. Record latency, throughput, availability, durability, and freshness requirements for each workload class.
- Capture database evidence. Use cumulative statistics, wait events, lock views, WAL and checkpoint metrics, autovacuum logs, and normalized statement statistics. Preserve reset times and distinguish counters from gauges.
- Fix query and schema causes first. Correct missing constraints, unstable query shapes, poor estimates, and access paths before hiding them with global parameter changes.
- Change one bounded variable. Canary the change, compare a representative peak, and keep a rollback value. Account for restart versus reload requirements.
- Retest failures. A setting that wins a benchmark can make recovery, replica lag, checkpoints, or memory exhaustion worse.
Use EXPLAIN ANALYZE carefully because it executes the statement. For writes, wrap a safe test in a transaction and roll it back only when all effects are transactional. Keep query text and parameters out of broadly accessible logs. The indexed PostgreSQL architecture deep dive explains the backend, memory, heap, WAL, and execution path in more detail.
Production readiness checklist
- Use supported PostgreSQL and extension releases, with rehearsed minor and major upgrades.
- Require TLS identity verification and least-privilege roles; keep application, migration, monitoring, replication, and break-glass identities separate.
- Bound connections and statement, lock, and idle-in-transaction timeouts according to the workload.
- Monitor storage growth, WAL retention, replication state, backup age, restore results, transaction age, vacuum, locks, and user-visible errors.
- Make schema changes reversible where practical and test lock acquisition and rollback on production-like data.
- Exercise restore and failover with the application, dependencies, routing, and write fencing included.
PostgreSQL's strength is the combination of relational integrity, advanced SQL, extensibility, and transparent operational primitives. The safe way to use that strength is to keep the core model simple, add specialized features only for measured needs, and treat every extension, replica, setting, and migration as an owned production dependency.