> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Case Study: Serverless URL Shortener

> Build a production-ready serverless application with Lambda, API Gateway, and DynamoDB

## Project Overview

<Info>
  **Estimated Time**: 2-3 hours | **Difficulty**: Intermediate | **Cost**: \~\$5/month for testing
</Info>

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. Think of it as building a mini Bit.ly -- a deceptively simple product that touches nearly every serverless building block AWS offers, from API design to data modeling to edge caching.

**What You'll Build:**

* REST API with API Gateway (your front door -- all traffic enters here)
* Lambda functions for business logic (stateless compute that scales to zero)
* DynamoDB for data persistence (single-digit millisecond lookups at any scale)
* CloudFront for caching and performance (reduces Lambda invocations by 80%+ for redirects)
* Complete observability with CloudWatch and X-Ray (because you cannot improve what you cannot measure)

**Skills Demonstrated:**

* Serverless architecture design
* DynamoDB data modeling (single-table design for a real use case)
* API design and implementation
* Cost optimization (this architecture costs roughly the price of a coffee per month at moderate traffic)
* Production-ready practices (error handling, monitoring, security layers)

## 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

```python theme={null}
# Table: url-shortener
# Partition Key: short_code (String)
# No Sort Key needed

table_schema = {
    "TableName": "url-shortener",
    "KeySchema": [
        # short_code is the partition key -- each short URL maps to exactly one item,
        # so a simple primary key (no sort key) is the right choice here.
        {"AttributeName": "short_code", "KeyType": "HASH"}
    ],
    "AttributeDefinitions": [
        {"AttributeName": "short_code", "AttributeType": "S"}
    ],
    # PAY_PER_REQUEST (on-demand) is ideal for a URL shortener because traffic
    # is unpredictable -- viral links can spike 1000x in minutes.
    # Cost tip: switch to provisioned mode once traffic stabilizes to save ~80%.
    "BillingMode": "PAY_PER_REQUEST",
    "TimeToLiveSpecification": {
        # TTL lets DynamoDB automatically delete expired URLs at no cost.
        # Items are removed within 48 hours of expiration (not instant).
        "AttributeName": "expires_at",
        "Enabled": True
    }
}

# Item structure
item = {
    "short_code": "abc123",           # Partition key -- the 6-char code in the URL
    "long_url": "https://example.com/very/long/url/path",
    "created_at": "2024-01-15T10:00:00Z",
    "expires_at": 1705312800,         # TTL epoch -- DynamoDB auto-deletes after this
    "click_count": 0,                 # Atomic counter updated on each redirect
    "user_id": "user_456"             # Optional -- enables per-user analytics later
}
```

### 2. Lambda: Create Short URL

```python theme={null}
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.
    
    Why SHA-256 + timestamp? The timestamp prevents collisions when the same URL
    is shortened multiple times (each submission gets its own short code).
    6 hex chars = 16^6 = ~16.7 million unique codes. For higher volume,
    increase to 7-8 chars or switch to base62 encoding for shorter URLs.
    
    Common mistake: using only the URL without a salt -- identical URLs
    would always produce the same code, breaking per-user analytics.
    """
    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

```python theme={null}
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 using atomic ADD operation.
    # Common mistake: using SET click_count = click_count + 1, which is NOT atomic.
    # ADD is atomic and handles concurrent updates correctly.
    # Production tip: for high-traffic URLs, decouple analytics into a Kinesis stream
    # so the redirect path stays fast (don't let analytics slow down the user).
    table.update_item(
        Key={'short_code': short_code},
        UpdateExpression='ADD click_count :inc',
        ExpressionAttributeValues={':inc': 1}
    )
    
    # Return 301 (permanent redirect) so browsers and CDN cache the mapping.
    # 301 vs 302: use 301 if the mapping is stable (better for SEO and caching).
    # Use 302 if you need to track every click (browsers won't cache 302s).
    # Cache-Control: 300s means CloudFront serves cached redirects for 5 minutes,
    # reducing Lambda invocations by 80%+ for popular links.
    return {
        'statusCode': 301,
        'headers': {
            'Location': long_url,
            'Cache-Control': 'max-age=300'
        }
    }
```

### 4. API Gateway Configuration

```yaml theme={null}
# serverless.yml (Serverless Framework)
service: url-shortener

provider:
  name: aws
  runtime: python3.11
  region: us-east-1
  
  iam:
    role:
      statements:
        # Least-privilege: only allow the specific DynamoDB actions this service needs.
        # Common mistake: using dynamodb:* -- this grants delete, scan, and admin
        # operations that your Lambda should never perform.
        - 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

```python theme={null}
# Lambda scales automatically
# Default: 1000 concurrent executions per region

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

concurrency_config = {
    "redirect_function": {
        # Reserved concurrency: guarantees this function can always use 500 slots.
        # This also CAPS it at 500 -- protecting other functions from being starved.
        "reserved_concurrency": 500,
        # Provisioned concurrency: keeps 100 containers warm at all times.
        # Eliminates cold starts for the first 100 concurrent requests.
        # Cost tip: at 128 MB memory, 100 provisioned instances cost ~$40/month.
        # Only use this for latency-critical paths (like the redirect function).
        "provisioned_concurrency": 100
    }
}
```

### 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

```python theme={null}
# 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

<CardGroup cols={2}>
  <Card title="API Gateway" icon="shield">
    * WAF integration
    * API keys for rate limiting
    * Request validation
  </Card>

  <Card title="Lambda" icon="lock">
    * Least privilege IAM
    * VPC for private resources
    * Secrets in Secrets Manager
  </Card>

  <Card title="DynamoDB" icon="database">
    * Encryption at rest
    * IAM for access control
    * VPC endpoints
  </Card>

  <Card title="CloudFront" icon="globe">
    * HTTPS only
    * Geo-restrictions if needed
    * Origin Access Identity
  </Card>
</CardGroup>

<Tip>
  **Key Takeaway**: Serverless architectures eliminate server management, scale automatically, and cost effectively for variable workloads. Start simple and add complexity (caching, analytics) as needed.
</Tip>

***

## 🎯 Interview Questions for This Case Study

<AccordionGroup>
  <Accordion title="Q1: Why use serverless for a URL shortener?">
    **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)
  </Accordion>

  <Accordion title="Q2: How would you handle 1 million concurrent users?">
    **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
  </Accordion>

  <Accordion title="Q3: How would you prevent abuse?">
    **Defense layers:**

    1. **Rate limiting**: API Gateway usage plans
       ```yaml theme={null}
       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
  </Accordion>

  <Accordion title="Q4: How would you add analytics?">
    **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
  </Accordion>

  <Accordion title="Q5: What's the total cost at scale?">
    **Cost breakdown at 100M requests/month:**

    | Service     | Cost              |
    | ----------- | ----------------- |
    | 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)
  </Accordion>
</AccordionGroup>

***

## 🧪 Hands-On Implementation Steps

<Steps>
  <Step title="Create DynamoDB Table">
    Create table with on-demand billing and TTL enabled
  </Step>

  <Step title="Create Lambda Functions">
    Deploy create and redirect functions with proper IAM roles
  </Step>

  <Step title="Set Up API Gateway">
    Create REST API with two endpoints, deploy to prod stage
  </Step>

  <Step title="Add CloudFront Distribution">
    Point to API Gateway, configure caching
  </Step>

  <Step title="Configure Monitoring">
    Set up CloudWatch alarms and X-Ray tracing
  </Step>

  <Step title="Test Load">
    Use Artillery or hey to test performance
  </Step>
</Steps>

***

## 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

<Card title="Return to Course Overview" icon="house" href="/aws/overview">
  Review the complete curriculum and resources
</Card>
