Serverless Architecture

Run code without managing servers. Functions as a Service, event triggers, cold starts, pay-per-use pricing, managed backends, and the patterns and limits you need to know.

Intermediate⏱ 5 min readLesson 11 of 13#architecture#serverless#lambda#faas#cloud

The big idea

Owning a car means paying for it 24/7, parking it, servicing it, insuring it, even though it sits idle 95% of the time. Taking a taxi means you pay only for the trips you take, and someone else handles the car.

Serverless is the taxi model for computing: you write functions, the cloud runs them when an event happens, scales them automatically (even to zero), and bills you per request and per millisecond. There are still servers; you just never see or manage them.

Serverless: events trigger functions that scale automatically and use managed servicesServerless: events trigger functions that scale automatically and use managed services

Two halves of serverless

What it isExamples
FaaS (Functions as a Service)Your code, as small functions triggered by eventsAWS Lambda, Google Cloud Functions, Azure Functions, Cloudflare Workers, Vercel Functions
BaaS (Backend as a Service)Managed building blocks you use instead of running your ownDynamoDB, S3, Firebase, Supabase, Auth0/Cognito, SQS, EventBridge

A "serverless architecture" usually glues managed services together with small functions.

Event triggers

Drawing diagram…
// AWS Lambda handler: create an order from an HTTP request
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";

const db = DynamoDBDocumentClient.from(new DynamoDBClient({})); // created ONCE per container, reused

export const handler = async (event: { body: string }) => {
  const input = JSON.parse(event.body);
  const order = { id: crypto.randomUUID(), ...input, status: "PENDING", createdAt: new Date().toISOString() };
  await db.send(new PutCommand({ TableName: process.env.ORDERS_TABLE, Item: order }));
  return { statusCode: 201, body: JSON.stringify({ id: order.id }) };
};
// Triggered by an S3 upload: create a thumbnail
export const handler = async (event: S3Event) => {
  for (const record of event.Records) {
    const image = await s3.getObject(record.s3.bucket.name, record.s3.object.key);
    const thumbnail = await sharp(image).resize(200).webp().toBuffer();
    await s3.putObject("thumbnails-bucket", record.s3.object.key + ".webp", thumbnail);
  }
};

How it scales and bills

Drawing diagram…
Always-on serverServerless
Idle cost💰 Pay 24/7✅ ~$0 when nobody uses it
Traffic spikesPlan capacity ahead✅ Scales automatically
Ops workPatching, scaling, monitoring hosts✅ The provider handles it
Cost at constant high load✅ Often cheaper💰 Can get expensive

Cold starts

When no warm instance exists, the platform must start a new one: download code, start the runtime, run your initialisation. That first request is slower: a cold start.

Drawing diagram…

Reduce cold starts: keep bundles small, initialise clients outside the handler (so warm calls reuse them), choose fast-starting runtimes (Node, Go, Rust), use provisioned concurrency for latency-critical paths, or edge runtimes (Cloudflare Workers, Vercel Edge) with near-zero cold starts.

Designing for serverless

PrincipleWhy
Stateless functionsAny instance can handle any request; instances disappear anytime
Small, single-purpose functionsFaster cold starts, independent scaling and permissions
Idempotent handlersEvent sources retry; the same event may arrive twice
Push work to queuesSmooth spikes, retry failures, add dead-letter queues
Watch connection limits1,000 concurrent functions × 1 DB connection each can overwhelm Postgres; use RDS Proxy, Data API, or serverless-friendly DBs
Least-privilege IAM per functionEach function gets only the permissions it needs

A typical serverless web backend

Drawing diagram…

Limits and trade-offs

LimitationTypical value / impact
Max execution time~15 minutes on AWS Lambda, so long jobs need Step Functions, containers or batching
Cold starts100 ms to several seconds, depending on runtime and bundle
Vendor lock-inTriggers, IAM and managed services are provider-specific
Local testing and debuggingHarder; use emulators (SAM, LocalStack) and good tracing
ObservabilityMany small pieces: invest in structured logs, tracing (X-Ray / OpenTelemetry)
Cost at sustained high loadContainers or VMs can be much cheaper

💡 Keep your business logic portable. Put it in plain modules (Hexagonal style) and keep the Lambda handler as a thin driving adapter. Moving to containers later then means writing a new adapter, not rewriting the logic.

// handler.ts: thin adapter
export const handler = async (event: APIGatewayProxyEvent) => {
  const result = await placeOrder.execute(JSON.parse(event.body ?? "{}")); // plain use case
  return { statusCode: 201, body: JSON.stringify(result) };
};

When to use it

✅ Spiky or unpredictable traffic, event processing (uploads, queues, webhooks), scheduled jobs, MVPs and small teams that want zero ops, glue between managed services.

❌ Constant high-throughput workloads, long-running or stateful processes (game servers, WebSocket-heavy apps), ultra-low-latency paths where cold starts hurt, and strict portability requirements.

Key takeaways

  • Serverless = FaaS (functions triggered by events) + BaaS (managed services); no servers to manage.
  • It scales automatically (to zero) and bills per use: great for spiky workloads.
  • Design functions to be stateless, small, idempotent and least-privileged; use queues for resilience.
  • Watch for cold starts, execution time limits, DB connection limits, lock-in and cost at scale.
  • Keep business logic in plain modules and handlers as thin adapters.