feat: migrate TradeConfirmationService from direct invocation (v1) to REST client interface (v2)#10
Conversation
… REST client interface (v2) - Create TradeConfirmationClient interface abstracting trade confirmation delivery - Add RestTradeConfirmationClient (REST implementation targeting external microservice) - Add FallbackTradeConfirmationClient (local logging when REST is unavailable) - Add ResilientTradeConfirmationClient (@primary decorator with toggle + fallback) - Update CounterpartyController to use TradeConfirmationClient interface - Update RFQExecutionSaga to send trade confirmation after RFQ execution - Mark old TradeConfirmationService and TradeConfirmationMicroserviceClient as @deprecated - Add unit tests for resilient client (REST path, fallback path, toggle off) - Add MIGRATION_CHECKLIST.md for remaining module migrations Co-Authored-By: Achal Channarasappa <achal.channarasappa@cognition.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| tradeConfirmationClient.sendConfirmation( | ||
| new TradeConfirmationDto(counterparty.getName(), rfqDto.getExecutionPrice())); |
There was a problem hiding this comment.
🔴 Trade confirmation sent inside @transactional boundary, before commit
The sendConfirmation call at lines 53-54 executes inside the @Transactional method executeRfq. The transaction only commits after the method returns (line 56), so when the REST path is enabled (use.confirmation.service=true), an HTTP POST is dispatched to the external confirmation microservice before the database transaction commits. If the commit subsequently fails (e.g., constraint violation detected at flush, DB connectivity loss), the confirmation has already been irrevocably sent for a trade that was rolled back — creating a phantom confirmation in the external service.
This directly violates the PR's own MIGRATION_CHECKLIST.md Phase 5, bullet 3: "If the module participates in a saga, verify the confirmation is sent after the transaction commits."
The fix is to move the confirmation outside the transactional boundary — e.g., using TransactionSynchronizationManager.registerSynchronization() with an afterCommit() callback, or a @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) pattern.
Prompt for agents
The trade confirmation REST call at RFQExecutionSaga.java:53-54 is made inside the @Transactional method executeRfq(). The transaction commits only after the method returns, so the confirmation is sent before the DB changes are committed. If the commit fails, a phantom confirmation has been sent to the external microservice.
To fix this, the confirmation should only be sent after the transaction successfully commits. Two common approaches:
1. Use TransactionSynchronizationManager: Register a synchronization callback inside executeRfq() that sends the confirmation in afterCommit(). This keeps the logic co-located.
2. Use @TransactionalEventListener: Publish a domain event (e.g., TradeExecutedEvent) from executeRfq(), and have a separate listener annotated with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) that calls tradeConfirmationClient.sendConfirmation().
Either approach ensures the REST call only fires after the trade data is durably committed. The second approach is more aligned with Spring best practices and the eventual move to event-driven choreography mentioned in MIGRATION_CHECKLIST.md.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch. Fixed in d45c235 — the saga now publishes a TradeExecutedEvent via ApplicationEventPublisher, and a separate TradeExecutedEventListener annotated with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) sends the confirmation only after the trade data is durably committed. This prevents phantom confirmations on rollback and aligns with the eventual event-driven choreography path noted in MIGRATION_CHECKLIST.md.
…onalEventListener Move the confirmation call out of the @transactional boundary to prevent phantom confirmations when the commit fails. RFQExecutionSaga now publishes a TradeExecutedEvent; TradeExecutedEventListener fires only AFTER_COMMIT. Co-Authored-By: Achal Channarasappa <achal.channarasappa@cognition.ai>
Summary
Migrates trade confirmation delivery from direct method invocation (
TradeConfirmationService) to aTradeConfirmationClientinterface backed by a resilient REST client with automatic fallback — following the Strangler Fig pattern.Before (v1)
CounterpartyControllerbranched on theuse.confirmation.servicetoggle to choose betweenTradeConfirmationService(local logger) andTradeConfirmationMicroserviceClient(REST).RFQExecutionSagadid not send any trade confirmation after execution.After (v2)
TradeConfirmationClientinterface in theclient/package.RestTradeConfirmationClient— calls the external confirmation microservice via REST.FallbackTradeConfirmationClient— logs locally when REST is unavailable.ResilientTradeConfirmationClient(@Primary) — respects the toggle; tries REST first, falls back on failure.CounterpartyControllerinjectsTradeConfirmationClient— no branching logic in callers.RFQExecutionSagapublishes aTradeExecutedEvent; aTradeExecutedEventListenerannotated with@TransactionalEventListener(phase = AFTER_COMMIT)sends the confirmation only after the transaction commits — preventing phantom confirmations for rolled-back trades.TradeConfirmationServiceandTradeConfirmationMicroserviceClientmarked@Deprecated.MIGRATION_CHECKLIST.mdprovides a repeatable playbook for migrating remaining modules.Files changed
client/TradeConfirmationClient.javaclient/RestTradeConfirmationClient.javaclient/FallbackTradeConfirmationClient.javaclient/ResilientTradeConfirmationClient.java@Primary)event/TradeExecutedEvent.javaevent/TradeExecutedEventListener.javacontroller/CounterpartyController.javaTradeConfirmationClientsaga/RFQExecutionSaga.javaTradeExecutedEventinstead of calling client directlyservice/TradeConfirmationService.java@Deprecatedrestclient/TradeConfirmationMicroserviceClient.java@Deprecatedclient/ResilientTradeConfirmationClientTest.javaclient/FallbackModeTest.javaMIGRATION_CHECKLIST.mdReview & Testing Checklist for Human
use.confirmation.service=trueand confirm the app starts; PATCH a counterparty credit without the external microservice running — logs should show theFALLBACKwarning instead of a 500 error.TradeExecutedEventListener../mvnw clean test— all 10 tests should pass (7 existing + 3 new).MIGRATION_CHECKLIST.md: Confirm the checklist steps match your team's migration workflow expectations.Recommended test plan:
cd monolith && ./mvnw spring-boot:runcurl -X PATCH http://localhost:8080/counterparties/1 -H 'Content-Type: application/json' -d '{"amount":"1000000","operation":"ADD"}'Notes
use.confirmation.servicetoggle defaults tofalseinapplication.properties, preserving v1 behaviour until the external microservice is deployed and the toggle is flipped.@TransactionalEventListener(AFTER_COMMIT)pattern ensures confirmations are only sent for durably committed trades — no phantom confirmations on rollback.Link to Devin session: https://app.devin.ai/sessions/2f870b8314bc4da08344f8d2c0609f33
Requested by: @achalc
Devin Review