Original Commit: 89f31423ade0df79ceee6bc75a33ab23cda1e818
Final Implementation: Spring Event-driven Architecture
Author: Alif Haikal [email protected]
Date: Wed Jul 30 2025
This document provides a comprehensive analysis of the refactoring implemented to prevent deadlocks in the PostSalesTransactionTrigger class. The solution evolved from an initial RabbitMQ queue-based approach to a final Spring Event-driven architecture that successfully resolves both deadlock issues and transaction persistence problems.
The original implementation of PostSalesTransactionTrigger was performing cost calculations synchronously within the main transaction flow, which could lead to:
- Database deadlocks when multiple transactions attempt to calculate costs simultaneously
- Long-running transactions that hold database locks for extended periods
- Performance bottlenecks during high transaction volumes
- Potential transaction failures due to lock timeouts
The refactoring went through two phases to arrive at the optimal solution:
- Approach: Asynchronous message queues for cost calculations
- Issue Discovered: Transaction persistence problems due to context isolation
- Status: Replaced with Spring Events
- Approach: Spring Events with proper transaction management
- Benefits: Same JVM context, proper transaction boundaries, lazy loading support
- Status: Production-ready solution
- Event Publisher:
PostSalesTransactionTriggerpublishes cost calculation events - Event Handler:
PostTransactionCostCalculationEventHandlerprocesses calculations asynchronously - Event Infrastructure: Spring's built-in event system with transaction support
- Transaction Management:
@TransactionalEventListenerwith proper propagation
Location: src/main/java/my/axaipay/backend/modules/payment/job/PostSalesTransactionTrigger.java
- Removed direct cost calculations from the main transaction flow
- Added Spring Event integration via
ApplicationEventPublisher - Extracted methods for better code organization and maintainability
- Removed RabbitMQ dependencies in favor of Spring Events
@Autowired
private ApplicationEventPublisher eventPublisher;Before: Synchronous cost calculations in main flow
// Old synchronous approach
try {
logger.info("3. CALCULATE ACQUIRER COST");
salesTransactionService.calculateAcquirerCost(salesTransaction);
} catch (ServiceException e) {
e.printStackTrace();
}
try {
logger.info("4. CALCULATE SALES PARTNER COMMISSION");
salesTransactionService.calculateSalesPartnerCommission(salesTransaction);
} catch (ServiceException e) {
logger.error("ERR_SP_001 Failed to calculate sales partner commission");
e.printStackTrace();
}After: Asynchronous event publishing
// New event-driven approach
logger.info("3. PUBLISH COST CALCULATION EVENT");
publishCostCalculationEvent(salesTransaction.getId(), transactionTime);handlePaidTransaction(): Orchestrates all paid transaction processingsendTransactionEmails(): Handles email notifications separatelyupdateTransactionSummary(): Manages summary updatespublishCostCalculationEvent(): Publishes Spring Events for cost calculations
Location: src/main/java/my/axaipay/backend/modules/payment/event/PostTransactionCostCalculationEvent.java
Event class that carries transaction data for cost calculations.
public class PostTransactionCostCalculationEvent extends ApplicationEvent {
private final Long salesTransactionId;
private final Date transactionTime;
// Constructor and getters
}Location: src/main/java/my/axaipay/backend/modules/payment/event/PostTransactionCostCalculationEventHandler.java
Event handler that processes cost calculations asynchronously with proper transaction management.
- Transactional Event Listener: Uses
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - Transaction Safety: Annotated with
@Transactional(propagation = Propagation.REQUIRES_NEW) - Error Handling: Comprehensive exception handling with detailed logging
- Lazy Loading Support: Proper transaction context prevents lazy loading issues
- Input Validation: Validates transaction data before processing
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleCostCalculation(PostTransactionCostCalculationEvent event) {
// Process both acquirer and sales partner cost calculations
}Location: src/main/java/my/axaipay/backend/init/RabbitMQQueueInitializer.java
Changes Made: Removed queue-related configurations that were added in the initial RabbitMQ implementation.
Location: src/main/resources/application.properties
Changes Made: Removed queue configuration properties that were no longer needed with the Spring Event approach.
- Isolated Transactions: Cost calculations run in separate transactions with
REQUIRES_NEWpropagation - Reduced Lock Duration: Main transaction completes faster
- No Cross-Transaction Dependencies: Each calculation is independent
- Non-blocking Operations: Main transaction flow doesn't wait for cost calculations
- Same JVM Processing: No network overhead unlike queue-based approach
- Better Resource Utilization: Database connections efficiently managed within application context
- Transaction Persistence: Spring Events run in proper transaction context ensuring persistence
- Lazy Loading Support:
@TransactionalEventListenerprovides proper Hibernate session context - Error Isolation: Calculation errors are contained and logged with detailed context
- Guaranteed Execution: Events processed within same JVM without external dependencies
- Separation of Concerns: Cost calculations isolated in dedicated event handler
- Cleaner Code Structure: Main trigger focuses on core transaction logic
- Easier Testing: Components can be tested with real database without external message broker
- Simpler Architecture: No external message broker dependency
- Transaction Persistence: Events maintain proper transaction boundaries
- Lazy Loading: Transaction context prevents
LazyInitializationException - Connection Management: Uses application's database connection pool
- Error Handling: Better exception handling with transaction rollback support
sequenceDiagram
participant ST as SalesTransaction
participant PST as PostSalesTransactionTrigger
participant ES as SpringEventSystem
participant EH as EventHandler
participant STS as SalesTransactionService
ST->>PST: Transaction becomes PAID
PST->>PST: handlePaidTransaction()
PST->>ES: Publish CostCalculationEvent
PST->>PST: Continue with emails, summary, etc.
PST->>PST: Commit main transaction
Note over ES,EH: After main transaction commits
ES->>EH: Deliver event (AFTER_COMMIT)
EH->>EH: Start new transaction (REQUIRES_NEW)
EH->>STS: calculateAcquirerCost()
EH->>STS: calculateSalesPartnerCommission()
EH->>EH: Commit cost calculation transaction
- Non-blocking Failures: If event publishing fails, it's logged but doesn't affect main flow
- Transaction Continuity: Main transaction continues even if event publishing fails
- Simple Error Handling: Events are published synchronously within same JVM
- Transaction Rollback: Uses
@Transactional(propagation = Propagation.REQUIRES_NEW) - Comprehensive Exception Handling: Separate handling for
ServiceException,NullPointerException, and general exceptions - Input Validation: Validates transaction existence and required fields before processing
- Detailed Logging: Context-aware error messages with transaction details
- Graceful Degradation: Missing or invalid transactions are handled without crashes
- Lazy Loading Protection: Transaction context prevents Hibernate lazy loading issues
- NullPointerException: Common with missing
MerchantSettingorPaymentMethodSettingdata - Lazy Loading Issues: Resolved with proper transaction context in event handler
- Missing Transaction Data: Early validation prevents processing incomplete transactions
- Service Layer Exceptions: Proper error codes and logging for different failure types
Location: src/test/java/my/axaipay/backend/modules/payment/event/PostTransactionCostCalculationEventIntegrationTest.java
Features:
- Real Database Integration: Tests with actual transaction data, no mocking
- Event Flow Testing: Validates complete event publishing and handling flow
- Bulk Processing: Tests multiple transactions from external file input
- Error Handling: Tests graceful handling of non-existent transactions
- Transaction Persistence: Verifies that cost calculations are actually persisted
- Event Properties: Validates event object creation and data integrity
Test Methods:
testSingleTransactionCostCalculation()- Single transaction end-to-end testtestBulkTransactionCostCalculation()- Bulk processing from file inputtestDirectEventHandlerCall()- Direct method invocation testingtestNonExistentTransaction()- Error handling validationtestEventProperties()- Event object validation
Benefits:
- No external dependencies (RabbitMQ) required for testing
- Real database validation ensures persistence works correctly
- Comprehensive error scenario coverage
- Support for bulk testing with transaction IDs from external files
No additional configuration required - Spring Events work out of the box with Spring Boot.
No additional properties needed for Spring Events.
Ensure the following annotations are properly configured:
- Event Handler:
@Componentfor Spring to detect the event handler - Event Listener:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - Transaction Management:
@Transactional(propagation = Propagation.REQUIRES_NEW)
- Zero-downtime deployment possible with Spring Events (no external dependencies)
- No external setup required (no RabbitMQ queues to configure)
- Immediate availability after deployment as events work within same JVM
- Application logs to monitor event processing success/failure rates
- Transaction metrics to track cost calculation performance
- Database monitoring to ensure calculations are being persisted
- Error rate monitoring through application logging framework
- Simple rollback as Spring Events have no external dependencies
- Database consistency maintained through proper transaction boundaries
- No cleanup required of external message brokers or queues
- Simplified deployment - no external message broker configuration
- Better reliability - no network calls or message broker failures
- Easier troubleshooting - all processing within same application logs
- Lower latency - no serialization/deserialization or network overhead
This refactoring successfully addresses the deadlock issues in the PostSalesTransactionTrigger by:
- Decoupling cost calculations from the main transaction flow
- Implementing asynchronous processing via Spring Events
- Improving system reliability and performance
- Maintaining data consistency through proper transaction handling
- Enhancing code maintainability through better separation of concerns
- Solving transaction persistence issues that occurred with the queue-based approach
The final Spring Event-driven solution provides a robust, maintainable approach to handling post-transaction processing while:
- Preventing database deadlocks from the original synchronous implementation
- Solving transaction persistence issues from the queue-based approach
- Providing proper lazy loading support through transaction context
- Eliminating external dependencies and complexity
- TransactionalEventListener: Ensures events run after main transaction commits
- REQUIRES_NEW Propagation: Creates independent transactions for cost calculations
- Same JVM Context: Maintains proper Hibernate session and connection management
- Comprehensive Error Handling: Gracefully handles various failure scenarios
- Real Database Testing: Integration tests validate actual persistence behavior
The solution successfully balances performance, reliability, and maintainability while eliminating the original deadlock problems.
| File | Changes | Lines Added | Lines Removed |
|---|---|---|---|
PostSalesTransactionTrigger.java |
Refactored to use Spring Events | +95 | -67 |
PostTransactionCostCalculationEvent.java |
New event class | +28 | 0 |
PostTransactionCostCalculationEventHandler.java |
New event handler | +102 | 0 |
RabbitMQQueueInitializer.java |
Removed queue declarations | 0 | -7 |
application.properties |
Removed queue configurations | 0 | -2 |
PostTransactionCostCalculationEventIntegrationTest.java |
New comprehensive test | +233 | 0 |
Removed Files:
PostTransactionCostCalculationQueueConsumer.java(replaced with event handler)PostTransactionCostCalculationQueueConsumerTest.java(replaced with integration test)
Total: +458 lines added, -76 lines removed
- Phase 1 (Queue Implementation): +592 lines, -59 lines
- Phase 2 (Spring Events): +458 lines, -76 lines
- Net Result: Cleaner, more maintainable solution with fewer lines of code