Building A Payment Processing Workflow For A Fintech App Using AWS Step Functions and AWS SAM
In today's digital age, financial technology (Fintech) applications require robust, scalable, and secure infrastructures to handle complex transactions and ensure smooth payment processing. AWS Step Functions and AWS Serverless Application Model (SAM) provide powerful tools to orchestrate and manage these workflows seamlessly. This hands-on lab will guide you through building a payment processing workflow for a Fintech app using AWS Step Functions and AWS SAM. By the end of this tutorial, you'll have a comprehensive understanding of how to implement a serverless payment processing system that integrates various AWS services to create an efficient and reliable solution.
It is important to note that this is a proof-of-concept solution. A more established method of building payment processing systems on AWS can be found here.
In this lab, we will create a payment processing workflow that is both scalable and maintainable, leveraging the power of AWS Step Functions and AWS SAM. Our workflow will involve multiple AWS services, including API Gateway, Lambda, DynamoDB, and SNS, to ensure a seamless transaction processing system. You might want to read this article on step functions first, before proceeding with this lab.
1. API Gateway:
2. AWS Step Functions:
3. AWS Lambda Functions:
To learn about AWS lambda's advanced features, read this comprehensive guide on AWS Lambda.
4. Amazon DynamoDB:
5. Amazon SNS:
AWS Step Functions play a crucial role in orchestrating the entire workflow. Here's a detailed breakdown of how Step Functions manage the process:
1. Initial Trigger:
2. ValidatePayment State:
`ValidatePayment`
Lambda function.3. ProcessPayment State:
`ProcessPayment`
Lambda function handles the actual payment processing.4. UpdateOrderStatus State:
`UpdateOrderStatus`
Lambda function updates the DynamoDB table with the order status.5. SendNotification State:
`SendNotification`
Lambda function publishes a message to the SNS topic to notify users of the payment status.6. Error Handling and Retries:
We will implement this workflow in several steps:
1. Setting Up AWS SAM Project:
2. Creating API Gateway and Lambda Functions:
3. Configuring DynamoDB and SNS:
4. Defining the Step Functions State Machine:
5. Deploying the Application:
6. Testing the Workflow:
You have to create the directory structure wherein your sam template and lambda code will exist.
mkdir -p payment-processing/src
Our lambda source code will reside in the src
folder.
The validate_payment lambda function is responsible for validating the credit card details.
src/validate_payment.py:
import boto3
import json
import os
def lambda_handler(event, context):
print("received event:", json.dumps(event))
# Validate the payment details from the event
payment_details = event['payment_details']
print("Payment Details:", payment_details)
if not validate_payment(payment_details):
raise Exception("Invalid Payment Details")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Payment validation successful. Workflow initiated.'
})
}
def validate_payment(details):
# Add your payment validation logic here
if not details.get('card_number') or not details.get('expiry_date') or not details.get('cvv'):
return False
return True
The process_payment
lambda function is responsible for processing the payment upon validation. Notice that in the source code below, we inroduced logic for random validation error. This will cause our state machine to trigger the handle_payment_error
function anytime this transient error occurs. This will occur o.3 (30% or 3/10) of the time.
src/process_payment.py:
import json
import random
def lambda_handler(event, context):
try:
# Simulate payment processing logic
payment_info = event.get('payment_info')
print(f"Processing payment: {payment_info}")
# Simulate a transient error with 30% probability
if random.random() < 0.3:
raise Exception("TransientError")
return {
'statusCode': 200,
'body': json.dumps("Payment processed successfully")
}
except Exception as e:
raise e
The update_payment_status function is responsible for persisting the status of the payment in a DynamoDB table. This is neccesary becase you want to persist the status of each transaction you process - both for compliance and quality assurance purposes.
src/update_payment_status.py:
import json
import boto3
import uuid
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Orders')
def lambda_handler(event, context):
try:
# Generate a random payment ID using uuid
payment_id = str(uuid.uuid4())
# Retrieve status from the event
status = event.get('status', 'Processed')
# Update the order status in DynamoDB with the generated payment_id
response = table.update_item(
Key={'PaymentId': payment_id}, # Assuming 'PaymentId' is your primary key
UpdateExpression='SET PaymentStatus = :val',
ExpressionAttributeValues={':val': status}
)
return {
'statusCode': 200,
'body': json.dumps(f"Payment ID {payment_id} status updated to {status} successfully")
}
except ClientError as e:
print(e.response['Error']['Message'])
raise e
except Exception as e:
print(str(e))
raise e
The send_notification.py will update the customer with the status of their payment. We are using SNS here, but ideally you should use SES or connect to an external provider in a production scenario.
N.B: YOU MUST UPDATE YOUR ARN IN THE CODE BELOW. All you need here is to replace the AccountID in the code with your actual AWS AccountID.
src/send_notification.py:
import json
import boto3
from botocore.exceptions import ClientError
sns_client = boto3.client('sns')
topic_arn = 'arn:aws:sns:us-east-1:123456789012:OrderNotifications' #UPDATE YOUR ACCOUNTID
def lambda_handler(event, context):
try:
# Retrieve order ID and status from the event
order_id = event.get('order_id')
status = event.get('status', 'Processed')
# Create the message to send
message = f"Order {order_id} status has been updated to {status}."
# Publish the message to the SNS topic
response = sns_client.publish(
TopicArn=topic_arn,
Message=message,
Subject="Order Status Update"
)
return {
'statusCode': 200,
'body': json.dumps(f"Notification sent for order {order_id}")
}
except ClientError as e:
print(e.response['Error']['Message'])
raise e
except Exception as e:
print(str(e))
raise e
The handle_payment_error function will handle any possible errors arising from the payment processing. It is triggered by our State machine once an error is noticed in the ProcessPayment state.
src/handle_payment_error.py:
import json
def lambda_handler(event, context):
try:
# Log the error and perform error handling
print("Handling payment error for event:", event)
# Simulate error handling logic
return {
'statusCode': 200,
'body': json.dumps("Payment error handled successfully")
}
except Exception as e:
print(str(e))
raise e
We will be defining our state machine using the Amazon States Language (ASL). It is a JSON-based, structured language used to define a step functions state machine. Take note of the code below. We will be integrating it directly into our SAM template.
{
"Comment": "Payment processing workflow triggered via API Gateway",
"StartAt": "ValidatePayment",
"States": {
"ValidatePayment": {
"Type": "Task",
"Resource": "${ValidatePaymentFunction.Arn}",
"Next": "ProcessPayment"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "${ProcessPaymentFunction.Arn}",
"Retry": [
{
"ErrorEquals": ["TransientError"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "HandlePaymentError"
}
],
"Next": "UpdatePaymentStatus"
},
"UpdatePaymentStatus": {
"Type": "Task",
"Resource": "${UpdatePaymentStatusFunction.Arn}",
"Next": "SendNotification"
},
"SendNotification": {
"Type": "Task",
"Resource": "${SendNotificationFunction.Arn}",
"End": true
},
"HandlePaymentError": {
"Type": "Task",
"Resource": "${HandlePaymentErrorFunction.Arn}",
"Next": "UpdatePaymentStatus"
}
}
}
The SAM template is an open-source framework that you can use to define and manage your serverless application infrastructure code.
template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
ValidatePaymentFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: validate_payment.lambda_handler
Runtime: python3.12
CodeUri: src/
Policies:
- AWSLambdaBasicExecutionRole
ProcessPaymentFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: process_payment.lambda_handler
Runtime: python3.12
CodeUri: src/
Policies: AWSLambdaBasicExecutionRole
UpdatePaymentStatusFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: update_payment_status.lambda_handler
Runtime: python3.12
CodeUri: src/
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'dynamodb:PutItem'
- 'dynamodb:UpdateItem'
Resource: !GetAtt OrdersTable.Arn
SendNotificationFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: send_notification.lambda_handler
Runtime: python3.12
CodeUri: src/
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'sns:List*'
- 'sns:Publish'
Resource: !Ref OrderNotificationsTopic
HandlePaymentErrorFunction:
Type: 'AWS::Serverless::Function'
Properties:
Handler: handle_payment_error.lambda_handler
Runtime: python3.12
CodeUri: src/
Policies:
- AWSLambdaBasicExecutionRole
OrdersTable:
Type: 'AWS::DynamoDB::Table'
Properties:
TableName: Orders
AttributeDefinitions:
- AttributeName: PaymentId
AttributeType: S
KeySchema:
- AttributeName: PaymentId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
OrderNotificationsTopic:
Type: 'AWS::SNS::Topic'
Properties:
TopicName: OrderNotifications
PaymentProcessingStateMachine:
Type: 'AWS::StepFunctions::StateMachine'
Properties:
DefinitionString: !Sub |
{
"Comment": "Payment processing workflow triggered via API Gateway",
"StartAt": "ValidatePayment",
"States": {
"ValidatePayment": {
"Type": "Task",
"Resource": "${ValidatePaymentFunction.Arn}",
"Next": "ProcessPayment"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "${ProcessPaymentFunction.Arn}",
"Retry": [
{
"ErrorEquals": ["TransientError"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "HandlePaymentError"
}
],
"Next": "UpdatePaymentStatus"
},
"UpdatePaymentStatus": {
"Type": "Task",
"Resource": "${UpdatePaymentStatusFunction.Arn}",
"Next": "SendNotification"
},
"SendNotification": {
"Type": "Task",
"Resource": "${SendNotificationFunction.Arn}",
"End": true
},
"HandlePaymentError": {
"Type": "Task",
"Resource": "${HandlePaymentErrorFunction.Arn}",
"Next": "UpdatePaymentStatus"
}
}
}
RoleArn: !GetAtt StepFunctionsExecutionRole.Arn
StepFunctionsExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'states.amazonaws.com'
Action: 'sts:AssumeRole'
Policies:
- PolicyName: StepFunctionsExecutionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'lambda:InvokeFunction'
Resource:
- !GetAtt ValidatePaymentFunction.Arn
- !GetAtt ProcessPaymentFunction.Arn
- !GetAtt UpdatePaymentStatusFunction.Arn
- !GetAtt SendNotificationFunction.Arn
- !GetAtt HandlePaymentErrorFunction.Arn
PaymentAPI:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
DefinitionBody:
swagger: '2.0'
info:
title: 'PaymentAPI'
version: '1.0'
paths:
/startPayment:
post:
summary: 'Initiate Payment Processing'
description: 'Initiate the payment processing workflow in Step Functions.'
consumes:
- 'application/json'
produces:
- 'application/json'
parameters:
- in: 'body'
name: 'body'
required: true
schema:
type: 'object'
properties:
payment_details:
type: 'object'
properties:
card_number:
type: 'string'
description: 'The card number for payment.'
expiry_date:
type: 'string'
description: 'The expiry date of the card (MM/YY format).'
cvv:
type: 'string'
description: 'The CVV code of the card.'
amount:
type: 'number'
description: 'The amount to be paid.'
responses:
'200':
description: 'Successful response'
schema:
type: 'object'
properties:
statusCode:
type: 'integer'
body:
type: 'string'
x-amazon-apigateway-integration:
type: 'AWS'
httpMethod: 'POST'
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:states:action/StartExecution'
credentials: !GetAtt PaymentAPIGWRole.Arn
requestTemplates:
application/json: !Sub |
{
"stateMachineArn": "${PaymentProcessingStateMachine}",
"input": "$util.escapeJavaScript($input.json('$'))"
}
responses:
default:
statusCode: '200'
responseTemplates:
application/json: |
{
"statusCode": 200,
"body": $input.json('$.output')
}
PaymentAPIGWRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'apigateway.amazonaws.com'
Action: 'sts:AssumeRole'
Policies:
- PolicyName: ApiGatewayInvokeStepFunctionsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: 'states:StartExecution'
Resource: !Ref PaymentProcessingStateMachine
Outputs:
ApiUrl:
Description: "API Gateway endpoint URL for Prod stage"
Value: !Sub "https://${PaymentAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/startPayment"
1. Build the App:
sam build
2. Deploy the App:
sam deploy --guided
As you can see, our state machine was successfully created.
Now, the hard work has been done. What remains for us is to tets our s3 bucket and make sure that this setup is working as smoothly as intended.
There are several ways of tesing this; using curl, using postman or creating a frontend through which we can query our backend api.
curl -X POST https://<your-api-id>.execute-api.<region>.amazonaws.com/Prod/startPayment -d '{
"payment_details": {
"card_number": "4111111111111111",
"expiry_date": "12/24",
"cvv": "123",
"amount": 100.0
}
}' -H "Content-Type: application/json"
My 'Orders' table also clearly shows that the payment information is beign persisted in the table.
Here you can also see that the workflow executed successfully:
If I were to enter an invalid card detail such as a detail without "cvv" or "card number", the workflow fails.
If you made it this far, CONGRATULATIONS!!! You Rock!!!
By following this tutorial, you have successfully built a comprehensive payment processing workflow for a Fintech app using AWS Step Functions and AWS SAM. This serverless architecture not only ensures scalability and reliability but also simplifies maintenance and reduces operational overhead. You have integrated multiple AWS services to create an efficient system that handles payment validation, processing, status updates, and notifications. This hands-on experience equips you with the skills to implement similar workflows for other complex applications, leveraging the full potential of AWS's serverless capabilities. As you continue to explore and innovate, you'll find even more ways to optimize and enhance your Cloud solutions as the quintessential Practical Cloud Engineer.
Here's a simillar project using step functions to implement a user onboarding orkflow.
Click on this GitHub repo to get all the source code used in this project.
Happy Clouding!!!
Did you like this post?
If you did, please buy me coffee 😊
Great article! I really appreciated the detailed explanation of using AWS Step Functions and AWS SAM for building payment processing workflows.
One question I have is about error handling in Step Functions: what are some best practices for managing errors during workflow execution, especially in the context of payment processing?
Any insights or additional resources would be helpful!
Thank you for your kind words and for your insightful question! Error handling is indeed crucial in payment processing workflows. A few best practices include:
For more details, I recommend checking out the AWS Step Functions documentation.