You are tasked with creating an AWS Lambda function integrated with AWS API Gateway in TypeScript that retrieves the current balance for a user. The function should:
- Retrieve the current balance for the specified user from a DynamoDB table.
- Have a default balance of 100 USD.
The function input will be as followed:
{
queryStringParameters: {
userId: '1'
}
}
You are also required to create an AWS Lambda function integrated with AWS API Gateway in TypeScript for processing transactions. The transact function should:
- Handle credits & debits.
- Process the transaction in an idempotent way to prevent duplicate transactions.
- Make sure the user balance can't drop below 0.
- Make sure no race conditions can happen.
The function input will be as followed:
{
headers: {
'Idempotent-Key': '1'
}
body: {
userId: '1',
ammount: '10',
type: 'credit',
}
}
erDiagram
transactions {
id string PK
user_id string
ammount string
type string
}
transactions_aggregate {
user_id string PK
balance string
is_locked boolean
}
transactions_aggregate ||--o{ transactions : "user_id"
Clean architecture with a bit of DDD.
.
├── src
│ ├── handlers // Responsible to be the entrypoint of the application (Lambda handlers)
│ │ ├── create-transaction.ts
│ │ └── [...]
│ ├── models // Responsible to represent the entities and DTO's used internally by the application
│ │ ├── create-transaction-input.ts
│ │ └── [...]
│ ├── repositories // Responsible to connect with the database according to it's entity relationed to a database table
│ │ ├── transactions-aggregate.repository.ts
│ │ └── [...]
│ ├── services // Responsible to abstract operations that can be helpfull for usecases
│ │ ├── context.service.ts
│ │ └── [...]
│ ├── usecases // Responsible to abstract entrypoint main objective
│ │ ├── create-transaction.usecase.ts
│ │ └── [...]
│ ├── utils // Generic code that can be helpfull to the whole application
│ │ ├── obj-to-class-converter.utils.ts
│ │ └── [...]
│ └── validators // Responsible to retain custom input validators
│ ├── is-big-decimal-greater-than-zero.validator.ts
│ └── [...]
└── tests
├── integration // I decided to create a suite for each task number for ease of translate requirements into scenarios
│ ├── task-1.spec.ts
│ └── [...]
└── unit
├── handlers // Each unit test suite should replicate src folder structure to be easier to find
│ └── [...]
└── [...]
graph TD
subgraph API Gateway
GET_BALANCE[task 1: GET /balance?userId=string]
POST_TRANSACTIONS[task 2: POST /transactions]
end
subgraph Lambdas
RetrieveBalanceByUserId[RetrieveBalanceByUserId Lambda]
CreateTransaction[CreateTransaction Lambda]
UnlockTransactionAggregateByUserId[UnlockTransactionAggregateByUserId Lambda]
end
subgraph DynamoDB
TransactionsAggregateTable[transactions_aggregate table]
TransactionsTable[transactions table]
end
subgraph AWS EventBridge
EventBridgeScheduler[EventBridge Scheduler]
end
GET_BALANCE -->|Route| RetrieveBalanceByUserId
POST_TRANSACTIONS -->|Route| CreateTransaction
RetrieveBalanceByUserId -->|Query| TransactionsAggregateTable
RetrieveBalanceByUserId -->|If not exists, run| CreateTransaction
CreateTransaction -->|Try lock| TransactionsAggregateTable
CreateTransaction -->|Put| TransactionsTable
CreateTransaction -->|Update and unlock| TransactionsAggregateTable
CreateTransaction -->|On failure, run| UnlockTransactionAggregateByUserId
UnlockTransactionAggregateByUserId -->|Try unlock| TransactionsAggregateTable
UnlockTransactionAggregateByUserId -->|If not successful, create| EventBridgeScheduler
EventBridgeScheduler -->|After 5 minutes, call| UnlockTransactionAggregateByUserId
- Install Node.js
- Install Typescript
- On root project folder, run following commands:
npm ci && npm test
- Install Terraform
- Use or create an IAM user with following permissions (replace account_id and aws_region placeholders):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"apigateway:DELETE", // If you want to destroy API Gateway resources
"apigateway:GET",
"apigateway:PATCH", // If you want to update API Gateway resources
"apigateway:POST",
"apigateway:PUT", // If you want to update API Gateway resources
"apigateway:UpdateRestApiPolicy", // If you want to update API Gateway resources
"dynamodb:CreateTable",
"dynamodb:DeleteTable", // If you want to destroy DynamoDB tables
"dynamodb:DescribeContinuousBackups",
"dynamodb:DescribeTable",
"dynamodb:DescribeTimeToLive",
"dynamodb:ListTagsOfResource",
"dynamodb:UpdateTable", // If you want to update DynamoDB tables
"iam:CreateRole",
"iam:DeleteRole", // If you want to destroy IAM roles
"iam:DeleteRolePolicy", // If you want to destroy IAM role policies
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfilesForRole",
"iam:ListRolePolicies",
"iam:PassRole",
"iam:PutRolePolicy", // If you want to update IAM role policies
"iam:UpdateAssumeRolePolicy", // If you want to update IAM role policies
"lambda:AddPermission",
"lambda:CreateFunction",
"lambda:DeleteFunction", // If you want to destroy Lambda functions
"lambda:DeleteLayerVersion", // If you want to destroy Lambda layers
"lambda:GetFunction",
"lambda:GetFunctionCodeSigningConfig",
"lambda:GetLayerVersion",
"lambda:GetPolicy",
"lambda:ListTags",
"lambda:ListVersionsByFunction",
"lambda:PublishLayerVersion",
"lambda:RemovePermission", // If you want to destroy Lambda permissions
"lambda:UpdateFunctionCode", // If you want to update Lambda code
"lambda:UpdateFunctionConfiguration", // If you want to update Lambda config
"s3:DeleteObjectVersion", // If you want to destroy S3 object versions
"s3:GetObject",
"s3:GetObjectTagging",
"s3:ListBucketVersions",
"s3:PutObject" // If you want to update Lambda code or layer
],
"Resource": [
"arn:aws:apigateway:${aws_region}::/restapis",
"arn:aws:apigateway:${aws_region}::/restapis/*",
"arn:aws:dynamodb:${aws_region}:${account_id}:table/transactions_aggregate",
"arn:aws:dynamodb:${aws_region}:${account_id}:table/transactions",
"arn:aws:iam::${account_id}:role/CreateTransactionLambdaExecutionRole",
"arn:aws:iam::${account_id}:role/RetrieveBalanceByUserIdLambdaExecutionRole",
"arn:aws:iam::${account_id}:role/UnlockTransactionAggregateByUserIdLambdaExecutionRole",
"arn:aws:lambda:${aws_region}:${account_id}:function:CreateTransactionLambda",
"arn:aws:lambda:${aws_region}:${account_id}:function:RetrieveBalanceByUserIdLambda",
"arn:aws:lambda:${aws_region}:${account_id}:function:UnlockTransactionAggregateByUserIdLambda"
"arn:aws:lambda:${aws_region}:${account_id}:layer:TransactionsChallengeLayer",
"arn:aws:s3:::transactions-challenge-lambda-store",
"arn:aws:s3:::transactions-challenge-lambda-store/*"
]
}
]
}
- Generate access key and secret from created user and inject into .env.test.integration file
- Create S3 bucket with:
- The name transactions-challenge-lambda-store
- Versioning
- Public access blocked
- Run following command:
npm run test:integration
-
Set TEST_SCHEDULER env var to true on Terraform according to each Lambda function:
Obs.: this requires the current AWS user to have permission for action lambda:UpdateFunctionConfiguration on following resources (replace account_id and aws_region placeholders):
- arn:aws:lambda:{aws_region}:{account_id}:function:CreateTransactionLambda
- arn:aws:lambda:{aws_region}:{account_id}:function:RetrieveBalanceByUserIdLambda
- arn:aws:lambda:{aws_region}:{account_id}:function:UnlockTransactionAggregateByUserIdLambda
-
Run following command:
npm run test:integration
-
Verify created schedulers on AWS EventBridge Schedulers
-
After 5 minutes, verify scheduler execution statuses on log group /aws/lambda/UnlockTransactionAggregateByUserIdLambda at AWS CloudWatch Logs