Back to blog
Havish Logo
havish
published on 2025-06-22 • 15 min read

AWS Serverless Architecture: From Concept to Production

Serverless architecture represents a fundamental shift in how we build and deploy applications. By abstracting away server management, developers can focus on writing business logic while AWS handles the underlying infrastructure. This comprehensive guide walks you through building production-ready serverless applications on AWS.

Core Serverless Components

A robust serverless architecture leverages multiple AWS services working in harmony:

  • AWS Lambda: Event-driven compute service for executing code
  • API Gateway: Managed service for creating RESTful and WebSocket APIs
  • DynamoDB: Fully managed NoSQL database with single-digit latency
  • S3: Object storage for static assets and data persistence
  • CloudWatch: Monitoring and logging service for observability
  • EventBridge: Event bus for decoupled application architecture

Architecture Patterns

Let's explore a typical serverless API architecture using Infrastructure as Code:

serverless.yml
service: user-management-api

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  environment:
    USERS_TABLE: ${self:service}-users-${self:provider.stage}
    JWT_SECRET: ${ssm:/${self:service}/jwt-secret}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.USERS_TABLE}"

functions:
  createUser:
    handler: src/handlers/users.create
    events:
      - http:
          path: users
          method: post
          cors: true
          authorizer: auth
          
  getUser:
    handler: src/handlers/users.get
    events:
      - http:
          path: users/{id}
          method: get
          cors: true
          authorizer: auth

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.USERS_TABLE}
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST

Lambda Function Best Practices

Writing efficient Lambda functions requires attention to several key principles:

user-handler.js
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');

// Initialize outside handler for connection reuse
const dynamodb = new AWS.DynamoDB.DocumentClient();

const createUser = async (event) => {
  try {
    // Parse and validate input
    const { name, email } = JSON.parse(event.body);
    
    if (!name || !email) {
      return {
        statusCode: 400,
        headers: corsHeaders,
        body: JSON.stringify({ error: 'Name and email are required' })
      };
    }
    
    const userId = uuidv4();
    const timestamp = new Date().toISOString();
    
    const user = {
      userId,
      name,
      email,
      createdAt: timestamp,
      updatedAt: timestamp
    };
    
    // Use consistent error handling
    await dynamodb.put({
      TableName: process.env.USERS_TABLE,
      Item: user,
      ConditionExpression: 'attribute_not_exists(userId)'
    }).promise();
    
    return {
      statusCode: 201,
      headers: corsHeaders,
      body: JSON.stringify(user)
    };
    
  } catch (error) {
    console.error('Error creating user:', error);
    
    return {
      statusCode: error.code === 'ConditionalCheckFailedException' ? 409 : 500,
      headers: corsHeaders,
      body: JSON.stringify({
        error: error.code === 'ConditionalCheckFailedException' 
          ? 'User already exists' 
          : 'Internal server error'
      })
    };
  }
};

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'Content-Type,Authorization',
  'Access-Control-Allow-Methods': 'OPTIONS,POST,GET,PUT,DELETE'
};

module.exports = { createUser };

Event-Driven Architecture

Serverless applications excel in event-driven scenarios. Here's how to implement robust event processing:

Event Processing Pattern
const processUserEvent = async (event) => {
  const promises = event.Records.map(async (record) => {
    try {
      const eventName = record.eventName;
      const dynamoRecord = record.dynamodb;
      
      switch (eventName) {
        case 'INSERT':
          await handleUserCreated(dynamoRecord.NewImage);
          break;
        case 'MODIFY':
          await handleUserUpdated(dynamoRecord.NewImage, dynamoRecord.OldImage);
          break;
        case 'REMOVE':
          await handleUserDeleted(dynamoRecord.OldImage);
          break;
      }
    } catch (error) {
      // Log error but don't fail the entire batch
      console.error('Error processing record:', record, error);
      
      // Send to DLQ for retry
      await sendToDeadLetterQueue(record, error);
    }
  });
  
  await Promise.allSettled(promises);
};

const handleUserCreated = async (userImage) => {
  const user = unmarshallDynamoImage(userImage);
  
  // Send welcome email
  await sendWelcomeEmail(user);
  
  // Update analytics
  await updateUserMetrics('user_created', user);
  
  // Publish event to EventBridge
  await publishEvent('user.created', user);
};

Cost Optimization Strategies

Serverless can be cost-effective when properly optimized. Here are key strategies:

  1. Right-size memory allocation: Monitor CloudWatch metrics to optimize Lambda memory settings
  2. Use provisioned concurrency sparingly: Only for functions requiring consistent low latency
  3. Implement efficient caching: Use DynamoDB DAX or ElastiCache for frequently accessed data
  4. Optimize cold starts: Minimize package size and use connection pooling
  5. Choose appropriate storage classes: Use S3 Intelligent-Tiering for cost optimization

Monitoring and Observability

Production serverless applications require comprehensive observability:

Observability Stack

  • CloudWatch Logs: Centralized logging with structured log format
  • X-Ray: Distributed tracing for request flow visualization
  • CloudWatch Metrics: Custom business metrics and alerting
  • AWS Config: Configuration compliance monitoring

Security Best Practices

  • Principle of Least Privilege: Grant minimal IAM permissions required
  • Environment Variable Encryption: Use AWS KMS for sensitive data
  • VPC Configuration: Deploy Lambda in VPC when accessing private resources
  • API Authentication: Implement JWT or AWS Cognito for user authentication
  • Resource-Based Policies: Use fine-grained access controls

Testing Strategies

Comprehensive testing is crucial for serverless applications:

Jest Test Example
// Mock AWS SDK
jest.mock('aws-sdk');

describe('User Handler', () => {
  beforeEach(() => {
    process.env.USERS_TABLE = 'test-users-table';
    AWS.DynamoDB.DocumentClient.mockClear();
  });

  test('should create user successfully', async () => {
    const mockPut = jest.fn().mockReturnValue({
      promise: () => Promise.resolve({})
    });
    
    AWS.DynamoDB.DocumentClient.mockImplementation(() => ({
      put: mockPut
    }));

    const event = {
      body: JSON.stringify({
        name: 'John Doe',
        email: 'john@example.com'
      })
    };

    const result = await createUser(event);
    
    expect(result.statusCode).toBe(201);
    expect(mockPut).toHaveBeenCalledWith(
      expect.objectContaining({
        TableName: 'test-users-table'
      })
    );
  });
});

Serverless architecture on AWS provides a powerful foundation for building scalable, cost-effective applications. By following these patterns and best practices, you can create production-ready systems that automatically scale with demand while minimizing operational overhead.