> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0071-postgres-only-runtime-dependency.md).

# ADR-0071: PostgreSQL is the only required runtime dependency

## Status

**Accepted.** Reconstructed from the codebase on 2026-05-31; the decision is live in the source today.

## Context

A platform that coordinates work across replicas (don't double-send, don't double-consume), runs durable background queues, reacts to data changes, traverses a lineage graph, and serves catalog-wide search would conventionally reach for a stack of infrastructure: a message broker (Kafka, RabbitMQ), a coordinator for leader election (ZooKeeper, Consul, etcd), a cache or lock store (Redis), and a search engine (Elasticsearch). Each is one more component an operator must provision, secure, monitor, version, and back up — and one more way a deployment can fail.

ODD already requires PostgreSQL as its system of record. The open question the codebase answers is whether those other concerns justify their own infrastructure, or whether they can be met by features Postgres already has.

## Decision

**PostgreSQL is the only required runtime dependency.** Every capability that would otherwise pull in extra infrastructure is implemented on a Postgres feature:

* **Cross-replica leader election and mutual exclusion** → Postgres **advisory locks**. One `PostgreSQLLeaderElectionManager` issues the *blocking* `pg_advisory_lock(<id>)`; the replica that holds the lock is the singleton worker, and the others block. A disjoint per-subsystem id namespace (90 partition creation, 100 notifications WAL, 110 data-collaboration receive, 120 data-collaboration send) keeps the several single-leader workers from colliding. Scheduled jobs that need run-once-across-the-cluster semantics use **ShedLock with its JDBC lock-table provider** — again Postgres, not ZooKeeper or Redis.
* **Durable outbound queue** → a Postgres table, drained by the advisory-lock leader (ADR-0020).
* **Change-driven notifications** → the Postgres **write-ahead log**, consumed via logical replication by the advisory-lock leader (ADR-0043).
* **Time-series growth management** → native **range partitioning**, created ahead of need (ADR-0028).
* **Lineage graph traversal** → a single `WITH RECURSIVE` query over the ODDRN-keyed `lineage` table — no graph database (see ADR-0073).
* **Catalog full-text search** → Postgres **full-text search**: a `STORED` generated `tsvector` column matched with the `@@ to_tsquery(...)` operator, with the search session persisted as a Postgres row — no Elasticsearch or OpenSearch.

The one nuance: the **Redis** client *is* on the classpath, but only as one of three selectable session stores. The shipped default is in-memory, the durable option is a Postgres-backed session repository, and Redis is opt-in (`session.provider=REDIS`) — no default deployment provisions it. So Postgres is the only data store an operator is required to run.

## Consequences

* A minimal ODD deployment is the platform plus one PostgreSQL — the repository's `docker-compose` provisions exactly that. Fewer components to provision, secure, monitor, and back up, and a single backup/restore story covers the system of record, the queues, the notification log, and the search index together.
* The trade-offs are the ones each "Postgres-as-X" choice carries. Single-leader workers do not scale horizontally — adding replicas adds standbys, not parallel throughput (ADR-0020, ADR-0043). Search is bounded by Postgres FTS semantics rather than a dedicated engine's relevance tuning. Graph traversal depth is bounded by what one recursive CTE will return in a round-trip. These are accepted in exchange for the single-dependency posture.
* **PostgreSQL becomes the single point of failure and the shared resource under contention.** Leader election, queue draining, WAL consumption, partition DDL, search, and every catalog read and write all land on the same database. That concentration is deliberate, but it means Postgres sizing, availability, and monitoring carry the whole platform.
* The choice is reversible per-concern but load-bearing in aggregate. A contributor adding a feature that "needs a broker" or "needs Elasticsearch" is working against the platform's defining posture and should first ask whether a Postgres feature serves.

## Evidence

* `gradle/libs.versions.toml` + `odd-platform-api/build.gradle` — the dependency set carries the Postgres stack (`r2dbc-postgresql`, jOOQ, Flyway, the JDBC `psql-driver`, ShedLock with its `shedlock-provider-jdbc-template`) and **no** client for Kafka, RabbitMQ/AMQP, ZooKeeper/Curator, Consul, etcd, Hazelcast, NATS, Pulsar, ActiveMQ, or Elasticsearch/OpenSearch/Solr. The only other store client is Redis (`spring-data-redis`, `spring-session-data-redis`), used solely for the optional session provider below.
* `odd-platform-api/.../config/SessionConfiguration.java:29-65` + `application.yml:28-30` — three session providers (`INTERNAL_POSTGRESQL` → a Postgres `JooqSessionRepository`; `IN_MEMORY`; `REDIS`); the shipped default is `provider: IN_MEMORY`, so Redis is opt-in and never required.
* `odd-platform-api/.../leaderelection/PostgreSQLLeaderElectionManagerImpl.java:22-23` — `SELECT pg_advisory_lock(<id>)` (the blocking form) is the cross-replica mutual-exclusion primitive; no external coordinator.
* `application.yml:177,198,201,202` — the disjoint advisory-lock id namespace (100 notifications WAL, 90 partition, 110 data-collaboration receive, 120 data-collaboration send).
* `odd-platform-api/.../repository/util/JooqFTSHelper.java:103` + `db/migration/V0_0_1__init.sql:196-199` — catalog search runs on Postgres FTS: a generated `search_vector tsvector` column matched with `? @@ to_tsquery(?)`. `.../service/search/SearchServiceImpl.java:75-81` — a search creates a `search_facets` session row and reads facets back by its id.
* `odd-platform-api/.../repository/reactive/ReactiveLineageRepositoryImpl.java:126-128` — the lineage graph is walked by `DSL.withRecursive(...)` in Postgres, with no graph database.
* `odd-platform-api/.../partition/service/PartitionServiceImpl.java:61` — growth is managed with native `CREATE TABLE ... PARTITION OF` (ADR-0028).
* `tests/docker/docker-compose.yaml:2-27` — the repository's compose provisions only `database` (PostgreSQL) alongside the platform (and a test-only enricher); no broker, cache, or coordinator service.

## See also

* [ADR-0073 — ODDRN is the universal identity for every entity](/developer-guides/architecture-decision-log/adr-0073-oddrn-universal-identity.md) — the lineage graph this posture keeps in Postgres is walked by a recursive CTE over ODDRN pairs.
* [ADR-0020 — Outbound Slack delivery is decoupled via a Postgres queue](/developer-guides/architecture-decision-log/adr-0020-decoupled-outbound-slack-delivery.md) — the durable queue, on Postgres.
* [ADR-0043 — The notification WAL consumer is a leader-elected singleton](/developer-guides/architecture-decision-log/adr-0043-notification-wal-single-leader.md) — notifications on the Postgres WAL plus an advisory lock.
* [ADR-0028 — High-volume tables are range-partitioned ahead of need](/developer-guides/architecture-decision-log/adr-0028-range-partition-lifecycle.md) — growth managed with native partitioning.
* [ADR-0044 — Postgres replication artefacts are lazy-created, never dropped](/developer-guides/architecture-decision-log/adr-0044-postgres-artefact-lazy-create-no-drop.md) — the replication slot and publication the WAL consumer relies on.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0071-postgres-only-runtime-dependency.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
