Skip to content

feat(ledger): add account and idempotency application services - #103

Merged
tiana-code merged 1 commit into
mainfrom
feat/E-01-account-idempotency-services
Jun 13, 2026
Merged

feat(ledger): add account and idempotency application services#103
tiana-code merged 1 commit into
mainfrom
feat/E-01-account-idempotency-services

Conversation

@tiana-code

Copy link
Copy Markdown
Owner

What

F-01.3 application services, PR 2a of 2 (Account + Idempotency; PR 2b will add Transaction posting + Balance).

  • AccountService (interface + impl): create, get, rename, changeStatus. Status transitions use the existing domain state machine (AccountStatus.canTransitionTo); changeStatus(.., CLOSED) is blocked unless the account holds no non-zero balance (account_balances read).
  • IdempotencyService (interface + impl): DB-backed, 24h TTL. Replays a stored response on a repeated (key, body); raises IdempotencyConflictException on a key reused with a different request; refreshes an expired key. Exactly-once under concurrency via the key_hash PK - the winner runs the action and commits, a racing loser fails the insert, rolls back, and retries in a fresh transaction to replay the committed response. SHA-256 hashes are computed at runtime (no hardcoded digests).
  • AccountPersistenceAdapter: hand-written domain<->entity conversion (resolves the Add MapStruct mappers (entity to domain, entity to DTO) #33 deferral for Account, since MapStruct cannot construct the value-class aggregate). MapStruct remains mandated for entity<->DTO in the REST slice.

@Transactional only in the impls; get is readOnly; no external I/O inside transactions.

Tests

Unit (MockK, 16 green locally): account create/get/rename/state-machine/close-guard, idempotency replay/conflict/expiry/race-signal/retry, adapter identity. Integration (@DataJpaTest + @Import, pg17 on CI): account create+read-back, idempotent replay (action runs once), conflict, close-blocked-by-balance, close-empty.

The full idempotency concurrency stress test is tracked separately as #57.

Verification

Local: :test (16/16 new unit tests green), :spotlessCheck, :detekt, :compileIntegrationTestKotlin, :assemble all BUILD SUCCESSFUL. Integration tests run on CI (Testcontainers); env-blocked under local WSL Docker.

Closes #18
Closes #20
Closes #26
Closes #44

AccountService (create, get, rename, status transitions) over the existing
domain state machine, with close-requires-zero-balance enforced by reading
account_balances. IdempotencyService is DB-backed with a 24h TTL: it replays
a stored response on a repeated key, raises a conflict on a key reused with a
different request, refreshes an expired key, and is exactly-once under
concurrency via the key_hash primary key (winner runs, loser retries and
replays). Account domain<->entity conversion is a hand-written adapter since
MapStruct cannot construct the value-class aggregate.

@transactional lives only in the impls; reads are readOnly; no external calls
inside transactions. Unit tests cover both services and the adapter; an
integration test exercises the account round-trip and idempotency paths on pg17.

Closes #18
Closes #20
Closes #26
Closes #44
@tiana-code
tiana-code merged commit ff3d9f4 into main Jun 13, 2026
6 checks passed
@tiana-code
tiana-code deleted the feat/E-01-account-idempotency-services branch June 13, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant