Skip to main content

Project Overview

Estimated Time: 2-3 hours | Difficulty: Intermediate | Cost: ~$5/month for testing
In this hands-on project, you’ll build a complete serverless URL shortener service. This is a common system design interview question and demonstrates key serverless patterns. What You’ll Build:
  • REST API with API Gateway
  • Lambda functions for business logic
  • DynamoDB for data persistence
  • CloudFront for caching and performance
  • Complete observability with CloudWatch and X-Ray
Skills Demonstrated:
  • Serverless architecture design
  • DynamoDB data modeling
  • API design and implementation
  • Cost optimization
  • Production-ready practices

Architecture Overview

Let’s design a serverless web application for a URL shortener service.

Requirements

  • Handle 10,000+ requests per second
  • Sub-100ms latency
  • 99.9% availability
  • Pay only for actual usage
  • Auto-scaling without management

High-Level Architecture

┌────────────────────────────────────────────────────────────────────┐
│                  Serverless URL Shortener                           │
├────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   User                                                              │
│     │                                                               │
│     │  1. POST /shorten                                             │
│     │  2. GET /abc123                                               │
│     ▼                                                               │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │                      CloudFront                              │  │
│   │              (CDN + Edge Caching)                            │  │
│   └─────────────────────────┬───────────────────────────────────┘  │
│                             │                                       │
│                             ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │                     API Gateway                              │  │
│   │              (REST API + Throttling)                         │  │
│   └─────────────────────────┬───────────────────────────────────┘  │
│                             │                                       │
│             ┌───────────────┼───────────────┐                      │
│             │               │               │                      │
│             ▼               ▼               ▼                      │
│   ┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐         │
│   │ Lambda: Create  │ │ Lambda:     │ │ Lambda:         │         │
│   │ Short URL       │ │ Redirect    │ │ Analytics       │         │
│   └────────┬────────┘ └──────┬──────┘ └────────┬────────┘         │
│            │                 │                  │                   │
│            └─────────────────┼──────────────────┘                   │
│                              │                                      │
│                              ▼                                      │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │                      DynamoDB                                │  │
│   │                  (URL Mappings Table)                        │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                                                                     │
└────────────────────────────────────────────────────────────────────┘

Implementation

1. DynamoDB Table Design

# Table: url-shortener
# Partition Key: short_code (String)
# No Sort Key needed

table_schema = {
    "TableName": "url-shortener",
    "KeySchema": [
        {"AttributeName": "short_code", "KeyType": "HASH"}
    ],
    "AttributeDefinitions": [
        {"AttributeName": "short_code", "AttributeType": "S"}
    ],
    "BillingMode": "PAY_PER_REQUEST",  # Auto-scaling
    "TimeToLiveSpecification": {
        "AttributeName": "expires_at",
        "Enabled": True
    }
}

# Item structure
item = {
    "short_code": "abc123",
    "long_url": "https://example.com/very/long/url/path",
    "created_at": "2024-01-15T10:00:00Z",
    "expires_at": 1705312800,  # TTL epoch
    "click_count": 0,
    "user_id": "user_456"  # Optional
}

2. Lambda: Create Short URL

import json
import boto3
import hashlib
import time

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')

def generate_short_code(url: str) -> str:
    """Generate 6-character short code from URL hash."""
    hash_object = hashlib.sha256(f"{url}{time.time()}".encode())
    return hash_object.hexdigest()[:6]

def lambda_handler(event, context):
    try:
        body = json.loads(event['body'])
        long_url = body['url']
        
        # Validate URL
        if not long_url.startswith(('http://', 'https://')):
            return {
                'statusCode': 400,
                'body': json.dumps({'error': 'Invalid URL'})
            }
        
        # Generate short code
        short_code = generate_short_code(long_url)
        
        # Store in DynamoDB
        table.put_item(Item={
            'short_code': short_code,
            'long_url': long_url,
            'created_at': int(time.time()),
            'click_count': 0
        })
        
        short_url = f"https://short.ly/{short_code}"
        
        return {
            'statusCode': 201,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({
                'short_url': short_url,
                'short_code': short_code
            })
        }
        
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

3. Lambda: Redirect

import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')

def lambda_handler(event, context):
    short_code = event['pathParameters']['code']
    
    # Get URL from DynamoDB
    response = table.get_item(Key={'short_code': short_code})
    
    if 'Item' not in response:
        return {
            'statusCode': 404,
            'body': json.dumps({'error': 'URL not found'})
        }
    
    long_url = response['Item']['long_url']
    
    # Increment click count (async would be better)
    table.update_item(
        Key={'short_code': short_code},
        UpdateExpression='ADD click_count :inc',
        ExpressionAttributeValues={':inc': 1}
    )
    
    # Return 301 redirect
    return {
        'statusCode': 301,
        'headers': {
            'Location': long_url,
            'Cache-Control': 'max-age=300'  # Cache redirect for 5 min
        }
    }

4. API Gateway Configuration

# serverless.yml (Serverless Framework)
service: url-shortener

provider:
  name: aws
  runtime: python3.11
  region: us-east-1
  
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
          Resource: !GetAtt UrlTable.Arn

functions:
  createUrl:
    handler: handlers.create_url
    events:
      - http:
          path: /shorten
          method: post
          cors: true

  redirect:
    handler: handlers.redirect
    events:
      - http:
          path: /{code}
          method: get

resources:
  Resources:
    UrlTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: url-shortener
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: short_code
            AttributeType: S
        KeySchema:
          - AttributeName: short_code
            KeyType: HASH

Scaling Considerations

API Gateway Limits

┌────────────────────────────────────────────────────────────────┐
│                    API Gateway Throttling                       │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Default Limits (per region):                                  │
│   • 10,000 requests/second                                      │
│   • 5,000 concurrent requests                                   │
│                                                                 │
│   Can request increase for high-traffic apps                    │
│                                                                 │
│   Throttling Strategies:                                        │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  1. Usage Plans - Per-API key limits                     │  │
│   │  2. Stage throttling - Per-stage limits                  │  │
│   │  3. Method throttling - Per-endpoint limits              │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

Lambda Concurrency

# Lambda scales automatically
# Default: 1000 concurrent executions per region

# Reserved concurrency - Guarantee capacity
# Provisioned concurrency - Eliminate cold starts

concurrency_config = {
    "redirect_function": {
        "reserved_concurrency": 500,  # Guarantee 500 concurrent
        "provisioned_concurrency": 100  # Pre-warmed instances
    }
}

DynamoDB Capacity

┌────────────────────────────────────────────────────────────────┐
│                  DynamoDB Scaling                               │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│   On-Demand Mode:                                               │
│   • Auto-scales instantly                                       │
│   • Pay per request                                             │
│   • Best for unpredictable traffic                              │
│                                                                 │
│   Provisioned Mode:                                             │
│   • Set RCU/WCU capacity                                        │
│   • Auto-scaling available                                      │
│   • Better for predictable traffic                              │
│                                                                 │
│   DAX (DynamoDB Accelerator):                                   │
│   ┌──────────┐      ┌──────────┐      ┌──────────┐             │
│   │  Lambda  │ ───► │   DAX    │ ───► │ DynamoDB │             │
│   └──────────┘      │ (cache)  │      └──────────┘             │
│                     └──────────┘                                │
│   • Microsecond latency                                         │
│   • 10x read performance                                        │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

Cost Analysis

# Monthly cost estimate for 10M requests/month

costs = {
    "api_gateway": {
        "requests": 10_000_000,
        "cost_per_million": 3.50,
        "total": 35.00  # $35
    },
    "lambda": {
        "requests": 10_000_000,
        "avg_duration_ms": 50,
        "memory_mb": 128,
        "gb_seconds": 10_000_000 * 0.050 * (128/1024),
        "compute_cost": 6.25,  # ~$0.0000166667 per GB-second
        "request_cost": 2.00,  # $0.20 per million
        "total": 8.25  # $8.25
    },
    "dynamodb": {
        "writes": 1_000_000,  # 10% create new
        "reads": 10_000_000,
        "write_cost": 1.25,  # $1.25 per million WRU
        "read_cost": 0.25,   # $0.25 per million RRU
        "storage_gb": 1,
        "storage_cost": 0.25,
        "total": 1.75  # $1.75
    },
    "cloudfront": {
        "requests": 10_000_000,
        "data_transfer_gb": 10,
        "total": 10.00  # ~$10
    },
    "total_monthly": 55.00  # ~$55/month for 10M requests
}

Monitoring & Observability

┌────────────────────────────────────────────────────────────────┐
│                    Monitoring Stack                             │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│   CloudWatch Metrics                                            │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  • Lambda: Invocations, Duration, Errors, Throttles      │  │
│   │  • API GW: Count, Latency, 4XX, 5XX                      │  │
│   │  • DynamoDB: ConsumedRCU, ConsumedWCU, Throttles         │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   CloudWatch Alarms                                             │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  • Lambda errors > 1% → Alert                            │  │
│   │  • API Gateway 5XX > 0.1% → Alert                        │  │
│   │  • DynamoDB throttling → Alert                           │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   X-Ray Tracing                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  API GW → Lambda → DynamoDB                              │  │
│   │  End-to-end latency visualization                        │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

Security Considerations

API Gateway

  • WAF integration
  • API keys for rate limiting
  • Request validation

Lambda

  • Least privilege IAM
  • VPC for private resources
  • Secrets in Secrets Manager

DynamoDB

  • Encryption at rest
  • IAM for access control
  • VPC endpoints

CloudFront

  • HTTPS only
  • Geo-restrictions if needed
  • Origin Access Identity
Key Takeaway: Serverless architectures eliminate server management, scale automatically, and cost effectively for variable workloads. Start simple and add complexity (caching, analytics) as needed.

🎯 Interview Questions for This Case Study

Advantages:
  • Variable traffic (pay for actual usage)
  • No server management
  • Auto-scales from 0 to millions
  • Quick time to market
Trade-offs:
  • Cold start latency (mitigate with provisioned concurrency)
  • 15-minute function limit (not an issue here)
  • Vendor lock-in
When NOT serverless:
  • Predictable, constant high traffic (EC2 cheaper)
  • Long-running processes
  • WebSocket-heavy (API Gateway WS works but pricey)
Scaling strategy:
  1. CloudFront: Cache redirects at edge
    • 301 with Cache-Control: max-age=300
    • 80%+ cache hit ratio
  2. API Gateway: Request higher limits
    • Default 10K/sec is usually enough
  3. Lambda: Reserved concurrency
    • Set to expected peak (e.g., 5000)
  4. DynamoDB: On-demand mode handles spikes
    • Add DAX for microsecond reads
  5. Monitoring: Alarms before hitting limits
Defense layers:
  1. Rate limiting: API Gateway usage plans
    usagePlan:
      quota:
        limit: 1000
        period: DAY
      throttle:
        burstLimit: 50
        rateLimit: 10
    
  2. WAF: Block malicious IPs, SQL injection
  3. CAPTCHA: For create endpoint
  4. URL validation: Block malicious targets
  5. User authentication: Cognito for premium features
Async analytics pipeline:
Lambda → Kinesis Data Firehose → S3 → Athena
  1. Log click events to Kinesis (async, non-blocking)
  2. Firehose batches to S3 every 60 seconds
  3. Query with Athena for dashboards
Metrics to track:
  • Click count per URL
  • Geographic distribution
  • Referrer analysis
  • Time-based patterns
Cost breakdown at 100M requests/month:
ServiceCost
API Gateway$350
Lambda$83
DynamoDB$35
CloudFront$85
Total~$550/month
Optimization opportunities:
  • CloudFront caching (reduces Lambda calls 80%)
  • Reserved concurrency (predictable billing)
  • DynamoDB provisioned (if traffic predictable)

🧪 Hands-On Implementation Steps

1

Create DynamoDB Table

Create table with on-demand billing and TTL enabled
2

Create Lambda Functions

Deploy create and redirect functions with proper IAM roles
3

Set Up API Gateway

Create REST API with two endpoints, deploy to prod stage
4

Add CloudFront Distribution

Point to API Gateway, configure caching
5

Configure Monitoring

Set up CloudWatch alarms and X-Ray tracing
6

Test Load

Use Artillery or hey to test performance

Course Complete! 🎉

Congratulations on completing the AWS Cloud Fundamentals course! You’ve learned:
  • ✅ AWS core concepts and regions
  • ✅ Compute services (EC2, Lambda, ECS)
  • ✅ Storage and databases (S3, RDS, DynamoDB)
  • ✅ Networking (VPC, security groups, load balancers)
  • ✅ Security (IAM, KMS, encryption)
  • ✅ Well-Architected Framework
  • ✅ Real-world serverless architecture
Next Steps:
  1. Practice with hands-on labs
  2. Build your own projects
  3. Prepare for AWS certification
  4. Review interview questions regularly

Return to Course Overview

Review the complete curriculum and resources