All Articles
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.

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.