Institutional · Programme work since 2023

DynaStore: canonical data and recoverable projections

Architecture and technical leadership at FAO, delivered with colleagues and collaborators

DynaStore separates API adapters, domain modules, storage capabilities and background tasks. My work connects those boundaries so that geospatial data can be exposed through interoperable APIs while search and slower work remain recoverable projections.

Problem: one geospatial domain, several storage capabilities

A catalog may need exact geometry, spatial joins, discovery search and access to existing files. Those operations do not have identical storage requirements. A platform that hides the differences behind a universal backend risks making unreliable promises about behavior and fallback.

The programme began in 2023. This case describes the inspected public repository snapshot and its documented contracts, not a claim that every branch component is deployed or that repository tests establish production capacity.

Architecture: extensions, modules and tasks

HTTP extensions translate API requests; modules own domain logic and data; tasks handle asynchronous work. Canonical PostgreSQL/PostGIS storage is distinct from secondary search indexes and drivers with other capabilities, including DuckDB read-in-place for supported files.

Routing separates WRITE, READ and INDEX operations. SEARCH is derived from INDEX, with READ as a configuration fallback. Query hints can favor a capable read driver, but do not redirect the primary write lane. An empty Elasticsearch result is not a universal trigger to retry a query in PostgreSQL.

Logical feature identity is separate from catalog names and physical backend identifiers. That separation helps route the same domain object across storage and index representations.

Domain ownership and operation lanes

API extensions enter a module boundary, which resolves domain identity and storage capabilities. The canonical write/read lane reaches PostgreSQL/PostGIS; asynchronous index work reaches a derived search store. File access remains a capability-specific lane. This is a component map, not a claim about production replicas.

Open full-size SVG
DynaStore · domain ownership and storage lanesAPI extensions call domain modules. Storage routing distinguishes write, read and index operations, with PostgreSQL canonical data, derived search and capability-specific file access. DynaStore · domain ownership and storage lanes Repository architecture · configuration-dependent capabilities API ADAPTERS Translate HTTP; domain rules live in modules Features / STAC / Tiles Protocol-specific extensions Catalog / item modules Domain identity and ownership Task dispatch Slow / deferred work domain calls enqueue CAPABILITY AND OPERATION BOUNDARY Storage registry + configured driver routing WRITE lane Primary persistence READ lane Query hints + capabilities INDEX lane Async projection write intent read intent task routing DATA OWNERSHIP Canonical state is distinct from projection state PostgreSQL / PostGIS Files / DuckDB access Search index canonical write exact / spatial read supported file query materialize SEARCH derives from INDEX, then READ configuration. Empty search results do not imply universal runtime failover. Logical feature IDs are independent of backend names. Driver behavior depends on the selected operation and path. API ADAPTERS Translate HTTP; domain rules live in modules CAPABILITY AND OPERATION BOUNDARY Storage registry + configured driver routing DATA OWNERSHIP Canonical state is distinct from projection state Request / integration Message / asynchronous work Persistence / data access

Decisions: bind the guarantee to the exact write path

In the PostgreSQL-primary bulk-upsert path, canonical rows, lightweight indexing obligations and the storage-drain task obligation use one transaction. The outbox stores IDs or write IDs; draining rereads canonical state before materialization rather than treating copied payloads as authoritative.

Ordinary item-upsert paths dispatch indexing after the primary write through a separate transaction. They do not inherit the bulk path’s atomicity. Cross-call outbox duplicates are also possible; retry-oriented materialization is not exactly-once delivery.

Tasks use capability-aware dispatch and database claiming, with heartbeat/visibility recovery and reaping of stale work. Collection lifecycle states constrain access during provisioning and deletion. External cleanup hooks can still be best-effort; a state machine does not prove every external side effect is fenced.

A retrospective alternative is synchronous dual writing to the primary database and search index. It gives a simpler visible request flow but creates a partial-failure window across systems. The outbox approach makes the pending obligation durable on the specified bulk path, while accepting index lag.

Bulk write transaction and asynchronous drain

Only the PostgreSQL-primary bulk path is inside the atomic boundary: canonical rows, ID/write-ID outbox obligations and a storage-drain task obligation. After commit, workers reread canonical data and materialize indexes. The separate ordinary-upsert path is explicitly outside this shared transaction guarantee.

Open full-size SVG
DynaStore · the bulk transaction stops at the outboxCanonical rows, ID or write-ID outbox obligations and a storage-drain task obligation share a transaction in the PostgreSQL bulk path. Workers later reread canonical state and materialize derived indexes. Ordinary upserts use a separate dispatch transaction. DynaStore · the bulk transaction stops at the outbox PostgreSQL-primary bulk-upsert contract · external indexes remain asynchronous ATOMIC BOUNDARY: PG-PRIMARY BULK UPSERT One managed PostgreSQL transaction Canonical rows PostgreSQL / PostGIS Storage outbox ID / write-ID obligations Drain task Task obligation atomic write enqueue AFTER COMMIT Task claiming, heartbeat and retry-oriented recovery Worker / drain Claim pending work Canonical state reread Hydrate current content Derived index Materialize data asynchronous drain read IDs index SEPARATE CONTRACT: ORDINARY UPSERT Primary write and dispatch do not share this bulk boundary Primary write Later dispatch transaction Index obligation post-write dispatch Lightweight obligations avoid making an event payload the authority. Cross-call duplicate obligations can occur. The bulk guarantee covers pending work, not exactly-once index effects. Unit-test seams are not production proof. ATOMIC BOUNDARY: PG-PRIMARY BULK UPSERT One managed PostgreSQL transaction AFTER COMMIT Task claiming, heartbeat and retry-oriented recovery SEPARATE CONTRACT: ORDINARY UPSERT Primary write and dispatch do not share this bulk boundary Request / integration Message / asynchronous work Persistence / data access

Outcomes: explicit contracts in public source

The inspected snapshot exposes the storage lanes, modular architecture, lifecycle state model and recoverable task machinery. Selected unit tests cover outbox rollback contracts and routing/lifecycle behavior. The atomicity tests use test seams and mocks; they are not a production or real-database rollback demonstration.

A published DuckDB case study illustrates querying supported geospatial files in place. It provides a complementary access pattern rather than evidence that all formats, all drivers or all queries share the same semantics.

DynaStore and its institutional deployments are FAO work delivered with colleagues and collaborators. This case does not turn design capacity, code coverage or conformance declarations into observed operational outcomes.

Learning: a useful abstraction preserves differences

Write durability, search freshness and read capability are separate contracts. Naming them makes an architecture easier to reason about and avoids describing a derived index as a second canonical database.

For further operational validation I would test real database rollback, worker death during index updates, delayed visibility changes and stale cleanup work. These are concrete verification targets beyond the source evidence inspected here.

Questions & discussion

Is the database write atomic with indexing?

The PostgreSQL-primary bulk-upsert commits canonical rows and durable indexing/task obligations together. External indexing happens later. Ordinary upsert paths use separate dispatch transactions and must be described separately.

Read the relevant section
Will failed or empty search always fall back to PostgreSQL?

No universal runtime failover is established. SEARCH configuration derives from INDEX and then READ, and selected helper paths have fallback behavior. The exact query path and driver capabilities control the semantics.

Read the relevant section
Why reread canonical state when draining?

The inspected outbox carries lightweight IDs or write IDs. Rereading makes PostgreSQL the source for projection content and supports retry-oriented materialization; it does not imply globally ordered events or exactly-once effects.

Read the relevant section

Sources & scope

Related documents

Presentation · 2026

Agro-Informatics Platform: GeoID and DynaStore development upgrade

Complete eight-slide source presentation for the v0.20.83 to v0.27.89 development-environment upgrade. Service boundaries, bounded database waits, task execution and catalog integrity. Claims concern the described development environment.

Carlo Cancellieri. July 2026 source presentation. Development-environment architecture and upgrade account; no production scale or independent performance validation is asserted.

Read the related case study