Synthetic Transaction Access with Spring Microservices: Validation and Upgrade Features

In mobile fintech applications or on the web, introducing new features in areas such as loan applications requires careful validation. Traditional testing with real user data, especially personally identifiable information (PII), presents significant challenges. Synthetic transactions offer a solution that enables thorough testing of new functionalities in a secure and controlled environment without compromising sensitive data.

By simulating realistic user interactions within an application, synthetic transactions allow developers and QA teams to identify potential problems in a controlled environment. Synthetic transactions help ensure that every aspect of a financial application functions properly after any major update or new feature rollout. In this article, we look at one approach to using synthetic transactions.

Synthetic transactions for financial applications

Key business entity

At the heart of every financial application is a key entity, whether it is a customer, a user or the loan application itself. This entity is often defined by a unique identifier, which serves as the cornerstone for transactions and operations within the system. The starting point of this entity, when it is first created, presents a strategic opportunity to categorize it as synthetic or real. This categorization is critical because it determines the nature of the interactions the entity will go through.

Marking entities as synthetic or for test purposes from the start allows for a clear demarcation between test and real data within the application ecosystem. Subsequently, all transactions and operations carried out with this entity can be safely recognized as part of synthetic transactions. This approach ensures that the functionality of the application can be thoroughly tested in a realistic environment.

Interception and management of synthetic transactions

A critical component of implementing synthetic transactions lies in intercepting and managing these transactions at the HTTP request level. Using Spring’s HTTP interceptor mechanism, we can parse and process synthetic transactions by examining specific HTTP headers.

The visual below shows the coordination between the synthetic HTTP interceptor and the state manager in managing the execution of HTTP requests:

Synthetic HTTP interceptor and state manager

Figure 1: Synthetic HTTP interceptor and state manager

The SyntheticTransactionInterceptor acts as the primary gatekeeper, ensuring that only transactions identified as synthetic are allowed through testing paths. Below is the implementation:

@Component
public class SyntheticTransactionInterceptor implements HandlerInterceptor 

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    SyntheticTransactionService syntheticTransactionService;

    @Autowired
    SyntheticTransactionStateManager syntheticTransactionStateManager;

    @Override
    public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object object) throws Exception 
        String syntheticTransactionId = request.getHeader("x-synthetic-transaction-uuid");
        if (syntheticTransactionId != null && !syntheticTransactionId.isEmpty())
            if (this.syntheticTransactionService.validateTransactionId(syntheticTransactionId))
                logger.info(String.format("Request initiated for synthetic transaction with transaction id:%s", syntheticTransactionId));
                this.syntheticTransactionStateManager.setSyntheticTransaction(true);
                this.syntheticTransactionStateManager.setTransactionId(syntheticTransactionId);
            
        
        return true;
    

In this implementation, the interceptor searches for a specific HTTP header (x-synthetic-transaction-uuid) carrying a UUID. This UUID not just any identifier but a validated, time-bound key intended for synthetic transactions. The validation process includes checks on the validity of the UUID, its lifetime, and whether it has been used before, thus ensuring a level of security and integrity for the synthetic testing process.

After the synthetic ID validates SyntheticTransactionInterceptorthe SyntheticTransactionStateManager plays a key role in maintaining the synthetic context for the current request. The SyntheticTransactionStateManager is designed with request scope in mind, meaning its lifecycle is tied to a single HTTP request. This scope is essential to preserve the integrity and isolation of synthetic transactions within the wider operational context of the application. By associating the state manager with the request scope, the application ensures that synthetic transaction states do not turn into unrelated operations or requests. Below is an implementation of the synthetic state manager:

@Component
@RequestScope
public class SyntheticTransactionStateManager 
    private String transactionId;
    private boolean syntheticTransaction;

    public String getTransactionId() 
        return transactionId;
    

    public void setTransactionId(String transactionId) 
        this.transactionId = transactionId;
    

    public boolean isSyntheticTransaction() 
        return syntheticTransaction;
    

    public void setSyntheticTransaction(boolean syntheticTransaction) 
        this.syntheticTransaction = syntheticTransaction;
    

When we persist a key entity, whether it’s a customer, user, or credit request—the service layer or the application repository layer consults SyntheticTransactionStateManager to confirm the synthetic nature of the transaction. If the transaction is indeed synthetic, the application continues to maintain not only a synthetic identifier but also an indicator that the entity itself is synthetic. This lays the foundations for a synthetic flow of transactions. This approach ensures that from the moment an entity is marked as synthetic, all associated operations and future APIs, whether they involve processing data or performing business logic, are performed in a controlled manner.

For further API calls initiated from the financial application, once we reach the microservice, we load the application context for that specific request based on the provided token or entity identifier. During context loading, we determine whether a key business entity (eg, loan request, customer/customer) is synthetic. If yes, then we set the state of the manager syntheticTransaction mark for true and also assign synthetic transactionId from the context of the application.

This approach negates the need to pass a synthetic transaction ID header for subsequent calls within the application flow. We only need to send the synthetic transaction ID during the initial API call that creates the key business entity. Since this step involves the use of explicit headers that may not be supported by the financial application, be it a mobile or web platform, we can manually make this first API call with Postman or a similar tool. After that, the application can continue with the rest of the flow in the financial application itself. In addition to managing synthetic transactions within an application, it is also critical to consider how external third-party API calls behave in the context of a synthetic transaction.

External Third Party API Interactions

In financial applications that process key entities with personally identifiable information (PII), we perform validation and fraud checks on user-supplied data, often using external third-party services. These services are essential for tasks such as PII verification and credit bureau report retrieval. However, when dealing with synthetic transactions, we cannot make calls to these third-party services.

The solution involves creating fake responses or using padding for these external services during synthetic transactions. This approach ensures that while synthetic transactions go through the same processing logic as real transactions, they do so without the need to actually submit data to third-party services. Instead, we simulate the responses that these services would provide if they were called with real data. This allows us to thoroughly test the integration points and data handling logic of our application. Below is a code snippet to extract the office report. This call happens as part of the API call where the key entity is created and then we subsequently pull the applicant bureau report:

@Override
@Retry(name = "BUREAU_PULL", fallbackMethod = "getBureauReport_Fallback")
public CreditBureauReport getBureauReport(SoftPullParams softPullParams, ErrorsI error) 
    CreditBureauReport result = null;
    try 
        Date dt = new Date();
        logger.info("UWServiceImpl::getBureauReport method call at :" + dt.toString());
        CreditBureauReportRequest request = this.getCreditBureauReportRequest(softPullParams);
        RestTemplate restTemplate = this.externalApiRestTemplateFactory.getRestTemplate(softPullParams.getUserLoanAccountId(), "BUREAU_PULL",
                softPullParams.getAccessToken(), "BUREAU_PULL", error);
        HttpHeaders headers = this.getHttpHeaders(softPullParams);
        HttpEntity<CreditBureauReportRequest> entity = new HttpEntity<>(request, headers);
        long startTime = System.currentTimeMillis();
        String uwServiceEndPoint = "/transaction";
        String bureauCallUrl = String.format("%s%s", appConfig.getUnderwritingTransactionApiPrefix(), uwServiceEndPoint);
        if (syntheticTransactionStateManager.isSyntheticTransaction()) 
            result = this.syntheticTransactionService.getPayLoad(syntheticTransactionStateManager.getTransactionId(),
                    "BUREAU_PULL", CreditBureauReportResponse.class);
            result.setCustomerId(softPullParams.getUserAccountId());
            result.setLoanAccountId(softPullParams.getUserLoanAccountId());
         else 
            ResponseEntity<CreditBureauReportResponse> responseEntity = restTemplate.exchange(bureauCallUrl, HttpMethod.POST, entity, CreditBureauReportResponse.class);
            result = responseEntity.getBody();
        
        long endTime = System.currentTimeMillis();
        long timeDifference = endTime - startTime;
        logger.info("Time taken for API call BUREAU_PULL/getBureauReport call 1: " + timeDifference);
     catch (HttpClientErrorException exception) 
        logger.error("HttpClientErrorException occurred while calling BUREAU_PULL API, response string: " + exception.getResponseBodyAsString());
        throw exception;
     catch (HttpStatusCodeException exception) 
        logger.error("HttpStatusCodeException occurred while calling BUREAU_PULL API, response string: " + exception.getResponseBodyAsString());
        throw exception;
     catch (Exception ex) 
        logger.error("Error occurred in getBureauReport. Detail error:", ex);
        throw ex;
    
    return result;

The code snippet above is quite elaborate, but we don’t need to go into the details of it. What we need to focus on is the code snippet below:

if (syntheticTransactionStateManager.isSyntheticTransaction()) 
	result = this.syntheticTransactionService.getPayLoad(syntheticTransactionStateManager.getTransactionId(),
			"BUREAU_PULL", CreditBureauReportResponse.class);
	result.setCustomerId(softPullParams.getUserAccountId());
	result.setLoanAccountId(softPullParams.getUserLoanAccountId());
 else 
	ResponseEntity<CreditBureauReportResponse> responseEntity = restTemplate.exchange(bureauCallUrl, HttpMethod.POST, entity, CreditBureauReportResponse.class);
	result = responseEntity.getBody();

Checks if there is a synthetic transaction with SyntheticTransactionStateManager. If it is correct, then the internal service calls instead of the third party SyntheticTransactionService to get the Synthetic Bureau report data.

Synthetic data service

Synthetic data service SyntheticTransactionServiceImpl is a generic utility whose responsibility is to retrieve synthetic data from the data store, parse it, and convert it to the object type passed as part of the parameter. Below is the implementation of the service:

@Service
@Qualifier("syntheticTransactionServiceImpl")
public class SyntheticTransactionServiceImpl implements SyntheticTransactionService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    SyntheticTransactionRepository syntheticTransactionRepository;

    @Override
    public <T> T getPayLoad(String transactionUuid, String extPartnerServiceType, Class<T> responseType) 
        T payload = null;
        try 
            SyntheticTransactionPayload syntheticTransactionPayload = this.syntheticTransactionRepository.getSyntheticTransactionPayload(transactionUuid, extPartnerServiceType);
            if (syntheticTransactionPayload != null && syntheticTransactionPayload.getPayload() != null)
                ObjectMapper objectMapper = new ObjectMapper()
                        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                payload = objectMapper.readValue(syntheticTransactionPayload.getPayload(), responseType);
            
        
        catch (Exception ex)
            logger.error("An error occurred while getting the synthetic transaction payload, detail error:", ex);
        
        return payload;
    

    @Override
    public boolean validateTransactionId(String transactionId) 
        boolean result = false;
        try
            if (transactionId != null && !transactionId.isEmpty()) 
                if (UUID.fromString(transactionId).toString().equalsIgnoreCase(transactionId)) 
                    //Removed other validation checks, this could be financial application specific check.
                
            
        
        catch (Exception ex)
            logger.error("SyntheticTransactionServiceImpl::validateTransactionId - An error occurred while validating the synthetic transaction id, detail error:", ex);
        
        return result;
    

With the generic method getPayLoad(), we provide a high degree of reusability, capable of returning different types of synthetic responses. This reduces the need for multiple, specific mock services for different external interactions.

To store different payloads for different types of third-party external services, we use a generic table structure as below:

CREATE TABLE synthetic_transaction (
  id int NOT NULL AUTO_INCREMENT,
  transaction_uuid varchar(36)
  ext_partner_service varchar(30)
  payload mediumtext
  create_date datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);

  • ext_partner_service: This is the external service identifier for which we are extracting the payload from the table. In this example above for a bureau report, it would be BUREAU_PULL.

Conclusion

In our research on synthetic transactions within fintech applications, we highlighted their role in improving the reliability and integrity of fintech solutions. By leveraging synthetic transactions, we simulate realistic user interactions while avoiding the risks associated with handling real Personal Information (PII). This approach allows our developers and QA teams to rigorously test new functionality and updates in a secure, controlled environment.

Moreover, our strategy of integrating synthetic transactions through mechanisms such as HTTP interceptors and state managers presents a versatile approach applicable to a wide range of applications. This method not only simplifies the embedding of synthetic transactions, but also significantly increases reusability, reducing the need to design unique workflows for each third-party service interaction.

This approach significantly increases the reliability and security of financial application solutions, ensuring that new features can be implemented with confidence.

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *