Cloud Backend Engineering · Serverless · Event-driven

AWS Architecture Lambda · RDS Aurora · SQS · Cognito · CDK

The world's most battle-tested cloud. A complete field guide to production AWS backend architecture — serverless-first, IaC from day one, and every managed service in its right place.

Lambda API Gateway RDS Aurora SQS + SNS Cognito DynamoDB ElastiCache EventBridge Secrets Manager CloudWatch S3 CDK (IaC) Bedrock AI WAF + Shield
01
Lambda
Serverless compute
02
API Gateway
HTTP + WebSocket
03
RDS Aurora
Serverless PostgreSQL
04
SQS + SNS
Async messaging
05
Cognito
Identity + RBAC
06
EventBridge
Event bus + rules
07
DynamoDB
NoSQL at any scale
08
CDK (IaC)
Infrastructure as code

01 — Architecture

The Layer Contract

Six distinct layers, each owned by specific AWS services. Every architectural decision traces back to this stack — from the CDN edge down to the database.

The AWS philosophy: Use managed services for everything that isn't your core business logic. Lambda replaces servers. RDS Aurora replaces database administration. SQS/SNS replace message brokers. Cognito replaces auth services. You write business logic — AWS operates the infrastructure.
6
Edge + CDN
CloudFront for static assets and API caching. WAF + Shield for DDoS and OWASP Top 10. Route 53 for DNS + health checks. ACM for managed TLS.
CloudFront WAF + Shield Route 53 ACM
5
API + Auth Gateway
API Gateway (HTTP API v2) for REST endpoints and WebSocket. Cognito User Pools + JWT authorizers. Lambda authorizers for custom RBAC logic.
API Gateway v2 Cognito Lambda Authorizer
4
Compute — Lambda + ECS
Lambda for stateless HTTP handlers, queue processors, and event handlers. ECS Fargate (via Docker) for long-running WebSocket servers, ML inference, and scheduled batch jobs.
Lambda ECS Fargate Step Functions
3
Messaging + Events
SQS for durable async task queues (at-least-once). SNS for fan-out pub/sub. EventBridge for scheduled rules and cross-service event routing. Step Functions for complex workflows.
SQS SNS EventBridge
2
Data + Cache
RDS Aurora PostgreSQL (Serverless v2) for relational data. DynamoDB for key-value at any scale. ElastiCache (Redis) for caching and rate limiting. S3 for object storage.
RDS Aurora DynamoDB ElastiCache S3
1
Security + Infrastructure
Secrets Manager for all credentials. IAM roles with least-privilege per Lambda. KMS for encryption key management. VPC with private subnets for RDS/ElastiCache. CDK for all infrastructure as code.
Secrets Manager IAM KMS VPC CDK

02 — Service Selection

Service Reference

AWS has 200+ services. These are the ones that form a complete, production-grade backend — and exactly when to use each one versus the alternatives.

Lambda
Serverless compute
Stateless function execution — HTTP handlers, SQS consumers, S3 triggers, EventBridge rules. Up to 15-min timeout, 10GB memory. ARM64 (Graviton) for 20% better price/performance. Provisioned concurrency for cold start elimination.
HTTP APISQS consumerS3 trigger
🌐
API Gateway v2 (HTTP API)
HTTP + WebSocket
Use HTTP API v2 (not REST API v1) — 71% cheaper, lower latency, supports JWT authorizers natively. WebSocket API for real-time bidirectional. Lambda integrations. VPC Link for private backends.
JWT authorizerWebSocket API
🗄️
RDS Aurora Serverless v2
PostgreSQL / MySQL
PostgreSQL-compatible, auto-scales 0.5–128 ACUs in seconds. RDS Proxy pools connections — critical for Lambda (which creates new connections on every cold start). Multi-AZ for automatic failover. Data API for HTTP-based SQL (no connection pooling needed).
RDS ProxyData APIMulti-AZ
📨
SQS + SNS
Async messaging
SQS Standard for at-least-once delivery. SQS FIFO for exactly-once ordered processing. SNS for fan-out — one publish → multiple SQS queues. Dead-letter queues (DLQ) for failed messages. Lambda SQS event source mapping with batch size and retry config.
Dead-letter queueFIFOFan-out
👤
Cognito User Pools
Identity + RBAC
Managed user directory with email/password, Google/Apple social login, MFA, and device remembering. Issues JWTs (id + access + refresh tokens). Custom attributes for RBAC. Pre-signup and post-confirmation Lambda triggers. API Gateway JWT authorizer integrates natively.
JWT authorizerCustom attributes
🔔
EventBridge
Event bus + scheduler
Default event bus for AWS service events. Custom event bus for your domain events. Rules route events to Lambda, SQS, Step Functions, API Gateway. EventBridge Scheduler for cron jobs and one-time scheduled tasks — replaces CloudWatch Events.
PutEvents()Scheduler
DynamoDB
NoSQL key-value
Single-digit millisecond at any scale. On-demand billing — no capacity planning. Global tables for multi-region active-active. DynamoDB Streams for change data capture → Lambda triggers. Use for session storage, leaderboards, time-series — not for complex relational queries.
DynamoDB StreamsGlobal tables
🏃
ECS Fargate
Containerized workloads
For workloads Lambda can't handle: WebSocket servers, long-running ML inference, batch processing over 15 min. Serverless containers — no EC2 to manage. ALB integration for HTTP. Service discovery via Cloud Map. Use with ECR for container registry.
WebSocket serverLong tasks
🔐
Secrets Manager
Credentials + rotation
Store DB passwords, API keys, JWT secrets. Automatic rotation for RDS credentials — Lambda never sees the password, just fetches the current secret. IAM resource policies control which Lambda functions can access which secrets. Caching in Lambda with AWS Parameters and Secrets Lambda extension.
GetSecretValue()Auto-rotation
🔵
CDK (Cloud Development Kit)
Infrastructure as code
TypeScript IaC — define Lambda, API Gateway, RDS, SQS, and all infrastructure as typed TypeScript classes. CDK synthesizes to CloudFormation. L2 constructs handle 80% of boilerplate. Constructs are composable and reusable across stacks. Always use CDK — never ClickOps.
cdk deploycdk synth
💾
ElastiCache (Redis)
In-memory cache
Redis 7 via Serverless mode — auto-scales, no cluster management. Use for: API response caching, session storage, rate limiting (INCR + EXPIRE), leaderboards, pub/sub. In VPC — Lambda must be in same VPC. Serverless mode supports IAM auth.
Serverless modeIAM auth
🧠
Bedrock
AI / foundation models
Claude (Anthropic), Llama, Titan, Stable Diffusion via unified API. No separate API key — uses IAM role. Streaming via InvokeModelWithResponseStream. Knowledge Bases for RAG. Agents for multi-step reasoning. Guardrails for content safety.
InvokeModel()Knowledge Bases

03 — Project Structure

Monorepo Layout

Monorepo with separate packages for Lambda functions, CDK infrastructure, and shared types. Esbuild bundles each Lambda independently — small artifact, fast cold start.

project-root /
packages/
functions/— Lambda handlers
src/
features/— one dir per domain
auth/
handler.tsLambda— HTTP event handler
service.ts— business logic
repository.tsRDS
schema.ts— Drizzle ORM table
products/
handler.tsLambda
consumer.tsSQS— SQS event processor
service.ts
repository.tsDrizzle
notifications/
consumer.tsSQS consumer
service.ts— SES email, FCM push
ai/
handler.tsLambda
bedrock.service.ts— AWS Bedrock / Claude
shared/
db.ts— Drizzle + RDS Proxy singleton
auth.middleware.tsCognito
errors.ts
sqs.publisher.ts— typed SQS message publisher
cache.ts— ElastiCache Redis helper
logger.ts— Powertools structured logger
validate.ts— Zod validation helper
secrets.ts— Secrets Manager with caching
infra/— AWS CDK stacks
stacks/
ApiStack.tsCDK— API Gateway + Lambda
DatabaseStack.tsCDK— RDS Aurora + Proxy
MessagingStack.tsCDK— SQS + SNS + EventBridge
AuthStack.tsCDK— Cognito User Pool
CacheStack.tsCDK— ElastiCache Serverless
VpcStack.tsCDK
bin/app.ts— CDK App entry
types/— shared TypeScript types across packages
pnpm-workspace.yaml
turbo.json— Turborepo build pipeline

04 — Code Patterns

TypeScript Blueprints

Production-ready TypeScript using AWS SDK v3, Drizzle ORM, AWS Lambda Powertools, and CDK v2. Every snippet is copy-paste ready.

products/handler.ts — HTTP LambdaLambda
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 }
  from 'aws-lambda'
import { logger }         from '../shared/logger'
import { requireAuth }    from '../shared/auth.middleware'
import { validate }       from '../shared/validate'
import { ProductService } from './service'
import { z }             from 'zod'

const CreateSchema = z.object({
  name:  z.string().min(1).max(200),
  price: z.number().positive(),
  sku:   z.string().min(1),
})

// Middy middleware wraps: Powertools tracer + logger
export const handler = async (
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {

  const user = requireAuth(event)
  if (!user) return { statusCode: 401, body: 'Unauthorized' }

  const method = event.requestContext.http.method

  if (method === 'GET') {
    const products = await ProductService.list(user.sub)
    return json(200, products)
  }

  if (method === 'POST') {
    const body = validate(CreateSchema, JSON.parse(event.body ?? '{}'))
    if (!body.success) return json(422, body.error.flatten())
    const product = await ProductService.create(body.data, user.sub)
    return json(201, product)
  }

  return json(405, { error: 'Method not allowed' })
}

const json = (status: number, body: unknown) => ({
  statusCode: status,
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
})
products/consumer.ts — SQS LambdaSQS consumer
import { SQSEvent, SQSBatchResponse } from 'aws-lambda'
import { logger } from '../shared/logger'

// Partial batch failure — only retry failed messages
export const handler = async (
  event: SQSEvent
): Promise<SQSBatchResponse> => {
  const failures: string[] = []

  await Promise.allSettled(
    event.Records.map(async (record) => {
      try {
        const msg = JSON.parse(record.body)
        await processMessage(msg)
        logger.info('Message processed', { id: record.messageId })
      } catch (err) {
        logger.error('Message failed', { id: record.messageId, err })
        // Report partial failure — only this message retries
        failures.push(record.messageId)
      }
    })
  )

  return {
    batchItemFailures: failures.map(id => ({
      itemIdentifier: id,
    })),
  }
}

async function processMessage(msg: unknown) {
  // Validate + handle the typed SQS message
  const parsed = ProductEventSchema.parse(msg)
  switch (parsed.type) {
    case 'product.created':
      return NotificationService.notifySeller(parsed.sellerId)
    default:
      logger.warn('Unknown event type', { type: parsed.type })
  }
}
Cognito Lambda triggersCognito
import { PreSignUpTriggerEvent, PostConfirmationTriggerEvent }
  from 'aws-lambda'
import { CognitoIdentityProviderClient,
  AdminAddUserToGroupCommand }
  from '@aws-sdk/client-cognito-identity-provider'

const cognito = new CognitoIdentityProviderClient({})

// Block disposable email domains before signup
export const preSignUp = async (e: PreSignUpTriggerEvent) => {
  const banned = ['mailinator.com', 'tempmail.com']
  const domain = e.request.userAttributes.email?.split('@')[1]
  if (banned.includes(domain ?? ''))
    throw new Error('Email domain not allowed')
  return e
}

// Create user profile in RDS after confirmation
export const postConfirmation = async (
  e: PostConfirmationTriggerEvent
) => {
  const { sub, email, name } = e.request.userAttributes

  // Add to 'users' Cognito group (default role)
  await cognito.send(new AdminAddUserToGroupCommand({
    UserPoolId: e.userPoolId,
    Username:   e.userName,
    GroupName:  'users',
  }))

  // Create DB record
  await UserRepository.create({ id: sub, email, name })
  return e
}
shared/db.ts — Drizzle + RDS ProxyRDS Aurora
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool }    from 'pg'
import { getSecret } from './secrets'
import * as schema from './schema'

let db: ReturnType<typeof drizzle> | null = null

// Lambda execution context reuses the connection pool
// RDS Proxy manages the actual PG connections
export async function getDb() {
  if (db) return db

  const secret = await getSecret('rds/credentials')
  const creds  = JSON.parse(secret)

  const pool = new Pool({
    // Connect to RDS Proxy endpoint — not direct RDS
    host:     process.env.RDS_PROXY_ENDPOINT,
    user:     creds.username,
    password: creds.password,
    database: process.env.DB_NAME,
    ssl:      { rejectUnauthorized: true },
    max:      5,  // Low — Lambda shares via proxy
    idleTimeoutMillis: 30_000,
  })

  db = drizzle(pool, { schema, logger: false })
  return db
}
auth/schema.ts — Drizzle schemaRDS
import { pgTable, text, timestamp, pgEnum }
  from 'drizzle-orm/pg-core'

export const roleEnum = pgEnum('user_role', [
  'admin', 'moderator', 'user',
])

export const users = pgTable('users', {
  id:        text('id').primaryKey(),      // Cognito sub
  email:     text('email').notNull().unique(),
  name:      text('name').notNull(),
  role:      roleEnum('role').default('user').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

export const products = pgTable('products', {
  id:        text('id').primaryKey()
              .$defaultFn(() => crypto.randomUUID()),
  name:      text('name').notNull(),
  price:     text('price').notNull(),
  sku:       text('sku').notNull().unique(),
  sellerId:  text('seller_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

export type User    = typeof users.$inferSelect
export type NewUser = typeof users.$inferInsert
products/repository.ts — Drizzle queriesdata
import { eq, ilike, desc } from 'drizzle-orm'
import { getDb }           from '../shared/db'
import { products }        from './schema'
import type { NewProduct } from './schema'

export class ProductRepository {

  static async findById(id: string) {
    const db = await getDb()
    return db.query.products.findFirst({
      where: eq(products.id, id),
    })
  }

  static async listBySeller(
    sellerId: string,
    opts: { search?: string; limit?: number } = {},
  ) {
    const db = await getDb()
    return db.select().from(products)
      .where(
        opts.search
          ? ilike(products.name, `%${opts.search}%`)
          : eq(products.sellerId, sellerId)
      )
      .orderBy(desc(products.createdAt))
      .limit(opts.limit ?? 20)
  }

  static async create(data: NewProduct) {
    const db = await getDb()
    const [row] = await db
      .insert(products)
      .values(data)
      .returning()
    return row
  }
}
shared/auth.middleware.ts — Cognito JWTauth
import { APIGatewayProxyEventV2 } from 'aws-lambda'
import { CognitoJwtVerifier }      from 'aws-jwt-verify'

const verifier = CognitoJwtVerifier.create({
  userPoolId:  process.env.USER_POOL_ID!,
  tokenUse:    'access',
  clientId:    process.env.USER_POOL_CLIENT_ID!,
})

export interface AuthContext {
  sub:      string
  email:    string
  groups:   string[]
  username: string
}

export async function requireAuth(
  event: APIGatewayProxyEventV2,
): Promise<AuthContext | null> {
  const token = event.headers.authorization
    ?.replace('Bearer ', '')
  if (!token) return null

  try {
    const payload = await verifier.verify(token)
    return {
      sub:      payload.sub,
      email:    payload.email as string,
      groups:   (payload['cognito:groups'] ?? []) as string[],
      username: payload.username as string,
    }
  } catch {
    return null
  }
}

// RBAC check — Cognito groups = roles
export function requireGroup(
  ctx: AuthContext,
  ...groups: string[]
): boolean {
  return groups.some(g => ctx.groups.includes(g))
}
API Gateway JWT authorizer via CDKCDK · API GW
// In ApiStack.ts — attach Cognito authorizer to HTTP API
import { HttpUserPoolAuthorizer }
  from 'aws-cdk-lib/aws-apigatewayv2-authorizers'
import { HttpLambdaIntegration }
  from 'aws-cdk-lib/aws-apigatewayv2-integrations'

const authorizer = new HttpUserPoolAuthorizer(
  'CognitoAuth',
  userPool,
  { userPoolClients: [userPoolClient] },
)

// All routes protected by Cognito JWT by default
api.addRoutes({
  path:         '/products',
  methods:      [HttpMethod.GET, HttpMethod.POST],
  integration:  new HttpLambdaIntegration('Products', productsFn),
  authorizer,
})

// Public routes — no authorizer
api.addRoutes({
  path:        '/health',
  methods:     [HttpMethod.GET],
  integration: new HttpLambdaIntegration('Health', healthFn),
})

// Admin routes — Lambda authorizer checks group membership
api.addRoutes({
  path:        '/admin/users',
  methods:     [HttpMethod.GET],
  integration: new HttpLambdaIntegration('Admin', adminFn),
  authorizer:  lambdaAuthorizer, // custom group check
})
shared/sqs.publisher.ts — typed publisherSQS
import { SQSClient, SendMessageCommand,
  SendMessageBatchCommand }
  from '@aws-sdk/client-sqs'

const sqs = new SQSClient({ region: process.env.AWS_REGION })

// Type-safe message publisher
export async function publishToQueue<T extends object>(
  queueUrl:     string,
  message:      T,
  opts: {
    delaySeconds?:    number
    dedupId?:         string  // FIFO only
    groupId?:         string  // FIFO only
  } = {},
) {
  return sqs.send(new SendMessageCommand({
    QueueUrl:               queueUrl,
    MessageBody:            JSON.stringify(message),
    DelaySeconds:           opts.delaySeconds,
    MessageDeduplicationId: opts.dedupId,
    MessageGroupId:         opts.groupId,
    MessageAttributes: {
      eventType: {
        DataType:    'String',
        StringValue: (message as any).type ?? 'unknown',
      },
    },
  }))
}

// In a service:
await publishToQueue(process.env.ORDERS_QUEUE_URL!, {
  type:    'order.placed',
  orderId: order.id,
  userId:  user.sub,
  amount:  order.total,
})
SNS fan-out + EventBridgeSNS · EventBridge
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns'
import { EventBridgeClient, PutEventsCommand }
  from '@aws-sdk/client-eventbridge'

const sns = new SNSClient({})
const eb  = new EventBridgeClient({})

// SNS — fan-out to multiple SQS queues
export async function publishToTopic(
  topicArn: string,
  message:  unknown,
  subject?: string,
) {
  return sns.send(new PublishCommand({
    TopicArn: topicArn,
    Message:  JSON.stringify(message),
    Subject:  subject,
  }))
}

// EventBridge — domain events, cross-account routing
export async function putDomainEvent<T>(
  source:     string,
  detailType: string,
  detail:     T,
) {
  return eb.send(new PutEventsCommand({
    Entries: [{
      Source:       source,
      DetailType:   detailType,
      Detail:       JSON.stringify(detail),
      EventBusName: process.env.EVENT_BUS_NAME,
    }],
  }))
}

// Example usage:
// await putDomainEvent('myapp.orders', 'OrderPlaced', { orderId, ... })
// EventBridge rule routes to Lambda, SQS, or Step Functions
shared/cache.ts — ElastiCache RedisElastiCache
import { createClient } from 'redis'

let client: ReturnType<typeof createClient> | null = null

async function getClient() {
  if (client?.isReady) return client

  client = createClient({
    // ElastiCache Serverless endpoint
    url:  `rediss://${process.env.CACHE_ENDPOINT}:6379`,
    socket: { tls: true, rejectUnauthorized: true },
  })
  await client.connect()
  return client
}

// Generic typed cache-aside helper
export async function cached<T>(
  key:     string,
  ttlSec:  number,
  fetcher: () => Promise<T>,
): Promise<T> {
  const c   = await getClient()
  const hit = await c.get(key)
  if (hit) return JSON.parse(hit) as T

  const value = await fetcher()
  await c.setEx(key, ttlSec, JSON.stringify(value))
  return value
}

// Rate limiting with Redis INCR + EXPIRE
export async function checkRateLimit(
  key:         string,
  maxRequests: number,
  windowSec:   number,
): Promise<boolean> {
  const c     = await getClient()
  const count = await c.incr(key)
  if (count === 1) await c.expire(key, windowSec)
  return count <= maxRequests
}
shared/secrets.ts — Secrets Managersecrets
import { SecretsManagerClient, GetSecretValueCommand }
  from '@aws-sdk/client-secrets-manager'

const sm    = new SecretsManagerClient({})
const cache = new Map<string, string>()

export async function getSecret(name: string): Promise<string> {
  // Cache in Lambda execution context (warm instances)
  if (cache.has(name)) return cache.get(name)!

  const { SecretString } = await sm.send(
    new GetSecretValueCommand({ SecretId: name })
  )

  if (!SecretString) throw new Error(`Secret ${name} is empty`)
  cache.set(name, SecretString)
  return SecretString
}

// Or use the Lambda Extension for zero-latency secrets:
// Set env vars at function level — extension fetches them
// before your handler runs and caches for 5 minutes.
//
// In CDK:
// fn.addEnvironment('MY_SECRET',
//   Secret.fromSecretsManager(mySecret).secretValue.toString())
//
// Then: process.env.MY_SECRET — no SDK call needed.
ai/bedrock.service.ts — Claude on AWSBedrock
import { BedrockRuntimeClient,
  InvokeModelCommand,
  InvokeModelWithResponseStreamCommand }
  from '@aws-sdk/client-bedrock-runtime'

const bedrock = new BedrockRuntimeClient({
  region: 'us-east-1',  // Bedrock model availability varies by region
})

export class BedrockService {

  // Claude 3.5 Sonnet via Bedrock — IAM auth, no API key
  static async generate(
    prompt:   string,
    maxTokens = 2048,
  ): Promise<string> {
    const { body } = await bedrock.send(
      new InvokeModelCommand({
        modelId:     'anthropic.claude-3-5-sonnet-20241022-v2:0',
        contentType: 'application/json',
        accept:      'application/json',
        body: JSON.stringify({
          anthropic_version: 'bedrock-2023-05-31',
          max_tokens:        maxTokens,
          messages: [{ role: 'user', content: prompt }],
        }),
      })
    )
    const res = JSON.parse(new TextDecoder().decode(body))
    return res.content[0].text
  }

  // Streaming — for Lambda response streaming or SSE
  static async* stream(prompt: string) {
    const { body } = await bedrock.send(
      new InvokeModelWithResponseStreamCommand({
        modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
        contentType: 'application/json',
        body: JSON.stringify({
          anthropic_version: 'bedrock-2023-05-31',
          max_tokens: 4096,
          messages: [{ role: 'user', content: prompt }],
          stream: true,
        }),
      })
    )
    for await (const chunk of body!) {
      const parsed = JSON.parse(
        new TextDecoder().decode(chunk.chunk?.bytes)
      )
      if (parsed.type === 'content_block_delta')
        yield parsed.delta?.text ?? ''
    }
  }
}
stacks/ApiStack.ts — CDK Lambda + APIGWCDK v2
import * as cdk    from 'aws-cdk-lib'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'

export class ApiStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: ApiProps) {
    super(scope, id, props)

    const productsFn = new NodejsFunction(this, 'ProductsFn', {
      entry:       'packages/functions/src/features/products/handler.ts',
      handler:     'handler',
      runtime:     lambda.Runtime.NODEJS_20_X,
      architecture:lambda.Architecture.ARM_64,  // Graviton — cheaper
      memorySize:  512,
      timeout:     cdk.Duration.seconds(29),
      vpc:         props.vpc,                   // must be in VPC for RDS
      vpcSubnets:  { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      environment: {
        RDS_PROXY_ENDPOINT: props.rdsProxy.endpoint,
        DB_NAME:            'myapp',
        CACHE_ENDPOINT:     props.cacheEndpoint,
      },
      bundling: { minify: true, sourceMap: true, externalModules: [] },
    })

    // Grant least-privilege access to Secrets Manager
    props.dbSecret.grantRead(productsFn)

    const api = new apigwv2.HttpApi(this, 'Api', {
      corsPreflight: {
        allowOrigins: ['https://myapp.com'],
        allowMethods: [apigwv2.CorsHttpMethod.ANY],
        allowHeaders: ['Authorization', 'Content-Type'],
      },
    })

    new cdk.CfnOutput(this, 'ApiUrl', { value: api.url! })
  }
}
stacks/DatabaseStack.ts — RDS AuroraCDK · RDS
import * as rds  from 'aws-cdk-lib/aws-rds'
import * as ec2  from 'aws-cdk-lib/aws-ec2'

export class DatabaseStack extends cdk.Stack {
  public readonly cluster:  rds.DatabaseCluster
  public readonly proxy:    rds.DatabaseProxy
  public readonly secret:   secretsmanager.ISecret

  constructor(scope: cdk.App, id: string, props: DbProps) {
    super(scope, id, props)

    this.cluster = new rds.DatabaseCluster(this, 'Cluster', {
      engine: rds.DatabaseClusterEngine.auroraPostgres({
        version: rds.AuroraPostgresEngineVersion.VER_16_1,
      }),
      serverlessV2MinCapacity: 0.5,
      serverlessV2MaxCapacity: 128,
      writer: rds.ClusterInstance.serverlessV2('writer', {
        publiclyAccessible: false,
      }),
      readers: [
        rds.ClusterInstance.serverlessV2('reader', {
          scaleWithWriter: true,
        }),
      ],
      vpc: props.vpc,
      storageEncrypted: true,        // KMS encryption
      deletionProtection: true,       // prod safeguard
      backup: { retention: cdk.Duration.days(7) },
    })

    // RDS Proxy — connection pooling for Lambda
    this.proxy = this.cluster.addProxy('Proxy', {
      secrets:            [this.cluster.secret!],
      vpc:                props.vpc,
      requireTLS:         true,
      iamAuth:            true,       // IAM instead of password
    })
    this.secret = this.cluster.secret!
  }
}
stacks/MessagingStack.ts — SQS + SNSCDK · SQS
import * as sqs from 'aws-cdk-lib/aws-sqs'
import * as sns from 'aws-cdk-lib/aws-sns'
import * as sub from 'aws-cdk-lib/aws-sns-subscriptions'
import { SqsEventSource }
  from 'aws-cdk-lib/aws-lambda-event-sources'

export class MessagingStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: cdk.StackProps) {
    super(scope, id, props)

    // DLQ — catches failed messages after 3 retries
    const dlq = new sqs.Queue(this, 'DLQ', {
      retentionPeriod: cdk.Duration.days(14),
      encryption: sqs.QueueEncryption.KMS_MANAGED,
    })

    const ordersQueue = new sqs.Queue(this, 'OrdersQueue', {
      visibilityTimeout: cdk.Duration.seconds(30),
      encryption:        sqs.QueueEncryption.KMS_MANAGED,
      deadLetterQueue:   { queue: dlq, maxReceiveCount: 3 },
    })

    // SNS topic → fan-out to multiple queues
    const ordersTopic = new sns.Topic(this, 'OrdersTopic')
    ordersTopic.addSubscription(new sub.SqsSubscription(ordersQueue))

    // Wire consumer Lambda to SQS with partial batch failure
    consumerFn.addEventSource(new SqsEventSource(ordersQueue, {
      batchSize:                    10,
      maxBatchingWindow:            cdk.Duration.seconds(5),
      reportBatchItemFailures:      true,  // partial failure
      maxConcurrency:               50,
    }))
  }
}
shared/logger.ts — Lambda Powertoolsobservability
import { Logger } from '@aws-lambda-powertools/logger'
import { Tracer } from '@aws-lambda-powertools/tracer'
import { Metrics, MetricUnit }
  from '@aws-lambda-powertools/metrics'

// Lambda Powertools — structured logging, tracing, metrics
// All auto-correlated via X-Ray trace ID
export const logger = new Logger({
  serviceName: process.env.SERVICE_NAME ?? 'api',
  logLevel:    process.env.LOG_LEVEL as any ?? 'INFO',
})

export const tracer = new Tracer({
  serviceName: process.env.SERVICE_NAME ?? 'api',
  captureHTTPsRequests: true,
})

export const metrics = new Metrics({
  namespace:   'MyApp',
  serviceName: process.env.SERVICE_NAME ?? 'api',
})

// Usage in handler:
// logger.info('Order placed', { orderId, userId })
// tracer.getSegment()?.addAnnotation('orderId', orderId)
// metrics.addMetric('OrdersPlaced', MetricUnit.Count, 1)

// drizzle-kit scripts:
// "db:generate": "drizzle-kit generate"
// "db:migrate":  "drizzle-kit migrate"
// "db:studio":   "drizzle-kit studio"
// "deploy":      "cdk deploy --all"
// "test":        "vitest run"

05 — Data Flows

Every Request Traced

From CloudFront edge to RDS row — every hop across managed services, with the AWS service at each step.

Authenticated HTTP request → RDS

Client
CloudFront + WAF
API Gateway v2
JWT authorizer (Cognito)
Lambda handler
RDS Proxy → Aurora

Order placed → async processing

Lambda creates order
INSERT into RDS
publishToQueue(SQS)
Consumer Lambda
SNS → email + push

Cache-aside with ElastiCache

Lambda request
Redis GET(key)
→ hit →
return cached
Redis miss
RDS Proxy query
Redis SETEX(300s)
return fresh

User registration flow

Cognito SignUp
preSignUp Lambda trigger
Email verify
postConfirmation trigger
INSERT user into RDS

EventBridge scheduled job

EventBridge rule (cron)
Lambda scheduled handler
Batch RDS cleanup
Powertools metrics logged

Bedrock AI streaming response

API Gateway WebSocket
Lambda streaming
Bedrock.stream()
Chunks via API GW WS
Client receives SSE

SQS DLQ + retry

Consumer Lambda fails
SQS makes message visible (30s)
3 attempts exhausted
DLQ — 14 day retention
CloudWatch alarm

06 — Feature Matrix

Every Requirement Covered

All backend requirements mapped to AWS-idiomatic managed services — nothing to operate, everything managed.

Lambda compute
ARM64 (Graviton3) for 20% better price-perf. Provisioned concurrency for cold start elimination on critical paths. Power Tuning tool to find optimal memory. Snapstart for Java. Up to 10GB memory, 15-min timeout.
aws-lambda
👤
Auth + RBAC via Cognito
User Pools with email/password + Google/Apple federation. Groups = roles (admins, users, moderators). API Gateway JWT authorizer validates tokens natively — no Lambda needed for auth. preSignUp + postConfirmation triggers for custom logic.
aws-jwt-verify
🗄️
RDS Aurora + RDS Proxy
Serverless v2 auto-scales 0.5–128 ACUs. RDS Proxy is mandatory for Lambda — it pools connections so each Lambda invocation doesn't create a new PG connection. IAM auth via Proxy eliminates password rotation complexity.
drizzle-orm pg
📨
SQS + SNS async
SQS Standard for most queues. FIFO for ordered, exactly-once processing (payments, inventory). Dead-letter queues catch failures after 3 retries — 14-day retention for replay. Partial batch failure reporting — only failed messages retry.
@aws-sdk/client-sqs
💾
ElastiCache Serverless
Redis 7 with no cluster management. Serverless mode auto-scales. IAM auth — no Redis password. In VPC for network isolation. Used for API caching, rate limiting (INCR+EXPIRE), session storage, and leaderboards.
redis
🔐
Secrets Manager
All credentials in Secrets Manager — RDS passwords auto-rotate every 30 days. Lambda functions access via IAM resource policy (no hardcoded ARNs in code). Lambda Extension for zero-latency secret injection at startup.
@aws-sdk/client-secrets-manager
🔔
EventBridge scheduler
Replaces cron daemons and CloudWatch Events. Cron syntax or rate expressions. One-time scheduled events with deduplication. Routes to Lambda, SQS, Step Functions, API destinations. DLQ for failed invocations.
@aws-sdk/client-eventbridge
🛡️
WAF + Shield + CloudFront
WAF on CloudFront — rate limiting, OWASP Top 10, IP reputation lists, geo-blocking. Shield Standard included. Shield Advanced for DDoS response team. All HTTPS via ACM-managed TLS. CloudFront caches static assets globally.
CDK WAFv2Construct
🧠
Bedrock AI
Claude 3.5 Sonnet, Llama 3, Titan, Stable Diffusion — all via unified Bedrock API. IAM auth — no API keys. Knowledge Bases for RAG with S3 data sources. Agents for multi-step tool use. Guardrails for content filtering.
@aws-sdk/client-bedrock-runtime
🌐
API Gateway WebSocket
WebSocket API for real-time bidirectional. Lambda handles $connect, $disconnect, and custom route handlers. Connection IDs stored in DynamoDB. @connections API sends messages back to specific clients. No persistent server needed.
API Gateway WS API
📊
Lambda Powertools
Structured JSON logging to CloudWatch Logs. X-Ray distributed tracing across all Lambda + AWS SDK calls. Custom CloudWatch metrics. All correlated via trace ID. Alarm on DLQ depth, Lambda errors, RDS connections, and latency P99.
@aws-lambda-powertools/logger
🏗️
CDK (IaC)
All infrastructure as TypeScript. NodejsFunction handles esbuild bundling per Lambda. CDK Pipelines for CI/CD — deploy to dev/staging/prod via CodePipeline. cdk diff before every deploy. Never use ClickOps — if it's not in CDK, it doesn't exist.
aws-cdk-lib

07 — SDK Registry

The npm Registry

Pinned to production-stable versions. AWS SDK v3 — modular, tree-shakable, smaller Lambda bundles than v2.

AWS SDK v3: Import only what you need — @aws-sdk/client-sqs, not the monolithic v2 aws-sdk. Each client is a separate package — esbuild bundles only the code your Lambda actually uses. Average Lambda bundle drops from 8MB to under 500KB.
PackageVersionPurposeLayer
aws-lambdatypesTypeScript types for all Lambda event types — APIGatewayProxyEventV2, SQSEvent, S3Event, etc.Lambda
@aws-sdk/client-cognito-identity-provider^3.xAdmin operations — AdminAddUserToGroup, AdminDisableUser, ListUsersInGroupAuth
aws-jwt-verify^4.xCognito JWT verification in Lambda — CognitoJwtVerifier with JWKS cachingAuth
@aws-sdk/client-rds-data^3.xRDS Data API — HTTP-based SQL without connection pooling (alternative to RDS Proxy)RDS
drizzle-orm^0.41Type-safe ORM for PostgreSQL — schema definition, typed queries, migrations via drizzle-kitRDS
pg^8.13PostgreSQL driver — used by Drizzle, connects via RDS ProxyRDS
drizzle-kit^0.30Migration generator, schema introspection, Drizzle Studio GUIdev
@aws-sdk/client-sqs^3.xSQS send, receive, delete, batch operationsSQS
@aws-sdk/client-sns^3.xSNS publish, topic management, subscriptionsSNS
@aws-sdk/client-eventbridge^3.xPutEvents for domain event publishing, manage rules and targetsEvents
@aws-sdk/client-secrets-manager^3.xGetSecretValue — fetch credentials, API keys, JWT secrets at runtimeSecrets
@aws-sdk/client-s3^3.xS3 object CRUD, presigned URLs for direct client upload/downloadStorage
@aws-sdk/client-bedrock-runtime^3.xInvokeModel, InvokeModelWithResponseStream — Claude, Llama, Titan via BedrockAI
redis^4.7ElastiCache Redis client — get/set/incr/expire, pub/sub, pipelineCache
@aws-lambda-powertools/logger^2.xStructured CloudWatch Logs — JSON, log levels, correlation IDs, samplingOps
@aws-lambda-powertools/tracer^2.xAWS X-Ray distributed tracing — annotate segments, capture HTTP callsOps
@aws-lambda-powertools/metrics^2.xCustom CloudWatch metrics — business KPIs, SLI/SLO trackingOps
aws-cdk-lib^2.170CDK v2 — all AWS constructs in one package, L1/L2/L3 constructsIaC
zod^3.23Runtime schema validation — request body, SQS message shapes, environment variablesShared
vitest^2.xUnit tests — Lambda handlers and services via AWS SDK mocking (aws-sdk-client-mock)test
aws-sdk-client-mock^4.xMock AWS SDK v3 clients in tests — mockClient(SQSClient).on(SendMessageCommand)test
REPLACED BY AWS MANAGED SERVICES: JWT library → Cognito + aws-jwt-verify · Redis server → ElastiCache Serverless · Celery/workers → SQS Lambda triggers · Nginx → CloudFront + API Gateway · Cron daemon → EventBridge Scheduler · Database server → RDS Aurora Serverless v2 · Secret storage → Secrets Manager · Auth service → Cognito User Pools

✦ — AI Scaffold

Generate with AI

Paste into Claude, ChatGPT, Gemini, or any AI agent to scaffold this exact architecture from scratch.

 Architecture Prompt
You are a senior AWS cloud architect. Scaffold a production-ready serverless backend.

Stack:
- Runtime: AWS Lambda (Node.js 20 / TypeScript)
- API: Amazon API Gateway HTTP API (v2)
- Auth: Amazon Cognito User Pools + aws-jwt-verify
- Database: Amazon RDS Aurora Serverless v2 (PostgreSQL) via RDS Proxy
- ORM: Drizzle ORM with drizzle-kit migrations
- Queue: Amazon SQS (FIFO) + Lambda event source mapping
- Events: Amazon EventBridge custom bus
- Storage: Amazon S3 + CloudFront
- Cache: Amazon ElastiCache Serverless (Redis 7)
- Secrets: AWS Secrets Manager
- IaC: AWS CDK v2 (TypeScript)
- Observability: Lambda Powertools (Logger, Tracer, Metrics)
- AI: Amazon Bedrock (Claude claude-sonnet-4-6)

Provide:
1. CDK stack definitions (ApiStack, AuthStack, DatabaseStack, QueueStack)
2. Lambda handler structure with Powertools decorators
3. Drizzle schema + migration pattern
4. IAM least-privilege role per Lambda
5. SQS consumer with dead-letter queue
6. EventBridge publish/subscribe pattern
7. GitHub Actions CI/CD pipeline (test → cdk diff → cdk deploy)
8. Environment variable management with Secrets Manager
crafted with by Sam
 Copied to clipboard!