Skip to content

feat: idempotency — transactional deduplication (flip the switch)#74

Open
alexgarzao wants to merge 11 commits intofeat/idempotency-key-support--transactional-repository-methodsfrom
feat/idempotency-key-support--transactional-deduplication
Open

feat: idempotency — transactional deduplication (flip the switch)#74
alexgarzao wants to merge 11 commits intofeat/idempotency-key-support--transactional-repository-methodsfrom
feat/idempotency-key-support--transactional-deduplication

Conversation

@alexgarzao
Copy link
Collaborator

Summary

Implements idempotent transaction validation: retrying POST /v1/validations with the same request_id returns the original result without re-processing.

What Changed

  • ValidateResult struct: New return type with Response and IsDuplicate fields
  • Duplicate detection: FindByRequestID check before any processing (Stripe model, DD-3)
  • HTTP status codes: 201 Created for new requests, 200 OK for duplicates (DD-9)
  • HashUUIDToInt32: FNV-1a utility for advisory lock keys (DD-8)
  • Bootstrap wiring: transactionValidationQueryRepo dependency added

Design Decisions Implemented

DD Decision
DD-3 Deduplication by request_id only (Stripe model)
DD-6 Inline transaction — tx passed as pgdb.DB parameter
DD-8 Advisory lock per request_id via pg_advisory_xact_lock
DD-9 Duplicate = success, not error
DD-11 DENY-by-rule uses ON CONFLICT DO NOTHING
DD-12 Best-effort persist for DENY/REVIEW

Behavior Change

Scenario Before After
New request HTTP 200 HTTP 201
Duplicate request_id Re-processes, double-counts HTTP 200, cached response
DENY retry UNIQUE violation risk Silent dedup

Test Coverage

  • 6 new idempotency service tests (duplicate detection, no-double-count, no-audit, DENY/REVIEW paths)
  • 2 new handler tests (201/200 status codes)
  • 4 hash utility tests (determinism, distribution, boundaries)
  • All existing tests updated for new constructor and ValidateResult return type
  • Unit tests pass, lint clean

Commits

  1. feat: add HashUUIDToInt32 utility for advisory lock keys
  2. feat: add ValidateResult and duplicate detection in Validate flow
  3. feat: return HTTP 201 for new validations, 200 for duplicates
  4. test: add idempotency unit tests for deduplication flow
  5. test: update existing tests for ValidateResult return type
  6. style: replace task ID references with descriptive comments

Breaking Changes

  • ValidationService.Validate() now returns *ValidateResult instead of *ValidationResponse
  • NewValidationService constructor takes 6 parameters (added transactionValidationQueryRepo)
  • POST /v1/validations returns HTTP 201 for new requests (was 200)

Integration tests note

Integration tests need to be updated to expect HTTP 201 for new validation requests (was 200). This is a deliberate behavior change per DD-9.

Depends On

  • T-001 (schema + FindByRequestID + ToValidationResponse)
  • T-002 (InsertWithTx + CheckLimitsWithTx)

FNV-1a 32-bit hash converts UUID to int32 for use with
pg_advisory_xact_lock. Deterministic, fast, and well-distributed
across the int32 range.
- Add ValidateResult struct with Response and IsDuplicate fields
- Add transactionValidationQueryRepo dependency for FindByRequestID
- Check for existing record before processing (DD-3: Stripe model)
- Duplicate requests return cached response, skip rule/limit eval
- Add ErrNilTransactionValidationQueryRepo sentinel error
- Update constructor to accept and validate query repo dependency
- Handler uses ValidateResult.IsDuplicate to select status code
- HTTP 201 Created for new requests (DD-9)
- HTTP 200 OK for duplicate requests returning cached response
- Wire transactionValidationQueryRepo in bootstrap config
- TestValidate_DuplicateRequestID_ReturnsOriginal
- TestValidate_DuplicateRequestID_NoDoubleCount
- TestValidate_DuplicateRequestID_NoAudit
- TestValidate_DenyByRule_RetryTolerant
- TestValidate_DenyByLimit_RollbackAtomic
- TestValidate_Review_RollbackAtomic
- TestValidationHandler_Validate_ReturnsCorrectStatusCodes (201/200)
Adapt all existing validation service and handler tests to use the
new 6-parameter constructor and ValidateResult return type. Add
FindByRequestID mock expectations for non-duplicate flows.
Replace TODO(T-003) markers with NOTE comments per
PROJECT_RULES.md rule against task IDs in source code.
@alexgarzao alexgarzao requested a review from a team as a code owner March 20, 2026 23:47
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • develop

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c236d69d-4a43-439a-b5ca-c25406bf3f52

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request introduces idempotency support for validation requests alongside infrastructure updates. The validation service now checks for duplicate requests by request_id before processing, returning cached responses with HTTP 200 OK for duplicates and 201 Created for new validations. Repository implementations were extended with transactional variants (InsertWithTx, CheckLimitsWithTx), and HTTP handlers updated to return appropriate status codes. Workflow configurations were upgraded to newer shared workflow versions (v1.18.1), and documentation strings were cleaned up by removing section references.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler as ValidationHandler
    participant Service as ValidationService
    participant QueryRepo as TransactionValidationQueryRepository
    participant RuleEval as RuleEvaluator
    participant LimitChecker
    participant CommandRepo as TransactionValidationRepository
    
    Client->>Handler: POST /v1/validations (request_id)
    Handler->>Service: Validate(ctx, request)
    
    Service->>QueryRepo: FindByRequestID(ctx, request_id)
    
    alt Duplicate Request Found
        QueryRepo-->>Service: existing validation
        Service-->>Handler: ValidateResult{Response: cached, IsDuplicate: true}
        Handler-->>Client: HTTP 200 OK (cached response)
    else New Request (Not Found)
        QueryRepo-->>Service: nil (not found)
        
        Service->>RuleEval: Evaluate(ctx, rules)
        RuleEval-->>Service: decision (Approve/Deny/Review)
        
        Service->>LimitChecker: CheckLimits(ctx, input)
        LimitChecker-->>Service: CheckLimitsOutput{Allowed, Details}
        
        Service->>CommandRepo: Insert(ctx, validation)
        CommandRepo-->>Service: ✓ inserted
        
        Service-->>Handler: ValidateResult{Response: new, IsDuplicate: false}
        Handler-->>Client: HTTP 201 Created (new response)
    end
Loading
🚥 Pre-merge checks | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title references 'T-003' (a task ID), which violates the project rule forbidding task/ticket IDs in code and related artifacts. While the feature itself (idempotent transactional deduplication) is the main change, the task ID reference makes the title non-compliant. Remove the task ID 'T-003' from the title. Use descriptive language instead, e.g., 'feat(idempotency): implement transactional deduplication and duplicate detection'.
Description check ❓ Inconclusive The description is comprehensive and well-structured, covering summary, changes, design decisions, behavior changes, test coverage, and breaking changes. However, it uses task ID references (T-001, T-002, T-003, DD-*) throughout, which violates the project rule against task/ticket IDs in source code and related materials. Replace all task ID and design-decision references (T-001, T-002, T-003, DD-3, DD-6, etc.) with descriptive text explaining the decisions and dependencies. Keep the structured format and content.

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


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

@lerian-studio
Copy link

Consider updating CHANGELOG.md to document this change. If this change doesn't need a changelog entry, add the skip-changelog label.

@lerian-studio
Copy link

This PR is very large (35 files, 2333 lines changed). Consider breaking it into smaller PRs for easier review.

@lerian-studio
Copy link

📊 Unit Test Coverage Report: tracer

Metric Value
Overall Coverage 82.8% ⚠️ BELOW THRESHOLD
Threshold 85%

Coverage by Package

Package Coverage
tracer/internal/adapters/cel 81.9%
tracer/internal/adapters/http/in/middleware 62.0%
tracer/internal/adapters/http/in 81.5%
tracer/internal/adapters/postgres/db 0.0%
tracer/internal/adapters/postgres 74.8%
tracer/internal/services/cache 95.6%
tracer/internal/services/command 81.5%
tracer/internal/services/query 80.6%
tracer/internal/services/workers 79.7%
tracer/internal/services 40.1%
tracer/internal/testhelper 0.0%
tracer/pkg/clock 50.0%
tracer/pkg/contextutil 100.0%
tracer/pkg/hash 100.0%
tracer/pkg/logging 100.0%
tracer/pkg/migration 89.0%
tracer/pkg/model 95.0%
tracer/pkg/net/http 88.3%
tracer/pkg/resilience 97.8%
tracer/pkg/sanitize 87.1%
tracer/pkg/validation 50.0%
tracer/pkg 96.6%

Generated by Go PR Analysis workflow

@lerian-studio
Copy link

🔒 Security Scan Results — tracer

Trivy

Filesystem Scan

✅ No vulnerabilities or secrets found.

Docker Image Scan

✅ No vulnerabilities found.


Docker Hub Health Score Compliance

✅ Policies — 4/4 met

Policy Status
Default non-root user ✅ Passed
No fixable critical/high CVEs ✅ Passed
No high-profile vulnerabilities ✅ Passed
No AGPL v3 licenses ✅ Passed

🔍 View full scan logs

// Write UUID bytes directly - uuid.UUID is a [16]byte array
h.Write(id[:])

return int32(h.Sum32())

Check failure

Code scanning / gosec

integer overflow conversion uint32 -> int32 Error

integer overflow conversion uint32 -> int32
func HashUUIDToInt32(id uuid.UUID) int32 {
h := fnv.New32a()
// Write UUID bytes directly - uuid.UUID is a [16]byte array
h.Write(id[:])

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
@alexgarzao alexgarzao changed the base branch from develop to feat/idempotency-key-support--transactional-repository-methods March 20, 2026 23:49
@blacksmith-sh
Copy link

blacksmith-sh bot commented Mar 20, 2026

No description provided.

The Validate endpoint now returns 201 for new requests and 200 for
duplicates, but Swagger only documented 200. Add the 201 response
entry and clarify both descriptions.
Add mutation of AccountID pointer inside Scopes to catch shallow
copy of nested pointer fields. The append-only check would miss
shared pointer references between original and copy.
These 4 comments were scaffolding from the TDD-RED phase and should
have been removed when the implementation was added in TDD-GREEN.
Replace time.Now().UTC() with testutil.DefaultTestTime for stable
test data across runs. Remove unused time import.
@alexgarzao alexgarzao force-pushed the feat/idempotency-key-support--transactional-deduplication branch from 7423e7c to efdda00 Compare March 21, 2026 00:19
@alexgarzao alexgarzao self-assigned this Mar 21, 2026
All POST /v1/validations now return 201 Created (new) or 200 OK
(duplicate). Update 16 integration test files:

- Change StatusOK to StatusCreated for POST validation assertions
- Preserve StatusOK for GET/PUT/PATCH endpoints (limits, rules, etc.)
- Update tests 1_1_52 and 1_1_53 for idempotent behavior: duplicate
  requestId returns cached response (200) instead of re-processing
- Use uuid.New() in auth and limit tests to avoid request_id
  collisions between tests sharing the same database
@alexgarzao alexgarzao changed the title feat(idempotency): T-003 — transactional deduplication (flip the switch) feat: idempotency — transactional deduplication (flip the switch) Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants