Growing one human
Time to grow our first human: the Waiter of Café Sukoon. Its one job: take orders from customers and announce them. We'll write its DNA as a single CDK construct, then deploy it and talk to it.
The Waiter's body plan
| Body part | Implementation |
|---|---|
| Senses | POST /orders and GET /orders/{orderId} on an HTTP API |
| Skin | zod schema validation |
| Brain | placeOrder business logic (the brain) |
| Memory | Its own DynamoDB table, on-demand, point-in-time recovery |
| Voice | Publishes OrderPlaced to the café's EventBridge bus |
| ID card | An IAM role with access only to its table and the bus |
| Heart | Structured logs, X-Ray tracing, an error alarm |
The brain's helpers
The brain needs a menu to look up prices. For the pilot, a small fixed menu is enough:
// services/waiter/src/memory/menu.ts
import type { Menu } from "../brain/place-order";
const PRICES: Record<string, number> = { chai: 40, coffee: 120, samosa: 30, sandwich: 150 };
export const menu: Menu = {
async priceOf(menuItemId) {
return PRICES[menuItemId];
},
};
The handler for reading an order:
// services/waiter/src/senses/get-order.http.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { orderMemory } from "../memory/order-memory";
export const handler = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {
const order = await orderMemory.recall(event.pathParameters?.orderId ?? "");
return order
? { statusCode: 200, body: JSON.stringify(order) }
: { statusCode: 404, body: JSON.stringify({ error: "Order not found" }) };
};
The place-order.http.ts handler, schema, brain, memory and voice modules are the ones built in the previous chapters.
The Waiter's DNA
// services/waiter/infra/waiter.ts
import * as path from "node:path";
import { Construct } from "constructs";
import { Duration, RemovalPolicy } from "aws-cdk-lib";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as events from "aws-cdk-lib/aws-events";
import * as apigw from "aws-cdk-lib/aws-apigatewayv2";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
export interface WaiterProps {
/** The town square every human announces to */
eventBus: events.IEventBus;
/** The café's front door (shared HTTP API) */
api: apigw.HttpApi;
}
export class Waiter extends Construct {
public readonly memory: dynamodb.Table;
constructor(scope: Construct, id: string, props: WaiterProps) {
super(scope, id);
// 🧠💾 Memory — owned by the Waiter alone
this.memory = new dynamodb.Table(this, "Memory", {
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
timeToLiveAttribute: "expiresAt",
removalPolicy: RemovalPolicy.RETAIN, // never lose memories when the stack is deleted
});
this.memory.addGlobalSecondaryIndex({
indexName: "gsi1",
partitionKey: { name: "gsi1pk", type: dynamodb.AttributeType.STRING },
sortKey: { name: "gsi1sk", type: dynamodb.AttributeType.STRING },
});
// Shared settings for the Waiter's brain cells (Lambda functions)
const cell: Partial<nodejs.NodejsFunctionProps> = {
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
memorySize: 512,
timeout: Duration.seconds(10),
tracing: lambda.Tracing.ACTIVE, // ❤️ follow each thought with X-Ray
environment: {
TABLE_NAME: this.memory.tableName,
EVENT_BUS_NAME: props.eventBus.eventBusName,
POWERTOOLS_SERVICE_NAME: "waiter",
},
bundling: { minify: true, sourceMap: true },
};
// 👂 Senses: place an order
const placeOrder = new nodejs.NodejsFunction(this, "PlaceOrder", {
...cell,
entry: path.join(__dirname, "../src/senses/place-order.http.ts"),
});
// 👂 Senses: recall an order
const getOrder = new nodejs.NodejsFunction(this, "GetOrder", {
...cell,
entry: path.join(__dirname, "../src/senses/get-order.http.ts"),
});
// 🪪 ID card: exactly the permissions each cell needs
this.memory.grantReadWriteData(placeOrder);
this.memory.grantReadData(getOrder);
props.eventBus.grantPutEventsTo(placeOrder); // 🗣️ voice
// 🚪 Connect the senses to the café's front door
props.api.addRoutes({
path: "/orders",
methods: [apigw.HttpMethod.POST],
integration: new HttpLambdaIntegration("PlaceOrderIntegration", placeOrder),
});
props.api.addRoutes({
path: "/orders/{orderId}",
methods: [apigw.HttpMethod.GET],
integration: new HttpLambdaIntegration("GetOrderIntegration", getOrder),
});
// 💓 Pain receptor: alarm when orders fail
placeOrder
.metricErrors({ period: Duration.minutes(5) })
.createAlarm(this, "PlaceOrderErrors", {
threshold: 1,
evaluationPeriods: 1,
alarmDescription: "The Waiter is in pain: orders are failing",
});
}
}
The world the Waiter lives in
// infra/cafe-stack.ts — the society: shared world + humans
import { Stack, StackProps, CfnOutput } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import * as apigw from "aws-cdk-lib/aws-apigatewayv2";
import { Waiter } from "../services/waiter/infra/waiter";
export class CafeStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const townSquare = new events.EventBus(this, "TownSquare"); // 📋 notice board
const frontDoor = new apigw.HttpApi(this, "FrontDoor", { // 🚪 for the outside world
corsPreflight: { allowOrigins: ["*"], allowMethods: [apigw.CorsHttpMethod.ANY], allowHeaders: ["content-type"] },
});
new Waiter(this, "Waiter", { eventBus: townSquare, api: frontDoor }); // 🧍 our first human
new CfnOutput(this, "FrontDoorUrl", { value: frontDoor.apiEndpoint });
}
}
// bin/cafe.ts — the app
import { App } from "aws-cdk-lib";
import { CafeStack } from "../infra/cafe-stack";
const app = new App();
new CafeStack(app, "CafeSukoon-dev", {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});
allowOrigins: ["*"] and an unauthenticated API are fine for a first experiment in a sandbox. Before anyone real uses it, restrict CORS to your UI's domain and add authentication (Cognito + JWT authorizer) — see the immune system.
Birth
cdk synth # check the DNA compiles into a template
cdk diff # see what will be created
cdk deploy # grow the Waiter
The output prints FrontDoorUrl. Talk to your new human:
URL=https://abc123.execute-api.ap-south-1.amazonaws.com # from the output
curl -X POST "$URL/orders" \
-H "content-type: application/json" \
-d '{"tableNumber": 4, "items": [{"menuItemId": "chai", "quantity": 2}, {"menuItemId": "samosa", "quantity": 1}]}'
# → 201 {"orderId":"…","total":110,"status":"PLACED",…}
curl "$URL/orders/<orderId>"
# → 200 {…the remembered order…}
Then look inside the body:
- CloudWatch Logs — the Waiter's diary for each request.
- DynamoDB console — the order in its memory.
- X-Ray — the trace from API Gateway through Lambda to DynamoDB and EventBridge.
One construct, one human: memory, brain cells, senses, ID card and pain receptors grown together from a single piece of DNA. Every other human in the café will follow the same pattern.
Cleaning up
cdk destroy
Because the table has RemovalPolicy.RETAIN, it survives destruction — delete it manually in the console when you're done experimenting.