Cloud & DevOps
18 min read

Serverless Architecture with AWS Lambda: Advanced Patterns

Explore advanced AWS Lambda patterns including cold start optimization, connection pooling, middleware patterns, and event-driven architectures.

Serverless Architecture with AWS Lambda: Advanced Patterns
DP

Dibyank Padhy

Engineering Manager & Full Stack Developer

Serverless Architecture with AWS Lambda: Advanced Patterns

AWS Lambda has revolutionized how we build and deploy applications. In this deep dive, we'll explore advanced patterns that go beyond basic function handlers to build production-ready serverless architectures.

Cold Start Optimization

Cold starts are the primary concern in Lambda performance. Here are proven strategies to minimize their impact:

typescript
// 1. Move initialization outside the handler
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

// Initialized once during cold start, reused across invocations
const ddbClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(ddbClient, {
  marshallOptions: { removeUndefinedValues: true },
});

// Pre-warm connections
let isWarmed = false;

async function warmUp() {
  if (isWarmed) return;

  // Make a lightweight call to establish connection
  try {
    await docClient.send(new DescribeTableCommand({ TableName: process.env.TABLE_NAME }));
  } catch (e) {
    // Ignore errors during warm-up
  }

  isWarmed = true;
}

export const handler = async (event: APIGatewayEvent) => {
  await warmUp();
  // Your handler logic
};

2. Provisioned Concurrency Configuration

yaml
# serverless.yml
functions:
  api:
    handler: src/handlers/api.handler
    provisionedConcurrency: 5  # Keep 5 instances warm
    events:
      - http:
          path: /api/{proxy+}
          method: any

# Or use Application Auto Scaling for dynamic provisioned concurrency
resources:
  Resources:
    ApiLambdaProvisionedConcurrencyAutoScaling:
      Type: AWS::ApplicationAutoScaling::ScalableTarget
      Properties:
        MaxCapacity: 50
        MinCapacity: 5
        ResourceId: !Sub function:${ApiLambdaFunction}:live
        RoleARN: !GetAtt AutoScalingRole.Arn
        ScalableDimension: lambda:function:ProvisionedConcurrency
        ServiceNamespace: lambda

    ApiLambdaAutoScalingPolicy:
      Type: AWS::ApplicationAutoScaling::ScalingPolicy
      Properties:
        PolicyName: ApiLambdaAutoScalingPolicy
        PolicyType: TargetTrackingScaling
        ScalingTargetId: !Ref ApiLambdaProvisionedConcurrencyAutoScaling
        TargetTrackingScalingPolicyConfiguration:
          TargetValue: 0.7  # Scale when 70% utilized
          PredefinedMetricSpecification:
            PredefinedMetricType: LambdaProvisionedConcurrencyUtilization

Middleware Pattern

Implement a clean middleware pattern for Lambda handlers:

typescript
type LambdaContext = {
  event: any;
  context: Context;
  response?: any;
  error?: Error;
  state: Record<string, any>;
};

type Middleware = (
  ctx: LambdaContext,
  next: () => Promise<void>
) => Promise<void>;

function compose(middlewares: Middleware[]) {
  return async (ctx: LambdaContext) => {
    let index = -1;

    async function dispatch(i: number): Promise<void> {
      if (i <= index) {
        throw new Error('next() called multiple times');
      }
      index = i;

      const fn = middlewares[i];
      if (!fn) return;

      await fn(ctx, () => dispatch(i + 1));
    }

    await dispatch(0);
  };
}

// Middleware implementations
const errorHandler: Middleware = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    ctx.error = error as Error;
    ctx.response = {
      statusCode: 500,
      body: JSON.stringify({
        error: 'Internal Server Error',
        requestId: ctx.context.awsRequestId,
      }),
    };

    // Log error with context
    console.error({
      error: (error as Error).message,
      stack: (error as Error).stack,
      event: ctx.event,
      requestId: ctx.context.awsRequestId,
    });
  }
};

const jsonParser: Middleware = async (ctx, next) => {
  if (ctx.event.body) {
    try {
      ctx.state.body = JSON.parse(ctx.event.body);
    } catch {
      ctx.response = {
        statusCode: 400,
        body: JSON.stringify({ error: 'Invalid JSON' }),
      };
      return;
    }
  }
  await next();
};

const authMiddleware: Middleware = async (ctx, next) => {
  const token = ctx.event.headers?.Authorization?.replace('Bearer ', '');

  if (!token) {
    ctx.response = {
      statusCode: 401,
      body: JSON.stringify({ error: 'Unauthorized' }),
    };
    return;
  }

  try {
    ctx.state.user = await verifyToken(token);
    await next();
  } catch {
    ctx.response = {
      statusCode: 401,
      body: JSON.stringify({ error: 'Invalid token' }),
    };
  }
};

const responseFormatter: Middleware = async (ctx, next) => {
  await next();

  if (ctx.response && !ctx.response.headers) {
    ctx.response.headers = {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    };
  }
};

// Create handler with middleware
function createHandler(
  middlewares: Middleware[],
  handler: (ctx: LambdaContext) => Promise<void>
) {
  const chain = compose([...middlewares, handler]);

  return async (event: any, context: Context) => {
    const ctx: LambdaContext = { event, context, state: {} };
    await chain(ctx);
    return ctx.response;
  };
}

// Usage
export const handler = createHandler(
  [errorHandler, responseFormatter, jsonParser, authMiddleware],
  async (ctx) => {
    const { user } = ctx.state;
    const result = await getUserData(user.id);

    ctx.response = {
      statusCode: 200,
      body: JSON.stringify(result),
    };
  }
);

Connection Pooling with RDS Proxy

Lambda functions can exhaust database connections. Use RDS Proxy or implement connection pooling:

typescript
import { Pool, PoolConfig } from 'pg';
import { Signer } from '@aws-sdk/rds-signer';

// Connection pool - initialized once per container
let pool: Pool | null = null;

async function getPool(): Promise<Pool> {
  if (pool) return pool;

  const signer = new Signer({
    hostname: process.env.DB_HOST!,
    port: 5432,
    username: process.env.DB_USER!,
    region: process.env.AWS_REGION,
  });

  // Generate IAM auth token
  const token = await signer.getAuthToken();

  const config: PoolConfig = {
    host: process.env.DB_HOST,
    port: 5432,
    user: process.env.DB_USER,
    password: token,
    database: process.env.DB_NAME,
    ssl: { rejectUnauthorized: false },
    max: 1, // Lambda best practice: 1 connection per container
    idleTimeoutMillis: 120000,
    connectionTimeoutMillis: 5000,
  };

  pool = new Pool(config);

  // Handle pool errors
  pool.on('error', (err) => {
    console.error('Unexpected pool error:', err);
    pool = null; // Reset pool on error
  });

  return pool;
}

// Wrapper for database operations with retry
async function query<T>(
  sql: string,
  params?: any[],
  retries = 3
): Promise<T[]> {
  const db = await getPool();

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const result = await db.query(sql, params);
      return result.rows;
    } catch (error: any) {
      if (error.code === 'ECONNREFUSED' || error.code === '57P01') {
        // Connection error - reset pool and retry
        pool = null;
        if (attempt === retries) throw error;
        await new Promise(r => setTimeout(r, 100 * attempt));
        continue;
      }
      throw error;
    }
  }

  throw new Error('Query failed after retries');
}

// Clean up on container shutdown
process.on('beforeExit', async () => {
  if (pool) {
    await pool.end();
  }
});

Event-Driven Patterns with EventBridge

Build loosely coupled services using EventBridge:

typescript
import {
  EventBridgeClient,
  PutEventsCommand
} from '@aws-sdk/client-eventbridge';

const eventBridge = new EventBridgeClient({});

interface DomainEvent<T> {
  source: string;
  detailType: string;
  detail: T;
  correlationId?: string;
}

// Event publisher
async function publishEvent<T>(event: DomainEvent<T>): Promise<void> {
  const command = new PutEventsCommand({
    Entries: [{
      Source: event.source,
      DetailType: event.detailType,
      Detail: JSON.stringify({
        ...event.detail,
        metadata: {
          correlationId: event.correlationId || crypto.randomUUID(),
          timestamp: new Date().toISOString(),
          version: '1.0',
        },
      }),
      EventBusName: process.env.EVENT_BUS_NAME,
    }],
  });

  const response = await eventBridge.send(command);

  if (response.FailedEntryCount && response.FailedEntryCount > 0) {
    throw new Error(`Failed to publish event: ${response.Entries?.[0]?.ErrorMessage}`);
  }
}

// Example: Order service publishes events
export async function createOrder(orderData: CreateOrderDTO) {
  const order = await saveOrder(orderData);

  // Publish domain event
  await publishEvent({
    source: 'orders.service',
    detailType: 'OrderCreated',
    detail: {
      orderId: order.id,
      customerId: order.customerId,
      items: order.items,
      total: order.total,
    },
  });

  return order;
}

// Event handler Lambda
export const orderCreatedHandler = async (event: EventBridgeEvent<'OrderCreated', OrderDetail>) => {
  const { detail } = event;
  const correlationId = detail.metadata?.correlationId;

  console.log(`Processing OrderCreated event: ${detail.orderId}`, { correlationId });

  // Process the event
  await sendOrderConfirmationEmail(detail);
  await updateInventory(detail.items);
  await notifyShipping(detail);
};

Lambda Layers for Shared Code

Organize shared code efficiently using Lambda Layers:

yaml
# Directory structure
layers/
  common/
    nodejs/
      node_modules/
      package.json
      utils/
        index.ts
        logger.ts
        metrics.ts

# serverless.yml
layers:
  common:
    path: layers/common
    name: ${self:service}-common-${sls:stage}
    description: Common utilities and dependencies
    compatibleRuntimes:
      - nodejs18.x
      - nodejs20.x
    retain: false

functions:
  api:
    handler: src/handlers/api.handler
    layers:
      - !Ref CommonLambdaLayer
    environment:
      NODE_PATH: /opt/nodejs/node_modules
typescript
// layers/common/nodejs/utils/logger.ts
import { Context } from 'aws-lambda';

interface LogContext {
  requestId: string;
  functionName: string;
  correlationId?: string;
  [key: string]: any;
}

let logContext: LogContext | null = null;

export function initLogger(context: Context, correlationId?: string) {
  logContext = {
    requestId: context.awsRequestId,
    functionName: context.functionName,
    correlationId,
  };
}

function formatLog(level: string, message: string, data?: Record<string, any>) {
  return JSON.stringify({
    timestamp: new Date().toISOString(),
    level,
    message,
    ...logContext,
    ...data,
  });
}

export const logger = {
  info: (message: string, data?: Record<string, any>) =>
    console.log(formatLog('INFO', message, data)),

  warn: (message: string, data?: Record<string, any>) =>
    console.warn(formatLog('WARN', message, data)),

  error: (message: string, error?: Error, data?: Record<string, any>) =>
    console.error(formatLog('ERROR', message, {
      ...data,
      error: error?.message,
      stack: error?.stack,
    })),

  debug: (message: string, data?: Record<string, any>) => {
    if (process.env.LOG_LEVEL === 'DEBUG') {
      console.log(formatLog('DEBUG', message, data));
    }
  },
};

Conclusion

AWS Lambda provides incredible flexibility for building serverless applications. By applying these advanced patterns - middleware composition, connection pooling, event-driven architecture, and proper code organization - you can build maintainable, scalable, and cost-effective serverless systems.

Remember: the key to successful Lambda development is understanding the execution model and optimizing for both cold starts and warm invocations.

Stay Updated

Get notified when I publish new articles on engineering, AI, and leadership. No spam, unsubscribe anytime.

Found this helpful? Share it with others

DP

About the Author

Dibyank Padhy is an Engineering Manager & Full Stack Developer with 7+ years of experience building scalable software solutions. Passionate about cloud architecture, team leadership, and AI integration.