‹ Juan Valentin Alducin Projects
Deep diveAWS serverlessStep FunctionsSDD

Pyzzeria: live order tracking on AWS serverless for $0 idle

2026-09-20 · 7 min read · Problem → Approach → Architecture → Decisions → Result
🍕 Demo retired★ 100% SDD/OpenSpecPublic repository

Note: the live demo was retired (Nov 2026); the code and this write-up remain.

Problem

I wanted a portfolio demo that a recruiter can use in under two minutes — not a README — and that exercises the parts of AWS I operate every day: API Gateway, Lambda, DynamoDB and Step Functions. The constraints were strict on purpose:

Approach

Treat the order lifecycle as a state machine and let AWS run it. The API only does two things when an order is placed: persist it in DynamoDB and start a Step Functions Express execution. From there, a chain of Wait → Task states advances the order — recibido → preparando → horno → listo → entregado — and each transition invokes a small Lambda that updates the row and pushes the new status to every browser subscribed to that order over a WebSocket API. The frontend is plain HTML/CSS/JS on S3 + CloudFront; there is no polling. The whole project was built with Spec-Driven Development: proposal, specs, design and tasks live in openspec/ in the public repo.

~70 sfrom order placed to delivered (5 states)
$0idle cost — pay-per-request only
1SAM template for every AWS resource
0polling — status arrives over WebSocket

Architecture

Pyzzeria architecture A vanilla JS frontend on S3 and CloudFront calls an API Gateway HTTP API backed by a FastAPI Lambda through Mangum. The Lambda writes orders to DynamoDB and starts a Step Functions Express workflow. The workflow alternates Wait and Task states; each Task invokes a status-update Lambda that writes the new status to DynamoDB and posts it to subscribed browsers through the API Gateway WebSocket API. A second DynamoDB table with TTL stores WebSocket connections. Everything is declared in one AWS SAM template. Browser — vanilla JS SPA S3 (OAC) + CloudFront · 4 screens POST /api/orders API Gateway · HTTP API menu · orders · OpenAPI/Swagger Lambda · FastAPI + Mangum Python 3.12 · pytest + moto Order tracker screen recibido → preparando → horno → listo → entregado PutItem DynamoDB · orders on-demand · key orderId StartExecution Step Functions · Express workflow Wait15 s Taskpreparando Wait12 s Taskhorno … → listo → entregado (timings in the ASL) invoke {orderId, newStatus} Lambda · status_update UpdateItem · query · push UpdateItem status query subscribers DynamoDB · ws-connections connectionId ↔ orderId · TTL cleanup post_to_connection API Gateway · WebSocket $connect · $disconnect · push wss:// subscribe ↓ · push ↑ {orderId, status, estimatedSeconds}

Figure 1 — Request path (solid) and real-time push path (dashed). One SAM template declares all 20+ resources: two DynamoDB tables, four Lambdas, HTTP + WebSocket APIs, the state machine, S3 and CloudFront.

Reading it left to right: the browser posts an order to the HTTP API; the FastAPI Lambda validates it against the menu, stores it in pyzzeria-orders and starts the Express workflow with the orderId. The browser opens a WebSocket and subscribes to that order; the $connect handler stores the connection in pyzzeria-ws-connections with a TTL so abandoned tabs clean themselves up. Every Task state invokes status_update, which writes the new status and pushes it to each live connection through the API Gateway management API — a GoneException deletes stale connections on the fly.

Decisions

D1 · Step Functions Express as the "kitchen clock"

Why: the order lifecycle is a timed sequence, and encoding it as Wait → Task states makes it visible, adjustable and free of sleeping Lambdas. Alternative rejected: a single long-running Lambda with sleep() — it bills for idle time and breaks at the 15-minute limit; or EventBridge Scheduler per transition — more moving parts for the same result. Express (not Standard) because executions are short, high-volume and pay per transition.

D2 · FastAPI behind Mangum instead of hand-written handlers

Why: the same app runs locally with uvicorn and on Lambda unchanged, and FastAPI generates the OpenAPI/Swagger documentation for free — the API is readable without the UI. Trade-off: a slightly bigger cold start than a bare handler; acceptable for a demo where the first request warms it.

D3 · WebSocket push, not polling

Why: polling every second from a browser tab is the cheapest way to leave the Free Tier. API Gateway's WebSocket API keeps the connection, the connections table keeps the mapping, and a status change costs exactly one post_to_connection per subscriber.

D4 · DynamoDB on-demand with two tables and TTL

Why: no instance to pay for, and the access pattern is trivial (get/update by orderId, query connections by order). TTL on the connections table is the garbage collector: no cron, no cleanup Lambda.

D5 · Everything in one SAM template, frontend on S3 + CloudFront with OAC

Why: sam build && sam deploy reproduces the whole stack; the bucket is private and only CloudFront (Origin Access Control) can read it. The frontend is vanilla JS on purpose — it is the same discipline as this portfolio: no build step, no framework, nothing to maintain.

D6 · Scope frozen by the spec

Why: the OpenSpec proposal lists what v1 does not do — authentication, real payments, multiple kitchens, cancellations — so the demo stays honest and finishes. Tests (pytest + moto, ~95% coverage of the backend) cover the menu, the order flow and the Swagger contract.

Result

Written by Juan Valentin Alducin Vázquez · Get in touch ›