Building a Serverless IoT Backend on AWS: Lambda, DynamoDB & Payment Integration
The Challenge: Building Serverless Infrastructure for Embedded Systems
I recently worked on a project for Sai Vending Pvt Ltd that required building a complete serverless backend infrastructure on AWS. The goal was to create a cloud-native system that could handle the entire lifecycle of transactions for an STM32 board - from processing requests with customizable options to integrating payment gateways and managing state across the entire flow.
The system needed to handle this architecture: An STM32 board sends transaction requests with multiple options and customizations. The backend calculates pricing, generates payment QR codes via Razorpay, handles webhook callbacks for payment confirmation, manages the device state, and tracks the complete transaction lifecycle. This presented an excellent opportunity to explore AWS serverless architecture at scale.
Understanding the Flow
The entire system architecture follows this workflow:
- STM32 board initiates a transaction with customizable parameters
- AWS Lambda calculates pricing based on selected options
- Razorpay payment gateway generates a UPI QR code
- API Gateway returns QR code data to the STM32 board
- User completes payment via UPI
- Razorpay webhook notifies Lambda of payment confirmation
- DynamoDB state is updated and STM32 board is authorized to proceed
- Transaction completion is logged with full audit trail in DynamoDB
Each step leverages AWS services for reliability, scalability, and security. This is a perfect use case for serverless architecture since traffic is unpredictable and traditional always-on servers would be wasteful.
AWS Serverless Architecture: The Why
This project was an ideal opportunity to fully leverage AWS serverless services. Here's the architectural breakdown:
AWS Lambda: Compute Without Servers
Lambda functions execute code in response to events without provisioning or managing servers:
- Auto-scaling: Handles 1 request or 1,000 requests seamlessly
- Pay-per-invocation: Only pay for actual compute time (100ms granularity)
- Zero server management: No patching, no capacity planning
- Built-in fault tolerance: Automatic retries and error handling
- 128MB memory allocation keeps cold starts under 200ms
Each Lambda function is single-purpose, following microservices patterns for better isolation and debugging.
Amazon API Gateway: RESTful HTTP API
API Gateway sits in front of Lambda functions, providing:
- HTTP API type (cheaper than REST API, perfect for this use case)
- CORS configuration for cross-origin requests
- Request validation before hitting Lambda
- Automatic HTTPS endpoints
- CloudWatch integration for monitoring
- Throttling and rate limiting built-in
Amazon DynamoDB: NoSQL at Scale
DynamoDB serves as the persistent data layer:
- Single-digit millisecond latency at any scale
- Pay-per-request billing mode: No minimum costs
- Automatic scaling: Handles traffic spikes without intervention
- Point-in-time recovery: Built-in backups
- Flexible schema: JSON documents map naturally to order data
- Strong consistency options for critical operations
The table uses orderId as partition key for fast lookups and efficient data distribution.
AWS SAM: Infrastructure as Code
SAM (Serverless Application Model) manages the entire infrastructure:
- Single YAML file defines all resources
- CloudFormation under the hood for reliable deployments
- Local testing with
sam localbefore deployment - Automatic IAM role creation with least privilege
- Environment variable management for secrets
Additional AWS Services
- CloudWatch Logs: Centralized logging for all Lambda invocations
- CloudWatch Metrics: Performance monitoring and alerting
- IAM: Fine-grained access control between services
- AWS SDK v3: Modular imports reduce bundle size
Node.js 20.x with ES Modules
Modern JavaScript runtime with:
- Native ES modules (ESM) for cleaner imports
- Top-level await support
- Better tree-shaking for smaller deployments
- Async/await throughout for readable async code
Lambda Functions: Microservices in Action
The system consists of 7 Lambda functions, each deployed independently with its own CloudWatch log group and IAM role:
1. CreateOrderFunction (POST /orders)
Handles transaction initialization from the STM32 board. Validates input, calculates pricing server-side (critical security requirement), generates a unique orderId, and stores initial state in DynamoDB.
Response structure:
{
"orderId": "554f5df5-7a3b-4fc5-aef0-f68a53613309",
"amount": 380,
"productType": "cappuccino",
"customizations": ["extra_shot", "extra_milk"],
"status": "pending"
}2. InitiatePaymentFunction (POST /orders/{id}/pay)
Integrates with Razorpay SDK to create a payment order. Generates UPI payment URL and updates DynamoDB with payment metadata.
AWS SDK usage:
- DynamoDB
GetItemto verify order exists - DynamoDB
UpdateItemto store payment ID - External API call to Razorpay
Response:
{
"paymentId": "order_Ra6Q26sp5i8WRe",
"qrCodeUrl": "upi://pay?pa=test@razorpay&pn=IoTDevice&am=380&cu=INR",
"amount": 380,
"currency": "INR"
}3. GetQRFunction (GET /orders/{id}/qr)
Simple read operation from DynamoDB to retrieve payment QR code data. Used by STM32 board to display payment interface.
DynamoDB operation: GetItem with orderId partition key
4. HandleWebhookFunction (POST /webhooks/razorpay)
The most critical Lambda function - handles asynchronous payment notifications from Razorpay:
- Verifies HMAC-SHA256 webhook signature for security
- Processes payment events:
payment.authorized,payment.captured,payment.failed - Updates DynamoDB transaction state atomically
- Returns 200 OK to acknowledge receipt
Security implementation:
const crypto = require('crypto');
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(payload))
.digest('hex');This prevents unauthorized parties from sending fake payment confirmations.
5. GetStatusFunction (GET /orders/{id})
Status polling endpoint for the STM32 board. Returns current transaction state from DynamoDB. Uses consistent reads to avoid stale data after payment confirmation.
DynamoDB operation: GetItem with ConsistentRead: true
6. NotifyDispenseFunction (POST /orders/{id}/dispense)
Called by STM32 board when physical operation begins. Updates state to dispensing with timestamp.
State validation: Ensures payment is confirmed before allowing state transition.
7. ConfirmDispenseFunction (POST /orders/{id}/confirm)
Final transaction confirmation. Marks order as fulfilled and generates complete audit trail.
Response includes full receipt:
{
"orderId": "554f5df5-7a3b-4fc5-aef0-f68a53613309",
"status": "fulfilled",
"receipt": {
"amount": 380,
"paidAt": "2025-10-31T14:01:09.534Z",
"dispensedAt": "2025-10-31T14:01:31.629Z",
"fulfilledAt": "2025-10-31T14:01:59.657Z"
}
}Each Lambda function is configured with:
- 128MB memory: Optimal for cost vs performance
- 10-second timeout: Prevents runaway executions
- Environment variables: Razorpay keys, JWT secrets, DynamoDB table name
- IAM role: Least-privilege access to DynamoDB operations
AWS Security Implementation
Security is built into every layer of the architecture:
IAM Roles and Least Privilege
Each Lambda function has its own IAM execution role created by SAM:
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrdersTableThis grants only the required DynamoDB operations (GetItem, PutItem, UpdateItem). No Lambda function has access to:
- Other AWS services it doesn't need
- Other DynamoDB tables
- Administrative permissions
Result: Even if a Lambda function is compromised, blast radius is limited.
API Gateway Security
- HTTPS-only endpoints: All traffic encrypted in transit
- Request validation: Malformed requests rejected before reaching Lambda
- Throttling: Prevents DDoS and excessive costs
- CORS configuration: Restricts which domains can call the API
Webhook Signature Verification
The HandleWebhookFunction implements cryptographic verification:
const crypto = require('crypto');
const signature = req.headers['x-razorpay-signature'];
const expectedSignature = crypto
.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSignature) {
return { statusCode: 401, body: 'Invalid signature' };
}This HMAC-SHA256 verification ensures webhooks genuinely come from Razorpay, preventing fake payment confirmations.
JWT Authentication for IoT Devices
STM32 boards authenticate using JWT tokens:
- 24-hour expiry: Limits window for compromised tokens
- Signed with HS256: Verified server-side before processing requests
- Machine ID embedded: Tracks which device made each request
const jwt = require('jsonwebtoken');
const token = jwt.verify(
authHeader,
process.env.JWT_SECRET,
{ algorithms: ['HS256'] }
);Server-Side Pricing Calculation
Critical for payment security: pricing logic lives entirely in Lambda. STM32 boards send product selection and options, but never the final price. This prevents:
- Price manipulation attacks
- Client-side tampering
- Business logic bypass
Secrets Management
Sensitive data stored in environment variables:
- Razorpay API keys
- JWT signing secrets
- Webhook secrets
Best practice: Should migrate to AWS Secrets Manager for automatic rotation, but environment variables work for initial deployment.
DynamoDB Security
- Encryption at rest: Enabled by default
- Encryption in transit: All SDK calls use TLS
- No public access: Only accessible via IAM-authenticated Lambda functions
- Point-in-time recovery: Enabled for disaster recovery
Deployment with AWS SAM
AWS SAM transforms deployment from complex CloudFormation gymnastics into simple commands. The entire infrastructure is defined in a single template.yaml:
SAM Template Structure
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs20.x
Architectures: [arm64]
MemorySize: 128
Timeout: 10
Environment:
Variables:
DYNAMODB_TABLE: !Ref OrdersTable
Resources:
# API Gateway HTTP API
VendingApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowOrigins: ["*"]
AllowMethods: ["GET", "POST"]
# DynamoDB Table
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: orderId
AttributeType: S
KeySchema:
- AttributeName: orderId
KeyType: HASH
# Lambda Functions
CreateOrderFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/functions/createOrder/index.handler
Events:
CreateOrder:
Type: HttpApi
Properties:
Path: /orders
Method: POST
ApiId: !Ref VendingApi
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrdersTableSAM CLI Commands
Build the application:
sam buildCompiles code, resolves dependencies, creates deployment packages for each Lambda function.
Deploy to AWS:
sam deploy --guidedCreates CloudFormation stack, provisions all resources, sets up API Gateway routes, creates IAM roles automatically.
Local testing:
sam local start-apiRuns API Gateway and Lambda locally using Docker containers - perfect for development without AWS costs.
What SAM Handles Automatically
- CloudFormation stack creation: Manages all AWS resources as a single unit
- IAM role generation: Creates execution roles with correct permissions
- API Gateway routing: Wires HTTP endpoints to Lambda functions
- Environment variable injection: Passes config to all functions
- Deployment packaging: Bundles code with node_modules
- Resource naming: Generates unique names to avoid conflicts
- Stack updates: Handles incremental changes safely
Deployment Output
After sam deploy, you get:
- API Gateway endpoint URL: Base URL for all API calls
- CloudFormation stack name: For resource management
- Lambda function ARNs: For direct invocation if needed
- DynamoDB table name: For manual queries
Example output:
Stack Name: vending-machine-backend
Region: ap-south-1
Outputs:
ApiEndpoint: https://abc123xyz.execute-api.ap-south-1.amazonaws.com
OrdersTableName: vending-machine-backend-OrdersTable-ABC123Testing and Validation
Postman Collection for API Testing
Created a comprehensive collection to validate the entire AWS infrastructure:
Complete transaction flow test:
POST /orders- Creates order with customizations → DynamoDB insertPOST /orders/{id}/pay- Initiates Razorpay integration → DynamoDB updateGET /orders/{id}/qr- Retrieves QR data → DynamoDB readPOST /webhooks/razorpay- Simulates webhook → State transition to 'paid'GET /orders/{id}- Polls status → Consistent read from DynamoDBPOST /orders/{id}/dispense- Starts operation → State to 'dispensing'POST /orders/{id}/confirm- Completes transaction → Final DynamoDB update
Performance metrics:
- Average Lambda execution time: 150-300ms
- DynamoDB read latency: 5-10ms
- End-to-end flow: <3 seconds
- API Gateway overhead: <50ms
Jest Unit Tests
47 test cases covering:
- Pricing calculation logic
- JWT token verification
- Webhook signature validation
- DynamoDB operations
- Error handling and edge cases
npm testAll tests run locally without AWS costs using mocked SDK calls.
AWS CloudWatch Integration
Every Lambda invocation automatically creates detailed logs in CloudWatch:
Log Groups structure:
/aws/lambda/vending-machine-backend-CreateOrderFunction-ABC123
/aws/lambda/vending-machine-backend-InitiatePaymentFunction-DEF456
/aws/lambda/vending-machine-backend-HandleWebhookFunction-GHI789
...Query all log groups:
aws logs describe-log-groups \
--log-group-name-prefix "/aws/lambda/vending-machine-backend"Search logs by orderId:
aws logs filter-log-events \
--log-group-name "/aws/lambda/vending-machine-backend-CreateOrderFunction-ABC123" \
--filter-pattern "554f5df5-7a3b-4fc5-aef0-f68a53613309"CloudWatch Metrics
Automatic metrics for each Lambda:
- Invocations: Total request count
- Duration: Execution time (p50, p99, p99.9)
- Errors: Failed invocations
- Throttles: Rate limit hits
- Concurrent Executions: Active function instances
CloudWatch Alarms
Set up alarms for operational monitoring:
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
MetricName: Errors
Threshold: 5
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThresholdX-Ray Tracing (Optional Enhancement)
Enable distributed tracing to visualize requests across services:
- API Gateway → Lambda → DynamoDB flow
- External API call latency (Razorpay)
- Cold start identification
- Bottleneck detection
AWS Deployment Results
After deploying to production on AWS, here's what the architecture delivers:
Performance Metrics
Lambda Execution Times:
- CreateOrderFunction: 180ms average
- InitiatePaymentFunction: 250ms (includes Razorpay API call)
- HandleWebhookFunction: 120ms
- GetStatusFunction: 85ms (simple DynamoDB read)
- P99 latency: <800ms across all functions
DynamoDB Performance:
- Read operations: 5-10ms latency
- Write operations: 10-15ms latency
- Consistent reads: <20ms
- Zero throttling with on-demand mode
API Gateway Overhead:
- HTTP API adds 30-50ms per request
- Much faster than REST API Gateway
- Automatic TLS termination
Cold Starts:
- 128MB memory allocation → cold start ~200ms
- arm64 architecture → 20% faster than x86_64
- ESM modules → smaller bundle size
- Minimal impact: functions stay warm with regular traffic
AWS Cost Analysis
The serverless architecture optimizes for low, predictable costs:
| Service | Configuration | Free Tier Limit | Expected Monthly Cost | |---------|--------------|-----------------|----------------------| | Lambda | 128MB, 200ms avg | 1M requests, 400K GB-seconds | $0.00 | | API Gateway | HTTP API | 1M requests | $0.00 | | DynamoDB | On-demand | 25 RCU/WCU, 25GB storage | $0.00 | | CloudWatch | Standard logs | 5GB ingestion, 5GB storage | $0.00 |
Estimated cost for 3,000 transactions/month: $0-2
Even scaling to 10,000 transactions would cost under $10/month. Compare this to:
- EC2 t3.micro (always-on): $7-10/month minimum
- RDS (even smallest instance): $15-20/month minimum
Reliability and Testing
Test Coverage:
- 47 Jest unit tests with mocked AWS SDK
- Webhook signature verification tested
- JWT authentication flow validated
- Pricing calculation edge cases covered
- DynamoDB operation error handling
Production Safeguards:
- IAM least privilege per function
- Idempotent API operations
- DynamoDB conditional writes prevent race conditions
- Automatic CloudWatch error logging
- API Gateway request throttling
Key AWS Learnings
1. SAM Dramatically Simplifies Serverless Deployment
Before SAM, managing CloudFormation templates for Lambda + API Gateway + DynamoDB was painful. SAM's simplified syntax and automatic resource wiring cut deployment complexity by 80%.
Traditional CloudFormation: 500+ lines of YAML SAM template: 150 lines with same functionality
2. DynamoDB On-Demand Is Perfect for IoT
No capacity planning required. The on-demand billing mode scales automatically and costs nothing when idle. For unpredictable IoT traffic patterns, this beats provisioned capacity.
Key insight: Start with on-demand, only switch to provisioned if you have consistent high traffic.
3. HTTP API Gateway vs REST API
HTTP API Gateway is:
- 70% cheaper than REST API Gateway
- Faster (lower latency)
- Simpler (fewer features, but that's often good)
Unless you need API keys, usage plans, or request/response transformation, always choose HTTP API.
4. Lambda Memory Affects More Than RAM
Increasing Lambda memory also increases CPU allocation. The 128MB setting was optimal for this workload:
- Fast enough (200ms cold starts)
- Cheap enough (minimal cost)
- Testing showed 256MB didn't improve performance enough to justify 2x cost
5. arm64 Architecture Wins
Switching from x86_64 to arm64 (Graviton2):
- 20% faster execution
- 20% lower cost
- Zero code changes (Node.js is architecture-agnostic)
One line in SAM template: Architectures: [arm64]
6. CloudWatch Insights is Underrated
Being able to query logs across all Lambda functions simultaneously:
fields @timestamp, @message
| filter orderId = "554f5df5-7a3b-4fc5-aef0-f68a53613309"
| sort @timestamp asc
This traces an entire transaction across all microservices. Invaluable for debugging.
7. IAM Policies Generated by SAM Are Production-Ready
Initially planned to write custom IAM policies. SAM's DynamoDBCrudPolicy generated exactly what was needed with perfect least-privilege scope. Don't over-engineer security.
Future AWS Enhancements
Several AWS services could extend this architecture:
AWS IoT Core Integration
Instead of HTTP polling, STM32 boards could use MQTT over AWS IoT Core:
- Persistent connections to AWS
- Real-time bidirectional communication
- Device shadows for state synchronization
- Built-in security with X.509 certificates
- Pub/sub model for event-driven updates
Amazon EventBridge
Replace direct Lambda invocations with event-driven architecture:
- Decouple payment events from business logic
- Multiple services can react to same event
- Built-in event replay and archiving
- Schedule-based triggers for analytics
AWS Step Functions
Complex workflows (retry logic, timeouts) could use Step Functions:
- Visual workflow representation
- Automatic error handling and retries
- Long-running workflows (up to 1 year)
- Built-in state machine management
Amazon Timestream
Time-series database for IoT metrics:
- Transaction completion times
- Device performance metrics
- Payment success rates over time
- Automatic data lifecycle management
AWS Secrets Manager
Migrate from environment variables:
- Automatic secret rotation
- Fine-grained IAM access
- Audit logging for secret access
- Integration with RDS, Redshift, etc.
Final Thoughts: AWS Serverless for IoT
This project showcases AWS serverless architecture at its best. The combination of Lambda, API Gateway, and DynamoDB creates a system that:
Scales effortlessly: 10 devices or 10,000 devices - same code, zero configuration changes
Costs appropriately: Pay only for actual usage. Idle devices cost nothing.
Operates reliably: AWS handles infrastructure, you handle business logic
Deploys simply: One sam deploy command creates entire infrastructure
The STM32 + AWS Pattern
This architecture pattern works for any IoT scenario:
- Edge device (STM32): Handles physical operations, local processing
- AWS Lambda: Business logic, integrations, calculations
- DynamoDB: State persistence, audit trails
- API Gateway: Secure communication channel
The STM32 board does what embedded systems do best (real-time control, sensor reading, actuator control), while AWS handles what cloud does best (scalability, integrations, data storage).
Key Takeaway
Serverless isn't just for web applications. It's exceptionally well-suited for IoT:
- Unpredictable traffic patterns → auto-scaling Lambda
- Global device fleets → CloudFront + API Gateway edge locations
- Minimal operations budget → pay-per-request pricing
- Fast iteration → deploy updates in seconds
From initial architecture to production deployment took just a few weeks. The AWS ecosystem (SAM, CloudWatch, IAM) provided everything needed without requiring additional tools or services.
Complete AWS Stack:
- Compute: Lambda (Node.js 20.x, arm64)
- API: API Gateway (HTTP API)
- Database: DynamoDB (on-demand)
- Monitoring: CloudWatch Logs + Metrics
- Security: IAM + JWT
- Deployment: SAM/CloudFormation
- Testing: Jest + Postman
If you're building IoT systems and haven't explored AWS serverless, this architecture pattern is production-tested and ready to adapt. The code is cleaner, the infrastructure is simpler, and the costs are lower than traditional server-based approaches.
Here's to building cloud-native IoT systems that scale.