Skip to content

feat(eventbus): add idempotent consumer dedup primitive - #203

Merged
tiana-code merged 1 commit into
mainfrom
feat/E-03-idempotent-consumer
Jun 18, 2026
Merged

feat(eventbus): add idempotent consumer dedup primitive#203
tiana-code merged 1 commit into
mainfrom
feat/E-03-idempotent-consumer

Conversation

@tiana-code

Copy link
Copy Markdown
Owner

What

Consumer-side counterpart to the at-least-once producer/dispatcher: a reusable dedup primitive so consumers process each envelope exactly once. No consumer service exists yet, so this ships the generic, tested toolkit in libs/fincore-eventbus; the per-service processed_events migration lands with the first real consumer (E-02).

  • ProcessedEventStore.markIfFirstSeen(envelopeId, consumerGroup) - dedup keyed by (envelope id, consumer group) so independent consumers each process an event once.
  • IdempotentEventProcessor.process(envelopeId, consumerGroup, handler) - claim-then-handle: a thrown handler propagates so the caller's transaction rolls back the claim and the event is retried (exactly-once effect, never silent loss or double effect).
  • JdbcProcessedEventStore - INSERT ... ON CONFLICT DO NOTHING, transaction-participating; InMemoryProcessedEventStore for tests/dev. The store is wired by the consuming service - never auto-defaulted to in-memory in production (that would silently reprocess after a restart).
  • Reference processed-events.sql DDL (PK (envelope_id, consumer_group)) for adopting services.

Security

tableName is the only interpolated SQL token (JDBC can't bind identifiers); it is validated at construction against ^[A-Za-z_][A-Za-z0-9_]*$. All values are bound parameters.

Gates

Local (-x integrationTest, Docker down in WSL): :libs:fincore-eventbus:{test,detekt,detektTest,spotlessCheck,assemble,compileIntegrationTestKotlin} green. The Testcontainers-Postgres IT runs on CI: dedup, claim rolled back when the surrounding transaction rolls back (asserted in a separate transaction), and two concurrent transactions where exactly one wins first-seen. critic GO, code-reviewer APPROVE (2 must-fixes applied), security-auditor PASS, evaluator 0.89. spring-jdbc added BOM-managed.

Closes #192

Add a reusable consumer-side dedup toolkit so at-least-once consumers process an
event exactly once. ProcessedEventStore keys dedup by (envelope id, consumer group);
IdempotentEventProcessor claims first then runs the handler, propagating handler
exceptions so the caller's transaction rolls back the claim and the event is retried.
JdbcProcessedEventStore claims via INSERT ON CONFLICT DO NOTHING and participates in
the ambient transaction; InMemoryProcessedEventStore serves tests and local dev. The
store is wired by the consuming service, never auto-defaulted to in-memory in
production. Covered by unit tests plus a Testcontainers-Postgres integration test for
dedup, transactional rollback of the claim, and concurrent first-seen.

Closes #192
@tiana-code tiana-code added this to the v0.2.0 milestone Jun 18, 2026
@tiana-code
tiana-code merged commit d99b651 into main Jun 18, 2026
8 checks passed
@tiana-code
tiana-code deleted the feat/E-03-idempotent-consumer branch June 18, 2026 05:18
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.

F-03.5 Idempotent consumer base class / annotation

1 participant