Skip to content

docs: specify Postgres repository contract#29

Merged
patrickleet merged 2 commits into
mainfrom
docs/postgres-repository-contract
May 21, 2026
Merged

docs: specify Postgres repository contract#29
patrickleet merged 2 commits into
mainfrom
docs/postgres-repository-contract

Conversation

@patrickleet
Copy link
Copy Markdown
Collaborator

@patrickleet patrickleet commented May 21, 2026

Summary

  • define the Postgres aggregate event table schema, sequence constraint, and indexes
  • specify stream identity as aggregate_type plus aggregate_id and keep aggregate event records separate from published messages
  • document snapshot side-table shape, transactional commit semantics, and optimistic concurrency mapping
  • record TypeORM lineage decisions, async-first/deferred implementation posture, and the test plan

Verification

  • cargo fmt --check
  • git diff --check
  • cargo test --doc --all-features
  • cargo test --all-features

Note: all-features still reports the existing Reservation dead-code warning in tests/sagas/order/inventory.rs.

Summary by CodeRabbit

  • Documentation
    • Expanded Postgres event store spec: formal stream identity and repository scope, durable table contracts (PKs, constraints, index guidance), separate snapshot table with hydration/retention rules, codec validation and error model, commit/transactional semantics (batch handling, sequence assignment, rollback, optimistic concurrency), locking and async guidance, TypeORM differences, and required test/behavior parity.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72efc3f7-374a-4612-ac50-7e6308fcc1c4

📥 Commits

Reviewing files that changed from the base of the PR and between b145f6c and 2247015.

📒 Files selected for processing (1)
  • docs/postgres-event-store.md

📝 Walkthrough

Walkthrough

This PR expands docs/postgres-event-store.md into a full Postgres event store contract covering repository scope/stream identity, durable event and snapshot schemas, codec validation/error model, commit/transaction semantics, optimistic concurrency, locking/async guidance, TypeORM lineage, and required tests.

Changes

Postgres Event Store Contract Specification

Layer / File(s) Summary
Repository Scope & Stream Identity
docs/postgres-event-store.md
Reframes the contract to define repository responsibilities (stream loading/appending, optimistic concurrency, payload metadata, batch commits) and formalizes stream identity as (aggregate_type, aggregate_id) tuple with guidance on stable naming.
Storage Schema & Hydration Algorithm
docs/postgres-event-store.md
Specifies aggregate_events table columns, constraints, PRIMARY KEY (aggregate_type, aggregate_id, sequence), recommended indexes, and adds aggregate_snapshots side-table; documents the "latest snapshot then tail events" hydration procedure (sequence 1 fallback).
Codec Validation & Error Model
docs/postgres-event-store.md
Requires write-time rejection for unknown/unsupported payload_codec tuples, read-time decode rejection unless a decode path exists, and maps these failures to RepositoryError::Model/Replay with associated Postgres tests.
Commit Semantics & Sequence Invariants
docs/postgres-event-store.md
Prescribes batch validation, duplicate-stream rejection within a batch, per-stream expected-version checks, computed sequence assignment, transactional all-or-nothing visibility, and maps unique-key violations to RepositoryError::ConcurrentWrite.
Optimistic Concurrency Conflict Handling
docs/postgres-event-store.md
Specifies expected version usage, next sequence assignment, and requires RepositoryError::ConcurrentWrite to include aggregate type/id and expected/actual versions (with JSON id formatting rules).
Locking Guidance & Async Posture
docs/postgres-event-store.md
Clarifies QueuedRepository is process-local, recommends DB-backed locks for cross-process coordination, and mandates async-first implementation posture for production Postgres repositories.
TypeORM Lineage & Field Mapping
docs/postgres-event-store.md
Documents TypeORM-to-Postgres field mapping and enumerates intentional semantic differences (PK composition, event name non-uniqueness, snapshots separated, synchronize not a migration strategy).
Testing Contract & Coverage
docs/postgres-event-store.md
Lists in-memory behavior parity requirements and adds Postgres-specific tests: migrations/schema, commit_batch atomicity, duplicate detection, concurrency conflict, snapshot cycles, codec rejection, and transactional rollback coverage.
Out-of-Scope Clarification
docs/postgres-event-store.md
Explicitly excludes Postgres repository implementation, read-model layout, outbox table design, and synchronous trait replacement from the document's scope.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A contract so clear, now written with care,

Streams and snapshots dancing in pair,
Codecs checked, sequences kept in a line,
Transactions commit, rollbacks decline,
a rabbit hops off, docs safe and fine

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'docs: specify Postgres repository contract' accurately and concisely describes the main change in the pull request, which is formalizing and expanding the Postgres event store documentation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/postgres-repository-contract

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
docs/postgres-event-store.md (2)

178-180: 💤 Low value

Consider delimiter collision risk in interim ID formatting.

The interim solution formats id as "{aggregate_type}:{aggregate_id}" for ConcurrentWrite errors. If aggregate types or IDs can contain colons, this formatting could be ambiguous. Since line 180 acknowledges this is temporary and "a later API can split type and ID if needed," consider using a less collision-prone delimiter (e.g., |, tab, or JSON encoding) if this interim solution lasts longer than expected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/postgres-event-store.md` around lines 178 - 180, The current interim
formatting of the ConcurrentWrite error uses string concat
"{aggregate_type}:{aggregate_id}" which risks collision if either part contains
':', so update the code that constructs RepositoryError::ConcurrentWrite { id,
expected, actual } to use a collision-resistant encoding (for example encode id
as JSON object like {"type":"...","id":"..."} serialized to a string, or use a
less common delimiter such as '|' or '\t'); locate the code that formats the id
for RepositoryError::ConcurrentWrite and replace the concat with the chosen
encoding/delimiter and add a brief comment referencing this is an interim format
to be split by a future API.

90-120: 💤 Low value

Consider documenting snapshot retention strategy.

The primary key (aggregate_type, aggregate_id, version) allows multiple historical snapshots per stream, which is useful for auditing. However, the contract doesn't specify whether implementations should retain all snapshots indefinitely, implement a retention policy (e.g., keep last N), or replace snapshots. If this flexibility is intentional, consider adding a brief note so implementers know retention is an implementation decision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/postgres-event-store.md` around lines 90 - 120, Add a short note to the
Snapshot Table section clarifying that the primary key (aggregate_type,
aggregate_id, version) permits multiple historical snapshots and that the
specification intentionally does not mandate retention semantics; suggest common
strategies implementers may choose (retain all for audit, keep last N,
time-based expiry, or replace on new snapshot) and recommend documenting chosen
policy per implementation so consumers know snapshot lifecycle expectations for
aggregate_snapshots, version, and hydration behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/postgres-event-store.md`:
- Around line 45-61: The contract for the aggregate_events table adds
payload_codec and payload_codec_version but doesn't specify codec validation
behavior; update the docs to add a "Codec validation and error model" subsection
that explicitly states whether validation occurs on write, on read, or both,
names the responsible components (e.g., Repository.write/Repository.read and
Serializer.deserialize/Serializer.validate), defines the exact error types or
error codes returned for codec mismatches, and documents the policy for allowing
unknown codecs on write (forward-compatibility) versus rejecting them (safety),
referencing the payload_codec and payload_codec_version fields and the
aggregate_events table so tests that assert "codec rejection for unknown
payload_codec or payload_codec_version" have a clear contract to validate
against.
- Around line 62-70: Update the schema constraints to enforce non-null,
non-empty aggregate identifiers by marking aggregate_type and aggregate_id NOT
NULL and adding CHECK clauses such as CHECK (aggregate_type <> '') and CHECK
(aggregate_id <> ''); ensure these are included alongside the existing PRIMARY
KEY (aggregate_type, aggregate_id, sequence) and the other CHECKs (sequence > 0,
event_version > 0, payload_codec <> '', payload_codec_version > 0) so the
database itself prevents empty/NULL aggregate_type and aggregate_id values.

---

Nitpick comments:
In `@docs/postgres-event-store.md`:
- Around line 178-180: The current interim formatting of the ConcurrentWrite
error uses string concat "{aggregate_type}:{aggregate_id}" which risks collision
if either part contains ':', so update the code that constructs
RepositoryError::ConcurrentWrite { id, expected, actual } to use a
collision-resistant encoding (for example encode id as JSON object like
{"type":"...","id":"..."} serialized to a string, or use a less common delimiter
such as '|' or '\t'); locate the code that formats the id for
RepositoryError::ConcurrentWrite and replace the concat with the chosen
encoding/delimiter and add a brief comment referencing this is an interim format
to be split by a future API.
- Around line 90-120: Add a short note to the Snapshot Table section clarifying
that the primary key (aggregate_type, aggregate_id, version) permits multiple
historical snapshots and that the specification intentionally does not mandate
retention semantics; suggest common strategies implementers may choose (retain
all for audit, keep last N, time-based expiry, or replace on new snapshot) and
recommend documenting chosen policy per implementation so consumers know
snapshot lifecycle expectations for aggregate_snapshots, version, and hydration
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e433dab8-93f0-48e8-9f45-45856d4503ad

📥 Commits

Reviewing files that changed from the base of the PR and between 21b93b9 and e547569.

📒 Files selected for processing (1)
  • docs/postgres-event-store.md

Comment thread docs/postgres-event-store.md
Comment thread docs/postgres-event-store.md
Implements [[tasks/design-postgres-repository-contract]]
@patrickleet patrickleet force-pushed the docs/postgres-repository-contract branch from e547569 to b145f6c Compare May 21, 2026 04:14
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
docs/postgres-event-store.md (2)

59-59: 💤 Low value

Consider showing explicit DEFAULT in the schema DDL documentation.

Line 59 mentions metadata with "default {}" and lines 250-252 state migrations should write metadata jsonb NOT NULL DEFAULT '{}', but the constraint section (lines 67-75) doesn't show this DEFAULT clause. For completeness, consider adding it to the schema documentation or constraints section.

📋 Optional schema refinement

Update line 59 or add to the constraints section:

-| `metadata` | `jsonb` | Event metadata, default `{}`. |
+| `metadata` | `jsonb` | Event metadata; `NOT NULL DEFAULT '{}'`. |

Also applies to: 250-252

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/postgres-event-store.md` at line 59, Update the schema docs to
explicitly show the DEFAULT for the metadata column: where `metadata` is
described as `jsonb` (the column symbol `metadata`) include the `NOT NULL
DEFAULT '{}'` clause in the DDL examples and in the constraints section;
likewise update the migration guidance that currently states `metadata jsonb NOT
NULL DEFAULT '{}'` so the constraints block and any sample CREATE TABLE/ALTER
statements consistently show `DEFAULT '{}'` for `metadata`.

161-163: 💤 Low value

Consider clarifying snapshot version validation during hydration.

The hydration algorithm specifies loading the newest snapshot then replaying events where sequence > snapshot.version. Consider adding guidance for the edge case where snapshot.version exceeds the current maximum event sequence (which could indicate data inconsistency or corruption).

📋 Suggested clarification

After line 163, add:

If the snapshot version exceeds the current maximum event sequence for the stream,
the implementation should either reject the load with `RepositoryError::Model` or
log a warning and proceed with the snapshot state. The first implementation should
fail fast to detect data inconsistencies.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/postgres-event-store.md` around lines 161 - 163, Update the hydration
docs to clarify validation when snapshot.version is larger than the current max
event sequence: in the hydrate algorithm (the description that loads the newest
snapshot and replays events where sequence > snapshot.version) add guidance that
implementations must detect this inconsistency and either reject the load with
RepositoryError::Model (fail-fast) or log a warning and continue using the
snapshot state; recommend preferring the fail-fast RepositoryError::Model
approach to surface data corruption.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/postgres-event-store.md`:
- Around line 57-58: Update the table schema and constraint documentation to
mark payload_codec, payload_codec_version, event_name, and payload as NOT NULL:
locate the event table definition where columns payload_codec and
payload_codec_version are listed and add NOT NULL to those column specs, and
likewise mark event_name and payload as NOT NULL in their column entries; also
update the constraint list to reflect these NOT NULL requirements (in addition
to the existing CHECKs that prevent empty strings) so the documentation and
constraints consistently enforce non-null codec metadata and required fields.
- Around line 143-144: Update the snapshot table column definitions in
docs/postgres-event-store.md so that payload, payload_codec, and
payload_codec_version are declared NOT NULL (to match the validation contract),
and apply the same NOT NULL change to the other snapshot occurrences in the
file; optionally add CHECK constraints for payload_codec (e.g., allowed codec
labels) and for payload_codec_version (e.g., > 0) to make the contract explicit.
Ensure you update the snapshot table column list where `payload`,
`payload_codec`, and `payload_codec_version` are documented and any adjacent
examples/DDL so they all reflect NOT NULL and the optional CHECKs.

---

Nitpick comments:
In `@docs/postgres-event-store.md`:
- Line 59: Update the schema docs to explicitly show the DEFAULT for the
metadata column: where `metadata` is described as `jsonb` (the column symbol
`metadata`) include the `NOT NULL DEFAULT '{}'` clause in the DDL examples and
in the constraints section; likewise update the migration guidance that
currently states `metadata jsonb NOT NULL DEFAULT '{}'` so the constraints block
and any sample CREATE TABLE/ALTER statements consistently show `DEFAULT '{}'`
for `metadata`.
- Around line 161-163: Update the hydration docs to clarify validation when
snapshot.version is larger than the current max event sequence: in the hydrate
algorithm (the description that loads the newest snapshot and replays events
where sequence > snapshot.version) add guidance that implementations must detect
this inconsistency and either reject the load with RepositoryError::Model
(fail-fast) or log a warning and continue using the snapshot state; recommend
preferring the fail-fast RepositoryError::Model approach to surface data
corruption.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d3d6218-affb-4a8b-a5ce-37698e2521e0

📥 Commits

Reviewing files that changed from the base of the PR and between e547569 and b145f6c.

📒 Files selected for processing (1)
  • docs/postgres-event-store.md

Comment thread docs/postgres-event-store.md Outdated
Comment thread docs/postgres-event-store.md Outdated
@patrickleet
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant