Serverless & Edge Computing

DynamoDB Serverless Patterns

DynamoDB is the database built for serverless: HTTP-based access with no connections to manage, single-digit millisecond latency at any scale, and pay-per-request billing that scales to zero. But DynamoDB requires a fundamentally different approach to data modeling than relational databases. We design and implement DynamoDB single-table schemas with access pattern-driven GSIs, TTL-based data lifecycle management, and DynamoDB Streams for event-driven processing.

Need this done for your project?

We implement, you ship. Async, documented, done in days.

Start a Brief

Single-Table Design Methodology

We start every DynamoDB design by listing all access patterns, then designing the key schema to support them with the minimum number of indexes.

# Access Pattern Matrix — SaaS project management app
# Pattern                        | Key Condition            | Index
# Get user by ID                 | PK=USER#id, SK=META      | Table
# List projects by user          | PK=USER#id, SK=PROJ#*    | Table
# Get project by ID              | PK=PROJ#id, SK=META      | Table
# List tasks by project          | PK=PROJ#id, SK=TASK#*    | Table
# List tasks by assignee         | GSI1PK=USER#id, GSI1SK=TASK#* | GSI1
# List tasks by due date         | GSI1PK=PROJ#id, GSI1SK=DUE#date | GSI1
# Get task by ID                 | PK=TASK#id, SK=META      | Table
# List recent activity           | GSI2PK=PROJ#id, GSI2SK=ACT#ts | GSI2

# Entity definitions
interface UserEntity {
  PK: `USER#${string}`;       // Partition key
  SK: 'META';                  // Sort key
  GSI1PK: `ORG#${string}`;    // For listing users in org
  GSI1SK: `USER#${string}`;   // Sort by user ID
  type: 'user';
  email: string;
  name: string;
  role: 'admin' | 'member';
}

interface TaskEntity {
  PK: `PROJ#${string}`;       // In project partition
  SK: `TASK#${string}`;       // Task ID as sort key
  GSI1PK: `USER#${string}`;   // Assignee lookup
  GSI1SK: `TASK#${string}`;   // Task ID
  type: 'task';
  title: string;
  status: 'todo' | 'doing' | 'done';
  assigneeId: string;
  dueDate: string;
}

The access pattern matrix is the source of truth. We never add a GSI without a documented access pattern that requires it. This prevents index bloat and keeps write costs predictable.

GSI Overloading & Sparse Indexes

We use GSI overloading to serve multiple access patterns from a single index, and sparse indexes to index only a subset of items.

// GSI1 overloading — serves 3 different access patterns
// depending on the entity type stored in GSI1PK/GSI1SK

// Pattern 1: List users in org
// GSI1PK = ORG#org123, GSI1SK = USER#user456

// Pattern 2: List tasks by assignee  
// GSI1PK = USER#user456, GSI1SK = TASK#task789

// Pattern 3: List invoices by customer
// GSI1PK = CUST#cust123, GSI1SK = INV#2025-01-15

// Sparse index — only items with GSI2PK are indexed
// Use case: index only "flagged" or "urgent" items
resource "aws_dynamodb_table" "main" {
  name         = "app-${var.env}"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "PK"
  range_key    = "SK"

  attribute {
    name = "PK"
    type = "S"
  }
  attribute {
    name = "SK"
    type = "S"
  }
  attribute {
    name = "GSI1PK"
    type = "S"
  }
  attribute {
    name = "GSI1SK"
    type = "S"
  }
  attribute {
    name = "GSI2PK"
    type = "S"
  }
  attribute {
    name = "GSI2SK"
    type = "S"
  }

  global_secondary_index {
    name            = "GSI1"
    hash_key        = "GSI1PK"
    range_key       = "GSI1SK"
    projection_type = "ALL"
  }

  # Sparse index — only items with GSI2PK attribute are indexed
  global_secondary_index {
    name            = "GSI2"
    hash_key        = "GSI2PK"
    range_key       = "GSI2SK"
    projection_type = "INCLUDE"
    non_key_attributes = ["type", "title", "status", "createdAt"]
  }

  ttl {
    attribute_name = "expiresAt"
    enabled        = true
  }

  point_in_time_recovery {
    enabled = true
  }

  stream_enabled   = true
  stream_view_type = "NEW_AND_OLD_IMAGES"
}

Sparse indexes are powerful for filtering: only items that have the GSI2PK attribute appear in the index. An "urgent tasks" index costs nothing for non-urgent tasks because they are not indexed. INCLUDE projection reduces storage costs by only projecting the attributes needed for the query.

TTL & Data Lifecycle

DynamoDB TTL automatically deletes expired items at no cost. We use TTL for session tokens, temporary data, audit log rotation, and soft deletes.

// TTL patterns

// 1. Session tokens — expire after 24 hours
const session = {
  PK: `SESSION#${sessionId}`,
  SK: 'META',
  userId: user.id,
  createdAt: now,
  expiresAt: Math.floor(Date.now() / 1000) + 86400,  // Unix timestamp
};

// 2. Rate limiting windows — expire after window closes
const rateLimit = {
  PK: `RATE#${ip}#${minuteBucket}`,
  SK: 'COUNT',
  count: 1,
  expiresAt: Math.floor(Date.now() / 1000) + 120,  // 2 min after window
};

// 3. Soft deletes — mark deleted, TTL after 30 days
async function softDelete(pk: string, sk: string) {
  await docClient.send(new UpdateCommand({
    TableName: TABLE,
    Key: { PK: pk, SK: sk },
    UpdateExpression: 'SET deleted = :true, expiresAt = :ttl REMOVE GSI1PK, GSI1SK',
    ExpressionAttributeValues: {
      ':true': true,
      ':ttl': Math.floor(Date.now() / 1000) + (30 * 86400),
    },
  }));
  // Removing GSI keys makes item invisible to index queries immediately
  // TTL deletes it permanently after 30 days
}

// 4. DynamoDB Streams capture TTL deletions
// event.Records[].userIdentity.type === 'Service'
// event.Records[].userIdentity.principalId === 'dynamodb.amazonaws.com'
// Use this to trigger cleanup actions (e.g., delete S3 objects)

TTL deletions appear in DynamoDB Streams with a special service principal, letting you trigger cleanup logic — like deleting associated S3 objects or sending expiration notifications — when items are automatically removed.

DynamoDB Streams for Event Processing

DynamoDB Streams captures every change to your table as an ordered sequence of events. We wire Streams to Lambda for real-time event processing, search index updates, and cross-region replication.

resource "aws_lambda_event_source_mapping" "stream" {
  event_source_arn                   = aws_dynamodb_table.main.stream_arn
  function_name                      = aws_lambda_function.stream_processor.arn
  starting_position                  = "LATEST"
  batch_size                         = 100
  maximum_batching_window_in_seconds = 5
  parallelization_factor             = 10
  maximum_retry_attempts             = 3
  bisect_batch_on_function_error     = true
  
  destination_config {
    on_failure {
      destination_arn = aws_sqs_queue.stream_dlq.arn
    }
  }
  
  filter_criteria {
    filter {
      pattern = jsonencode({
        eventName = ["INSERT", "MODIFY"]
        dynamodb = {
          NewImage = {
            type = { S = ["task"] }
          }
        }
      })
    }
  }
}

// Stream processor Lambda
export const handler = async (event: DynamoDBStreamEvent) => {
  for (const record of event.Records) {
    const newImage = unmarshall(record.dynamodb!.NewImage! as any);
    
    switch (newImage.type) {
      case 'task':
        // Update Elasticsearch index
        await esClient.index({
          index: 'tasks',
          id: newImage.SK.replace('TASK#', ''),
          body: {
            title: newImage.title,
            status: newImage.status,
            projectId: newImage.PK.replace('PROJ#', ''),
            updatedAt: newImage.updatedAt,
          },
        });
        break;
    }
  }
};

Event source mapping filters reduce Lambda invocations by filtering events at the service level — you only process the events you care about. Bisect-on-error splits a failed batch in half and retries each half independently, preventing a single bad record from blocking the entire stream. The DLQ captures records that fail after all retries for manual investigation.

Why Anubiz Engineering

100% async — no calls, no meetings
Delivered in days, not weeks
Full documentation included
Production-grade from day one
Security-first approach
Post-delivery support included

Ready to get started?

Skip the research. Tell us what you need, and we'll scope it, implement it, and hand it back — fully documented and production-ready.