DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • IntelliJ and Java Spring Microservices: Productivity Tips With GitHub Copilot
  • Ensuring API Resilience in Spring Microservices Using Retry and Fallback Mechanisms
  • Security Challenges for Microservice Applications in Multi-Cloud Environments
  • WireMock: The Ridiculously Easy Way (For Spring Microservices)

Trending

  • How To Get Started With New Pattern Matching in Java 21
  • How to Submit a Post to DZone
  • Service Mesh Unleashed: A Riveting Dive Into the Istio Framework
  • API Appliance for Extreme Agility and Simplicity
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. An Approach To Synthetic Transactions With Spring Microservices: Validating Features and Upgrades

An Approach To Synthetic Transactions With Spring Microservices: Validating Features and Upgrades

Learn how synthetic transactions in fintech help in assuring quality and confidence, validating business functionality post major updates or new features.

By 
Amol Gote user avatar
Amol Gote
DZone Core CORE ·
Feb. 27, 24 · Tutorial
Like (6)
Save
Tweet
Share
6.2K Views

Join the DZone community and get the full member experience.

Join For Free

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

By simulating realistic user interactions within the application, synthetic transactions enable developers and QA teams to identify potential issues in a controlled environment. Synthetic transactions help in ensuring that every aspect of a financial application functions correctly after any major updates or new features are rolled out. In this article, we delve into one of the approaches for using synthetic transactions.

Synthetic Transactions for Financial Applications

Key Business Entity

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

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

Intercepting and Managing Synthetic Transactions

A critical component of implementing synthetic transactions lies in the interception and management of these transactions at the HTTP request level. Utilizing Spring's HTTP Interceptor mechanism, we can discern and process synthetic transactions by examining specific HTTP headers.

The below visual outlines the coordination between a synthetic HTTP interceptor and a state manager in managing the execution of an HTTP request:

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 the testing pathways. Below is the implementation:

Java
 
@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 looks for a specific HTTP header (x-synthetic-transaction-uuid) carrying a UUID. This UUID is not just any identifier but a validated, time-limited key designated for synthetic transactions. The validation process includes checks on the UUID's validity, its lifespan, and whether it has been previously used, ensuring a level of security and integrity for the synthetic testing process.

 After a synthetic ID is validated by the SyntheticTransactionInterceptor, the SyntheticTransactionStateManager plays a pivotal 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 the individual HTTP request. This scoping is essential for preserving the integrity and isolation of synthetic transactions within the application's broader operational context. By tying the state manager to the request scope, the application ensures that synthetic transaction states do not bleed over into unrelated operations or requests. Below is the implementation of the synthetic state manager:

Java
 
@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 the key entity, be it a customer, user, or loan application—the application's service layer or repository layer consults the SyntheticTransactionStateManager to confirm the transaction's synthetic nature. If the transaction is indeed synthetic, the application proceeds to persist not only the synthetic identifier but also an indicator that the entity itself is synthetic. This sets the foundations for the synthetic transaction flow. This approach ensures that from the moment an entity is marked as synthetic, all related operations and future APIs, whether they involve data processing or business logic execution, are conducted in a controlled manner. 

For further API calls initiated from the financial application, upon reaching the microservice, we load the application context for that specific request based on the token or entity identifier provided. During the context loading, we ascertain whether the key business entity (e.g., loan application, user/customer) is synthetic. If affirmative, we then set the state manager's syntheticTransaction flag to true and also assign the synthetic transactionId from the application context. 

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

External Third-Party API Interactions

In financial applications handling key entities with personally identifiable information (PII), we conduct validations and fraud checks on user-provided data, often leveraging external third-party services. These services are crucial for tasks such as PII validation and credit bureau report retrieval. However, when dealing with synthetic transactions, we cannot make calls to these third-party services.

The solution involves creating mock responses or utilizing stubs for these external services during synthetic transactions. This approach ensures that while synthetic transactions undergo the same processing logic as real transactions, they do so without the need for actual data submission 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 the code snippet for pulling the bureau report. This call happens as part of the API call where the key entity is been created, and then subsequently we pull the applicant's bureau report:

Java
 
@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 get into the details of that. What we need to focus on is the code snippet below:

Java
 
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();
}


It checks for the synthetic transaction with the SyntheticTransactionStateManager. If true, then instead of going to a third party, it calls the internal service SyntheticTransactionService to get the Synthetic Bureau report data.

Synthetic Data Service

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

Java
 
@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 various types of synthetic responses. This reduces the need for multiple, specific mock services for different external interactions.

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

MySQL
 
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 an external service identifier for which we pull the payload from the table. In this above example for bureau report, it would be BUREAU_PULL. 

Conclusion

In our exploration of synthetic transactions within fintech applications, we've highlighted their role in enhancing the reliability, and integrity of fintech solutions. By leveraging synthetic transactions, we simulate realistic user interactions while circumventing the risks tied to handling real personally identifiable information (PII). This approach enables our developers and QA teams to rigorously test new functionalities and updates in a secure, controlled environment. 

Moreover, our strategy in integrating synthetic transactions through mechanisms such as HTTP interceptors and state managers showcases a versatile approach applicable across a wide array of applications. This method not only simplifies the incorporation of synthetic transactions but also significantly boosts reusability, alleviating the need to devise unique workflows for each third-party service interaction.

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

Information security Spring Framework microservice API testing

Opinions expressed by DZone contributors are their own.

Related

  • IntelliJ and Java Spring Microservices: Productivity Tips With GitHub Copilot
  • Ensuring API Resilience in Spring Microservices Using Retry and Fallback Mechanisms
  • Security Challenges for Microservice Applications in Multi-Cloud Environments
  • WireMock: The Ridiculously Easy Way (For Spring Microservices)

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: