Lambda Test Events as Contracts: Schema-Locked Fixtures Across Teams

Lambda Test Events as Contracts: Schema-Locked Fixtures Across Teams

Console “test events” rot the moment the producer team renames a field. Treat Lambda test events as versioned contracts — JSON Schema + golden fixtures checked into git — so local invokes, SAM/CDK tests, and CI all hit the same production shapes.

⚡ TL;DR: Store shareable fixtures under events/ with $schema + SemVer; validate every fixture in CI before packaging; generate TypeScript types from the schema; reject console-only events that never land in git. Pair with Lambda Timeouts, Retries, and DLQs and Lambda Warm Pools so retry paths and warm handlers see identical payloads.

Own one schema per event family

// events/schemas/order.created.v1.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://acme.dev/events/order.created.v1.json",
  "title": "order.created",
  "type": "object",
  "required": ["version", "orderId", "tenantId", "totalCents"],
  "properties": {
    "version": { "const": "1" },
    "orderId": { "type": "string", "format": "uuid" },
    "tenantId": { "type": "string", "minLength": 1 },
    "totalCents": { "type": "integer", "minimum": 0 },
    "correlationId": { "type": "string" }
  },
  "additionalProperties": false
}

❌ Copying a one-off console payload into Slack with no schema. ✅ Bumping v2 when fields change and keeping v1 fixtures until consumers migrate.

Lock fixtures in CI

// scripts/assert-event-fixtures.ts
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const ajv = new Ajv({ allErrors: true, strict: true });
addFormats(ajv);

const root = "events";
for (const family of readdirSync(root)) {
  const schema = JSON.parse(readFileSync(join(root, family, "schema.json"), "utf8"));
  const validate = ajv.compile(schema);
  for (const file of readdirSync(join(root, family, "fixtures"))) {
    const doc = JSON.parse(readFileSync(join(root, family, "fixtures", file), "utf8"));
    if (!validate(doc)) {
      throw new Error(`${family}/${file}: ${ajv.errorsText(validate.errors)}`);
    }
  }
}
console.log("all fixtures schema-ok");

Wire this into the same pipeline that runs sam local invoke / CDK assertions so a bad fixture fails the PR, not production.

Share across teams without Slack archaeology

# .github/workflows/event-contracts.yml
name: event-contracts
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: corepack enable && pnpm i --frozen-lockfile
      - run: pnpm tsx scripts/assert-event-fixtures.ts
      - run: pnpm --filter @acme/events build   # emits .d.ts from schema

Publish @acme/events as an internal package so producer and consumer Lambdas import the same types. See also Spec-First AI Development for the OpenAPI twin of this discipline.

Console and local must load git fixtures

# ✅ Invoke with repo fixture
aws lambda invoke \
  --function-name orders-handler \
  --payload file://events/order.created/fixtures/happy-path.json \
  /tmp/out.json

# ❌ Hand-edited console event never committed

Document a team rule: console test events are disposable scratch; only events/** is source of truth. Rotate owners via CODEOWNERS on events/.

Closing checklist

✅ Dos
– ✅ JSON Schema + SemVer per event family
– ✅ CI validates every fixture before merge
– ✅ Generate types; share via internal package
– ✅ Invoke local/CI/console from the same files
– ✅ Keep vN fixtures until all consumers cut over

❌ Don’ts
– ❌ Don’t treat console events as documentation
– ❌ Don’t allow additionalProperties: true on money/tenant fields
– ❌ Don’t delete v1 schemas the day v2 lands
– ❌ Don’t skip negative fixtures (missing tenant, bad UUID)

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply