web statistics
top of page
surface-of-the-bill-note-filled-with-flow-of--dotted-data-stream-flowing--on-the-black-bac

Transition from your monolith to Cloud-Native solution with FraudAway

Updated: Nov 17, 2023



In this post, we will explore a practical use case that involves the migration of existing code from a legacy system to the cloud. Our discussion will specifically cover the implementation of a solution partially based on the AWS stack. Furthermore, we will compare and contrast the "lift and shift" approach with the cloud-native solutions. Finally, we will provide a comprehensive overview of a payment solution, integrating with FraudAway technology, taking the holistic approach in mind.

Fraud solution based on the "classical" monolith approach


So, let's start with some simple code first (using nodejs).

First, we can imagine creating a rules configuration file that holds different rules and thresholds, in this way:

// config.js
const moment = require('moment');

module.exports = {
  fraudDetection: {
    rules: [
      // Example 1: High Amount
      transaction => transaction.amount > this.thresholds.highAmount,

      // Example 2: Frequent Transactions
      transaction => transaction.userTransactions.length > this.thresholds.transactionFrequency,

      // Example 3: High-Risk Country
      transaction => this.thresholds.highRiskCountries.includes(transaction.userCountry),

      // Example 4: Unusual Temporal Pattern (More than 3 transactions within a 10-minute window)
      transaction => {
        const currentTransactionTime = moment(transaction.timestamp);
        const rapidTransactionWindow = this.thresholds.rapidTransactionWindow; // in minutes
        const rapidTransactions = transaction.userTransactions.filter(
          prevTransaction => moment(prevTransaction.timestamp).isBetween(currentTransactionTime.clone().subtract(rapidTransactionWindow, 'minutes'), currentTransactionTime)
        );
        return rapidTransactions.length <= this.thresholds.maxRapidTransactions;
      },
    ],
    thresholds: {
      highAmount: 1000,
      transactionFrequency: 10,
      highRiskCountries: ['Nigeria', 'North Korea'],
      rapidTransactionWindow: 10,
      maxRapidTransactions: 3,
    },
  },
};

This will be passed through the RuleBasedFraudDetector, a class that takes the rules configuration as an input and checks if the transactions are fraudulent (this code also runs the example, we did it deliberately to avoid cluttering the code too much):


// fraudDetector.js
const moment = require('moment');
const { rules } = require('./config');

class RuleBasedFraudDetector {
  constructor(rules) {
    this.rules = rules;
  }

  detectFraud(transaction) {
    // Apply all rules and check for any violations
    const violations = this.rules.filter(rule => !rule(transaction));

    // Combine results to make a fraud decision
    if (violations.length > 0) {
      return "Fraud Detected";
    } else {
      return "No Fraud Detected";
    }
  }
}

// Example usage:
const transactions = [
  { amount: 1200, userCountry: 'United States', timestamp: '2023-11-15T12:30:00' },
  { amount: 500, userCountry: 'Nigeria', timestamp: '2023-11-15T12:35:00' },
  // Add more transactions as needed ...
];

// Add a list of previous transactions to each transaction - for demo purposes
transactions.forEach(transaction => {
  transaction.userTransactions = generatePreviousTransactions(5)
});

// Create an instance of the RuleBasedFraudDetector with rules from the config file
const fraudDetector = new RuleBasedFraudDetector(rules);

// Iterate through transactions and detect fraud
transactions.forEach(transaction => {
  const result = fraudDetector.detectFraud(transaction);
  console.log(`Transaction Amount: ${transaction.amount}, Result: ${result}`);
});

You can package this code and run it as part of the pipeline, or you can try to turn it into a REST service, where you pass one transaction at the time. So far so good! (exercise caution to avoid finding yourself in a situation similar to that of this company "Scripting: The Path to Chaos").


Now that we have made our first app, how we can make this solution more scalable and easier to manage in the future? How can we communicate to the rest of the team what these rules do, why they have fired, and where results will be stored? Database? Or send results to the message bus etc... That is where the cloud comes in, we want the ease of extensions and integrations with diverse systems.


Now that we have this covered, using the example above, I'll outline a solution that includes AWS Lambda and AWS Step Functions for orchestration. Additionally, you can simply extend this example with Simple Notification Service (SNS) or Kinesis for communication, but that is left from the example below.


Finally, I will take the FraudAway engine as an orchestrator and describe the benefits of our solution compared to using AWS Step Functions. Before that, let's clarify some other things first:


The difference between "lift and shift" and cloud native paradigm


"Lift and shift" and "cloud-native" are terms often used in the context of cloud computing, but they refer to different concepts.

"Lift and shift" generally refers to the process of migrating existing applications, services, or infrastructure to a cloud environment, mostly a straightforward move of existing applications to the cloud without significant modification. That means that I could have turned the previous monolith application into the REST service, bring one or more machines up and plug it back to the existing pipeline.


The cloud-native approach on the other hand, refers to a design and development approach that utilizes cloud computing principles and services to build and run applications. Cloud-native applications are typically developed with the cloud in mind and take full advantage of cloud services, scalability, and containerization. They are built from the ground up to leverage cloud services and are designed to be agile, scalable, and resilient. Cloud-native applications are often developed using a microservices architecture, where the application is broken down into smaller, independently deployable services.


So, let's go back to our previous code example and try to turn it into the cloud native app. For starters, let's try to make it using the AWS stack. First thing we need to do is to create all functions in the cloud, using AWS lambdas. For simplicity I will only show one of them here:


// Lambda Function: lambdaHighAmount
exports.handler = async (event) => {
  const transaction = event.transaction;
  const threshold = event.threshold;

  const isHighAmount = transaction.amount > threshold;

  return {
    isHighAmount,
    message: isHighAmount ? "High amount detected" : "No fraud detected",
  };
};

Here is a small troubling part. You need to pass the threshold from the payment event. Otherwise you need to fetch it from the configuration - DynamoDB can be an on option or put it as the environment variable or just hard code it within a function. For the moment, let's just keep this in mind, and let's see later what we can do about it.


Now let's try to make use of AWS Step Functions as we want to capture the output of each Lambda function and check for the fraud indicators in the end:


{
  "Comment": "Fraud Detection Workflow",
  "StartAt": "HighAmount",
  "States": {
    "HighAmount": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:lambdaHighAmount",
      "ResultPath": "$.highAmountResult",
      "Next": "CheckHighAmountFraud"
    },
    "CheckHighAmountFraud": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.highAmountResult.isHighAmount",
          "BooleanEquals": true,
          "Next": "FraudDetected"
        }
      ],
      "Default": "TransactionFrequency"
    },
    "TransactionFrequency": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:lambdaTransactionFrequency",
      "ResultPath": "$.transactionFrequencyResult",
      "Next": "CheckTransactionFrequencyFraud"
    },
    "CheckTransactionFrequencyFraud": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.transactionFrequencyResult.isTransactionFrequency",
          "BooleanEquals": true,
          "Next": "FraudDetected"
        }
      ],
      "Default": "HighRiskCountry"
    },
    "HighRiskCountry": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:lambdaHighRiskCountry",
      "ResultPath": "$.highRiskCountryResult",
      "Next": "CheckHighRiskCountryFraud"
    },
    "CheckHighRiskCountryFraud": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.highRiskCountryResult.isHighRiskCountry",
          "BooleanEquals": true,
          "Next": "FraudDetected"
        }
      ],
      "Default": "UnusualTemporalPattern"
    },
    "UnusualTemporalPattern": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:lambdaUnusualTemporalPattern",
      "ResultPath": "$.unusualTemporalPatternResult",
      "Next": "CheckUnusualTemporalPatternFraud"
    },
    "CheckUnusualTemporalPatternFraud": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.unusualTemporalPatternResult.isUnusualTemporalPattern",
          "BooleanEquals": true,
          "Next": "FraudDetected"
        }
      ],
      "Default": "NoFraudDetected"
    },
    "FraudDetected": {
      "Type": "Fail",
      "Error": "FraudDetectedError",
      "Cause": "Fraud Detected!"
    },
    "NoFraudDetected": {
      "Type": "Succeed"
    }
  }
}

In this example each Lambda function result is stored in a specific field in the state machine's output, like a context if you like: ($.highAmountResult, $.transactionFrequencyResult, etc.). After each Lambda function, there's a state (CheckHighAmountFraud, CheckTransactionFrequencyFraud, etc.) that checks the fraud indicator in the Lambda function result. If a fraud condition is detected, the state machine transitions to the FraudDetected state, which fails the execution with an error indicating "fraud".


Finally, let's make a REST API for Transactions on top:

We will need to set up an API Gateway endpoint that triggers the lambdaProcessTransaction function. And voila, you've made it cloud native!


// Lambda Function: lambdaProcessTransaction
exports.handler = async (event) => {
  const transaction = event.transaction;
  // Trigger Step Functions with the transaction
  // Optionally, return a response to the API caller
  return { message: 'Transaction processing initiated' };
};


Let's make it all the way, with FraudAway


Before we do that, let's see what the issues are with the previous solution:


My first problem is that all these thresholds and rules are spread all over the place. When you initiate the Step Functions execution, you can pass the necessary parameters as input, including any thresholds or configuration values specific to the transaction being processed, but I would love the rule itself and all its configurations to be present in one place.


Also, let us not forget that fraud detection solutions typically cover both routing and rules execution, as described in this article. Making that the responsibility of yet another API Gateway, exposed over lambda, clutters this solution even further.


Another topic is the speed: solutions based on AWS lambda and AWS Step functions are not able to process payments faster than lambdas (I know it sounds obvious), which is at least hundreds of ms per transaction if not seconds (when many functions are chained). AWS Step functions lacks the speed of stream processing engines, which FraudAway can achieve.


Another issue is a use of Finite State machine, AWS Step functions like many RPA tools, are not meant for complex decision-making scenarios (more about that later).


Solution based on FraudAway


So here let's check the complete solution, with 4 sub-rules configured for AML use case. In case that any of the sub-rules find a risk of fraud, a risk event is sent over the message bus towards the Case Management tool. We also make use of a simple OR gate to indicate that if any of sub-rules finds an issue, we can annotate this transaction with a sub-rule that fired the problem and flag it down the processing pipeline. It is that simple!


AML solution with 4 sub-rules

Each sub-rule is tested and can be embedded in various scenarios. They also hold all thresholds and default values (more about it here). I could have also made use of the template tags to create library or rules that are fetched for a given use case (as the part of the routing process), which I will cover in some future blog post.


One interesting aspect of this sub-rule below is that it is not calling any external Lambdas. Of course, unless it is really necessary - for instance you have a complicated algorithm to implement - hence you need to code it in the function, or you need to call external API's, databases or ML models, as described in this article.


One sub-rule that checks for fund transfers

As mentioned, all functions here are native functions, like the example of the one in the rule above:

Native function emdeded in the rule

Finally, you can plug these rules directly into the data stream, as described FraudAway integration with AWS Kinesis , where you can as well apply the routing as well.


Putting it all together


As described in the article "Strengthening the Financial Fortress: The Four Pillars of Anti-Money Laundering Solutions", robust fraud solution encompasses more than just a rules execution. The four pillars – Data Processing, Rules Execution, Case Management, and Analytics Reports – collectively create a comprehensive and robust AML strategy. These pillars empower financial institutions to detect, prevent, and report money laundering activities effectively, thereby protecting the integrity of the financial system and upholding their regulatory responsibilities. In an ever-evolving landscape of financial crime, these pillars form the bedrock of defence against money laundering.

Holistic approach to payment solutions


In Conclusion



SOS Piet is my favourite Belgian television program featuring Piet Huysentruyt, a chef and culinary personality in Belgium. The show focuses on cooking and culinary tips. The term "SOS" is not accidental, suggesting that the program is there to help and rescue people in need of cooking assistance. So, let me try to close this very long blog post in SOS Piet style, that is to say, there are only three things to remember:

  1. Monoliths are great unless you need to scale and manage your application according to the scale, speed and dynamic nature of attacks - which is what fraud prevention is all about.

  2. "Lift and shift" is a cloud move too, but beyond blaming someone for the reason your application is not working, it's not much of a help.

  3. Cloud native approach is the way to go, yet it's crucial to navigate the complexities (and cost) that arise with the management of serverless architecture and microservices. This is particularly true for payment solutions, where a comprehensive understanding and clear management of decisions and transaction processes are paramount.

63 views0 comments

Recent Posts

See All
bottom of page