Krish Gaur ~ /home/krish
krish@dev
cat ~/blog/building-vending-machine-backend-aws.mdx

Building a Serverless IoT Backend on AWS: Lambda, DynamoDB & Payment Integration

November 1, 2025
9 min read
aws
serverless
lambda
dynamodb
api-gateway
iot
sam
nodejs

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:

  1. STM32 board initiates a transaction with customizable parameters
  2. AWS Lambda calculates pricing based on selected options
  3. Razorpay payment gateway generates a UPI QR code
  4. API Gateway returns QR code data to the STM32 board
  5. User completes payment via UPI
  6. Razorpay webhook notifies Lambda of payment confirmation
  7. DynamoDB state is updated and STM32 board is authorized to proceed
  8. 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 local before 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 GetItem to verify order exists
  • DynamoDB UpdateItem to 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 OrdersTable

This 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 OrdersTable

SAM CLI Commands

Build the application:

sam build

Compiles code, resolves dependencies, creates deployment packages for each Lambda function.

Deploy to AWS:

sam deploy --guided

Creates CloudFormation stack, provisions all resources, sets up API Gateway routes, creates IAM roles automatically.

Local testing:

sam local start-api

Runs 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-ABC123

Testing and Validation

Postman Collection for API Testing

Created a comprehensive collection to validate the entire AWS infrastructure:

Complete transaction flow test:

  1. POST /orders - Creates order with customizations → DynamoDB insert
  2. POST /orders/{id}/pay - Initiates Razorpay integration → DynamoDB update
  3. GET /orders/{id}/qr - Retrieves QR data → DynamoDB read
  4. POST /webhooks/razorpay - Simulates webhook → State transition to 'paid'
  5. GET /orders/{id} - Polls status → Consistent read from DynamoDB
  6. POST /orders/{id}/dispense - Starts operation → State to 'dispensing'
  7. 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 test

All 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: GreaterThanThreshold

X-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.

2025 © Krish Gaur |chmod +x life