Skip to content

feat(request): add optional idempotency key for request creation#72

Merged
igorsatsyuk merged 13 commits into
mainfrom
feature/idempotency-key-for-request-creation
Jun 27, 2026
Merged

feat(request): add optional idempotency key for request creation#72
igorsatsyuk merged 13 commits into
mainfrom
feature/idempotency-key-for-request-creation

Conversation

@igorsatsyuk

Copy link
Copy Markdown
Owner

Summary

  • Add optional idempotencyKey (UUID) field to CreateClientRequest DTO
  • When provided, the key is used as the request id instead of generating a new UUID
  • Duplicate idempotency key gracefully returns the existing request instead of failing

Changes

  • CreateClientRequest: added optional UUID idempotencyKey field
  • RequestService.submitClientCreateRequest: uses idempotency key as request ID when present; handles duplicate key constraint violations by returning the existing request
  • Updated all existing CreateClientRequest constructor calls across test files

Tests added

Unit tests (RequestServiceTest):

  • submitClientCreateRequest_withIdempotencyKey_usesKeyAsRequestId
  • submitClientCreateRequest_withoutIdempotencyKey_generatesNewUuid
  • submitClientCreateRequest_duplicateIdempotencyKey_returnsExistingRequest
  • submitClientCreateRequest_duplicateIdempotencyKey_returnsExistingRequestWhenCompleted
  • submitClientCreateRequest_nonIdempotencyDuplicateKeyError_propagatesOriginalError

Integration tests (RequestIntegrationIT):

  • create_client_request_with_idempotencyKey_uses_key_as_request_id
  • create_client_request_duplicate_idempotencyKey_returns_existing_request
  • create_client_request_without_idempotencyKey_generates_new_id
  • create_client_request_with_idempotencyKey_different_keys_create_separate_requests

Add optional idempotencyKey (UUID) field to CreateClientRequest. When provided,
it is used as the request id instead of generating a new UUID. Duplicate
idempotency key gracefully returns the existing request instead of failing.

Unit and integration tests cover positive scenarios (key used as id, null key
generates new UUID, different keys create separate requests) and negative
scenarios (duplicate key returns existing request, unrelated DB errors propagate).
@igorsatsyuk igorsatsyuk self-assigned this Jun 26, 2026
- Add idempotency_key column (UNIQUE, nullable) to request table via V5 migration
- Server always generates request.id; idempotency key stored in separate column
- Validate payload equality on duplicate idempotency key; throw
  IdempotencyKeyConflictException (409) on mismatch
- Detect duplicate key via DuplicateKeyException and SQL state 23505
  instead of fragile message string matching
- Add IdempotencyKeyConflictException with GlobalExceptionHandler support
- Add localized error messages (en/ru) for idempotency conflict
- Update unit and integration tests for new flow
@igorsatsyuk

Copy link
Copy Markdown
Owner Author

Summary

  • Add optional idempotencyKey (UUID) field to CreateClientRequest DTO
  • Server always generates request.id; idempotency key stored in separate idempotency_key column
  • Duplicate idempotency key with same payload returns existing request (idempotent)
  • Duplicate idempotency key with different payload returns 409 Conflict

Changes

  • CreateClientRequest: added optional UUID idempotencyKey field
  • Request model: added idempotencyKey field mapped to idempotency_key column
  • RequestRepository: updated insertRequest to include idempotency key; added findByIdempotencyKey
  • RequestService.submitClientCreateRequest: lookup by idempotency key before insert; validate payload equality on duplicate; IdempotencyKeyConflictException on mismatch
  • IdempotencyKeyConflictException: new exception with GlobalExceptionHandler mapping to 409
  • V5__add_request_idempotency_key.sql: adds nullable UUID column with unique partial index
  • Localized error messages (en/ru) for idempotency conflict

Review feedback addressed

  1. Security: idempotency key no longer exposed as request ID; server-generated UUID is always the public identifier
  2. Payload validation: duplicate key with different payload returns 409 Conflict instead of silently succeeding
  3. Fragile error detection: replaced message string matching with DuplicateKeyException and SQL state 23505

Tests

Unit tests (RequestServiceTest):

  • submitClientCreateRequest_withIdempotencyKey_generatesServerIdAndPersists
  • submitClientCreateRequest_withoutIdempotencyKey_generatesNewUuid
  • submitClientCreateRequest_duplicateIdempotencyKey_samePayload_returnsExistingRequest
  • submitClientCreateRequest_duplicateIdempotencyKey_samePayload_completedStatus
  • submitClientCreateRequest_duplicateIdempotencyKey_differentPayload_throwsConflict
  • submitClientCreateRequest_idempotencyKey_lookupError_propagates

Integration tests (RequestIntegrationIT):

  • create_client_request_with_idempotencyKey_generates_server_id
  • create_client_request_duplicate_idempotencyKey_same_payload_returns_existing
  • create_client_request_duplicate_idempotencyKey_different_payload_returns_conflict
  • create_client_request_without_idempotencyKey_generates_new_id
  • create_client_request_with_idempotencyKey_different_keys_create_separate_requests

- Remove idempotency_key column, IdempotencyKeyConflictException, and
  related migration — idempotency key is stored as Request.id, not a
  separate column
- When idempotencyKey provided in CreateClientRequest, use it as
  Request.id instead of generating a new UUID
- On DuplicateKeyException (same key reused), return existing request
- Use DuplicateKeyException instead of fragile message string matching
When the same idempotency key is reused with a different payload,
return 409 Conflict instead of silently returning the existing request.
This ensures correct idempotency semantics: same key + same payload =
idempotent; same key + different payload = error.
…atus

- Add auth_client_id column to request table via V5 migration
- Store clientId from token in Request.authClientId on creation
- On duplicate idempotency key, verify both payload AND authClientId
  match before returning existing request; otherwise 409 Conflict
- On GET /api/requests/{id}, verify authClientId matches; otherwise 404
- Controllers extract clientId from SecurityService reactive chain
- Add unit and integration tests for ownership validation
response_data is always NULL on insert, so hardcode it in the SQL
query and remove it as a method parameter, satisfying the SonarQube
rule limiting method parameters to 7.
… UUID tests

- Handle HttpMessageNotReadableException in GlobalExceptionHandler
  returning 400 with localized error.request.invalidPayload message
- Add error.request.invalidPayload to all messages*.properties (en/ru)
- Add integration tests for invalid UUID in GET /api/requests/{id}
- Add integration test for invalid idempotencyKey UUID in POST /api/clients
- Add unit test for HttpMessageNotReadableException handler
Existing rows get 'unknown' as default; model default matches.
Column auth_client_id is NOT NULL, so the test must supply a value.
…le reads

- Merge V6 into V5: composite PK (id, auth_client_id) in one migration
- getRequestStatus uses findByIdAndAuthClientId; falls back to findById
  for 'unknown' auth_client_id (transitional period for old rows)
- markCompleted/markFailed include auth_client_id in WHERE clause
- Worker processClaimedRequest passes authClientId to mark methods
- Tests updated for new signatures and composite PK behavior
- Different clients can now have separate requests with the same
  idempotency key (idempotency is per-client)
- V5 migration: just adds auth_client_id column (NOT NULL DEFAULT 'unknown'),
  no composite PK or unique index needed — id is already unique PK
- Worker queries (claim, reclaim) are correct as-is since id is unique
- markCompleted/markFailed include auth_client_id in WHERE for safety
- RequestWorkerRetryIT: set authClientId on Request builder to fix NPE
- getRequestStatus uses findByIdAndAuthClientId with findById fallback
  for backward-compatible reads of 'unknown' auth_client_id rows
- API.md: document optional idempotencyKey field, idempotency semantics,
  and 409 Conflict on payload mismatch
- CHANGELOG.md: add idempotency key, auth_client_id, HttpMessageNotReadable
  exception handler, and new error messages
- ClientController: update @operation description for idempotency key
@sonarqubecloud

Copy link
Copy Markdown

@igorsatsyuk igorsatsyuk merged commit c7ee71e into main Jun 27, 2026
8 checks passed
@igorsatsyuk igorsatsyuk deleted the feature/idempotency-key-for-request-creation branch June 27, 2026 08:14
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