Simplifying AWS Lambda Development with a Stateless Stack Pattern
- Mark Kendall
- Sep 10
- 3 min read
Simplifying AWS Lambda Development with a Stateless Stack Pattern
Introduction
When teams scale out with dozens—or even hundreds—of AWS Lambdas, a recurring pain emerges:
• Developers must manage not just their handler code, but also the AWS CDK stack definitions.
• Each Lambda starts looking different, with drift in runtime versions, IAM policies, observability, and tagging.
A stateless stack pattern solves this. Developers write only the Lambda business code, while a shared “stateless” CDK stack wires everything together consistently.
This article explains why this works, how to structure it, and provides code you can use today.
⸻
Why Use a Stateless Stack?
• Focus on business logic: Devs build just the handler. No CDK boilerplate.
• Centralized governance: IAM roles, alarms, log retention, tags, and VPC config applied once in the infra repo.
• Consistency at scale: Every Lambda gets uniform defaults (memory, timeout, tracing, Powertools).
• Easier onboarding: New function = new folder + one manifest entry.
• Future-proofing: Runtime upgrades, observability, or org-wide policy changes happen in one place.
Trade-off: Infra changes (like a new trigger or new permissions) require updates to the shared stack. But that’s by design—it enforces review and safety.
⸻
Repository Structure
/services
/orders
/lambdas
/create-order
handler.ts
package.json
/cancel-order
handler.ts
/infra
functions.manifest.ts
cdk-app.ts
stacks/
stateless-functions.stack.ts
⸻
Lambda Developer Experience
Example Handler
// services/orders/lambdas/create-order/handler.ts
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = event.body ? JSON.parse(event.body) : {};
// Use shared environment variables
const table = process.env.TABLE_NAME;
// Business logic only
return {
statusCode: 201,
body: JSON.stringify({ id: "abc123", ...body })
};
};
That’s it. The developer doesn’t touch CDK.
⸻
Defining Lambdas in a Manifest
// services/orders/infra/functions.manifest.ts
export type LambdaDef = {
name: string;
sourcePath: string;
handler?: string;
http?: { method: "GET"|"POST", path: string };
memoryMb?: number;
timeoutSec?: number;
environment?: Record<string,string>;
};
export const lambdas: LambdaDef[] = [
{
name: "orders-create",
sourcePath: "../../lambdas/create-order",
http: { method: "POST", path: "/orders" },
environment: { TABLE_NAME: "orders" },
timeoutSec: 10
},
{
name: "orders-cancel",
sourcePath: "../../lambdas/cancel-order",
http: { method: "POST", path: "/orders/{id}/cancel" },
timeoutSec: 6
}
];
Each entry is declarative—just enough metadata for the infra to stitch it together.
⸻
The Stateless CDK Stack
// services/orders/infra/stacks/stateless-functions.stack.ts
import { Stack, StackProps, Duration } from "aws-cdk-lib";
import { Construct } from "constructs";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Runtime, Tracing } from "aws-cdk-lib/aws-lambda";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import * as path from "path";
import { lambdas } from "../functions.manifest";
export class StatelessFunctionsStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const api = new apigw.RestApi(this, "OrdersApi", {
deployOptions: { stageName: "dev" }
});
for (const def of lambdas) {
const fn = new NodejsFunction(this, def.name, {
entry: path.join(__dirname, def.sourcePath, "handler.ts"),
handler: def.handler ?? "handler",
runtime: Runtime.NODEJS_20_X,
memorySize: def.memoryMb ?? 512,
timeout: Duration.seconds(def.timeoutSec ?? 8),
tracing: Tracing.ACTIVE,
environment: def.environment ?? {}
});
if (def.http) {
const res = api.root.resourceForPath(def.http.path);
res.addMethod(def.http.method, new apigw.LambdaIntegration(fn));
}
}
}
}
⸻
How
Comments