๐ ๏ธ Weekend Project: Advanced Backend & Apis
This is the module's capstone โ a hands-on build, not a lecture. Over a weekend you'll design and ship the backend for a small digital marketplace, wiring together the microservices, serverless functions, and resilience patterns you met earlier in Module 26. You'll work in clearly staged milestones, tick off a checklist as you go, and measure your result against a concrete "what good looks like" rubric.
๐ฏ Learning Objectives
By the end of this project, you will be able to:
- Decompose a product requirement into independently deployable serverless microservices behind a single API gateway
- Implement a service end-to-end with validation, authentication, and structured error handling in AWS Lambda
- Apply three advanced patterns โ CQRS, the Saga pattern, and the circuit breaker โ to a real workflow
- Evaluate your build against a rubric covering security, performance, cost, and resilience
Estimated Time: 6โ10 hours across a weekend โข Difficulty: Advanced
Hands-on: Ship one complete marketplace service (your choice) to AWS and prove it works end-to-end.
In This Lesson
The Brief
You're building the backend API for a digital marketplace โ a site where people buy and sell digital goods (e-books, design templates, small software tools). It's the perfect capstone because it touches every advanced topic in this module: several independent domains, money changing hands, spiky traffic during sales, and a real need to keep costs down when nobody's buying.
๐ก How to use this lesson. Don't try to build all six services. The goal of a weekend project is a complete, working slice โ one service, shipped and tested โ plus a clear understanding of how the whole system fits together. You'll pick your slice in Milestone 1.
Functionally, the full marketplace needs to handle six domains. You'll design for all of them and build one:
| Domain | Responsibility |
|---|---|
| Users | Registration, authentication, profiles |
| Products | Create, read, update, delete digital listings |
| Orders | Purchase flow, payment, delivery of the download |
| Reviews | Ratings and comments from verified buyers |
| Search | Fast full-text discovery across products |
| Analytics | Sales and performance metrics for sellers |
๐ Key Terms
Microservice: a small, independently deployable service that owns one domain and its data.
Serverless: code (AWS Lambda functions) that runs on demand with no servers to manage โ you pay per request, and it scales to zero when idle.
API gateway: the single front door that routes every incoming request to the right service and handles cross-cutting concerns like auth and rate limiting.
The Milestone Map
The project is organized into five milestones. Each one has a clear deliverable, so you always know where you are and what "done" means. They loosely follow George Pรณlya's timeless problem-solving loop โ understand, plan, do, review โ which keeps you from coding before you know what you're building.
Understand & Scope] --> M2[Milestone 2
Architecture & Plan] M2 --> M3[Milestone 3
Build the Core] M3 --> M4[Milestone 4
Advanced Patterns] M4 --> M5[Milestone 5
Harden & Reflect] M5 -->|Improvements| M1
๐ก Suggested weekend schedule
- Friday evening: Milestones 1โ2 (scope + architecture on paper).
- Saturday: Milestone 3 (get one service working locally, then deployed).
- Sunday: Milestone 4 (add one advanced pattern) and Milestone 5 (harden, test, reflect).
Milestone 1 โ Understand & Scope
Deliverable: a one-page scope note that names your chosen service, its endpoints, and its non-functional targets.
Resist the urge to open your editor. First, get crisp on the problem. Answer these before writing any code:
๐ Scoping questions
- Which single service will you build this weekend? (Products is the friendliest starting point.)
- Who calls it, and what are they trying to accomplish?
- What are the security concerns โ whose data is at risk, and where does money enter the picture?
- What are your non-functional targets? (e.g. p95 latency under 200 ms, scale to zero when idle.)
- What can fail, and what should happen when it does?
To ground the design, sketch the primary buyer journey. Even a rough sequence diagram exposes the data each service needs and where they hand off to each other:
Write down, in a sentence each, your non-functional targets. These become the yardstick you'll measure against in Milestone 5, so make them specific and testable.
Milestone 2 โ Architecture & Plan
Deliverable: an architecture diagram and a chosen tech stack for your service.
Here's the target system. Every domain becomes an independent service behind one API gateway, each owning its own data store. Your weekend focuses on one of these boxes โ but designing the whole picture keeps your service's boundaries honest.
A sensible default stack
You can build this on any cloud, but a fully managed serverless stack keeps the weekend focused on your code, not on running servers. One reasonable set of choices:
| Concern | Choice | Why |
|---|---|---|
| Gateway | Amazon API Gateway | Built-in auth, rate limiting, and request validation |
| Auth | Amazon Cognito + JWT | Managed sign-up, sign-in, and token issuing |
| Compute | AWS Lambda | Scales to zero; pay only per request |
| Database | DynamoDB (on-demand) | No capacity planning; low, predictable latency |
| Search | OpenSearch / Elasticsearch | Fast full-text queries the query side can lean on |
| Deploy | Serverless Framework | Infrastructure-as-code; one command to ship |
Scaffold the project so all your services share one deployable repo:
# Create and initialise the project
mkdir digital-marketplace-api && cd digital-marketplace-api
npx serverless create --template aws-nodejs
# One directory per domain
mkdir -p services/{auth,products,orders,reviews,search,analytics}
# Shared runtime + dev dependencies
npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb jsonwebtoken uuid joi
npm install --save-dev serverless-offline serverless-iam-roles-per-function
โ ๏ธ Modern AWS SDK note
Use AWS SDK v3 (the modular @aws-sdk/* packages), not the old monolithic aws-sdk v2. v3 gives smaller Lambda bundles, first-class async/await, and per-command imports. The code in this lesson uses v3 throughout.
Milestone 3 โ Build the Core
Deliverable: one service running locally with serverless offline, then deployed to AWS and reachable over HTTP.
We'll walk through the Products service as the reference build. It's the best first slice: no payment complexity, but it still exercises validation, auth, a database write, and event publishing.
The infrastructure (serverless.yml)
Declare the runtime, the products table, and the one function that creates a product. Everything is described as code, so your teammate โ or future you โ can reproduce it exactly:
service: digital-marketplace-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs20.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
environment:
PRODUCTS_TABLE: ${self:service}-products-${self:provider.stage}
httpApi:
cors: true
plugins:
- serverless-offline
- serverless-iam-roles-per-function
functions:
createProduct:
handler: services/products/create.handler
events:
- httpApi:
path: /products
method: post
iamRoleStatements:
- Effect: Allow
Action: [dynamodb:PutItem]
Resource: !GetAtt ProductsTable.Arn
resources:
Resources:
ProductsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.PRODUCTS_TABLE}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
- AttributeName: sellerId
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: SellerIdIndex
KeySchema:
- AttributeName: sellerId
KeyType: HASH
Projection:
ProjectionType: ALL
The create handler
Notice the shape every good Lambda handler shares: authenticate, validate, do the work, respond โ with each failure mode mapped to the right HTTP status. Never let a raw exception leak to the caller.
// services/products/create.js โ AWS SDK v3
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { randomUUID } from 'node:crypto';
import Joi from 'joi';
const db = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const schema = Joi.object({
title: Joi.string().max(120).required(),
description: Joi.string().max(5000).required(),
price: Joi.number().positive().required(),
category: Joi.string().required(),
files: Joi.array().items(
Joi.object({
name: Joi.string().required(),
type: Joi.string().required(),
url: Joi.string().uri().required(),
})
).min(1).required(),
});
const json = (statusCode, body) => ({
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
export const handler = async (event) => {
try {
// 1. Authenticate โ the gateway put the verified claims here
const sellerId = event.requestContext?.authorizer?.jwt?.claims?.sub;
if (!sellerId) return json(401, { error: 'Unauthorized' });
// 2. Validate
const { error, value } = schema.validate(JSON.parse(event.body));
if (error) return json(400, { error: 'Validation Error', details: error.details });
// 3. Do the work
const now = new Date().toISOString();
const product = {
id: randomUUID(),
sellerId,
...value,
status: 'active',
createdAt: now,
updatedAt: now,
};
await db.send(new PutCommand({ TableName: process.env.PRODUCTS_TABLE, Item: product }));
// 4. Respond
return json(201, product);
} catch (err) {
console.error('createProduct failed', err);
return json(500, { error: 'Internal Server Error' });
}
};
Run it, then ship it
# Run the whole stack on your laptop
npx serverless offline
# In another terminal, hit the local endpoint
curl -X POST http://localhost:3000/products \
-H 'Content-Type: application/json' \
-d '{"title":"Icon Pack","description":"200 SVGs","price":9,"category":"design","files":[{"name":"pack.zip","type":"application/zip","url":"https://example.com/pack.zip"}]}'
# When it works locally, deploy to the cloud
npx serverless deploy --stage dev
Expected response (201 Created)
{
"id": "6f1c...",
"sellerId": "auth0|...",
"title": "Icon Pack",
"price": 9,
"status": "active",
"createdAt": "2026-08-01T..."
}
When you see that 201, Milestone 3 is done: you have a real, deployed, authenticated endpoint that writes to a database. Everything after this is making it resilient.
Milestone 4 โ Add the Advanced Patterns
Deliverable: at least one advanced pattern implemented and demonstrably working (show it failing gracefully, not just succeeding).
Pick one of the three patterns below to implement this weekend. Each solves a distinct distributed-systems problem. Read all three so you understand the trade-offs, then go deep on one.
Pattern A โ CQRS (separate reads from writes)
Command Query Responsibility Segregation means the code that changes data and the code that reads it are separate, and can even use different data stores. Products are written to DynamoDB (great for single-item writes) but read through a search index (great for full-text queries). A write publishes an event; a small indexer keeps the read side up to date.
// services/products/create.js โ emit an event after the write (CQRS write side)
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
const bus = new EventBridgeClient({});
await bus.send(new PutEventsCommand({
Entries: [{
Source: 'marketplace.products',
DetailType: 'ProductCreated',
Detail: JSON.stringify(product),
EventBusName: 'default',
}],
}));
// A separate indexer Lambda listens for ProductCreated and writes to the search index.
// The read side (search.js) then queries the index, never the write DB.
Pattern B โ Saga (safe multi-step transactions)
An order spans several services โ charge the card, grant access, notify the buyer. You can't wrap that in one database transaction. A saga runs each step as its own local transaction and defines a compensating action to undo it if a later step fails. AWS Step Functions is a natural fit: each state is a Lambda, and Catch blocks route failures to the rollback path.
{
"Comment": "Order processing saga",
"StartAt": "ProcessPayment",
"States": {
"ProcessPayment": {
"Type": "Task",
"Resource": "${ProcessPaymentFunction.Arn}",
"Next": "GrantAccess",
"Catch": [{ "ErrorEquals": ["PaymentFailedError"], "Next": "FailOrder" }]
},
"GrantAccess": {
"Type": "Task",
"Resource": "${GrantAccessFunction.Arn}",
"Next": "NotifyBuyer",
"Catch": [{ "ErrorEquals": ["AccessError"], "Next": "RefundPayment" }]
},
"NotifyBuyer": { "Type": "Task", "Resource": "${NotifyBuyerFunction.Arn}", "Next": "CompleteOrder" },
"CompleteOrder": { "Type": "Task", "Resource": "${CompleteOrderFunction.Arn}", "End": true },
"RefundPayment": { "Type": "Task", "Resource": "${RefundPaymentFunction.Arn}", "Next": "FailOrder" },
"FailOrder": { "Type": "Task", "Resource": "${FailOrderFunction.Arn}", "End": true }
}
}
Pattern C โ Circuit breaker (stop cascade failures)
When a dependency like the search index gets slow or unavailable, hammering it makes things worse. A circuit breaker counts recent failures; once it trips it fails fast (or returns a fallback) for a cooldown period, then cautiously tests the water again. Three states: CLOSED (normal), OPEN (failing fast), and HALF-OPEN (probing recovery).
// lib/circuitBreaker.js โ a small, dependency-free breaker
export class CircuitBreaker {
constructor({ failureThreshold = 5, resetTimeout = 30000, fallback } = {}) {
this.failureThreshold = failureThreshold;
this.resetTimeout = resetTimeout;
this.fallback = fallback;
this.state = 'CLOSED';
this.failures = 0;
this.nextAttempt = 0;
}
async exec(fn, ...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
if (this.fallback) return this.fallback(...args);
throw Object.assign(new Error('Circuit open'), { name: 'CircuitBreakerError' });
}
this.state = 'HALF-OPEN'; // time to probe
}
try {
const result = await fn(...args);
this.failures = 0;
this.state = 'CLOSED';
return result;
} catch (err) {
this.failures++;
if (this.state === 'HALF-OPEN' || this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
throw err;
}
}
}
// services/products/search.js โ read side guarded by the breaker + a DB fallback
import { CircuitBreaker } from '../../lib/circuitBreaker.js';
const breaker = new CircuitBreaker({
failureThreshold: 3,
resetTimeout: 20000,
fallback: scanDynamoForProducts, // degrade to a simple DB scan
});
export const handler = async (event) => {
const term = event.queryStringParameters?.q ?? '';
const products = await breaker.exec(searchOpenSearch, term);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' },
body: JSON.stringify({ products }),
};
};
โ Prove it works
A pattern isn't done when the happy path passes โ it's done when you've demonstrated the failure path. For the circuit breaker, force the search dependency to throw and confirm you get fallback results, not a 500. For the saga, make the payment step fail and confirm the order ends in a clean failed state with no access granted.
Milestone 5 โ Harden & Reflect
Deliverable: a short reflection note scoring your build against your Milestone 1 targets, plus at least two concrete hardening steps applied.
Pรณlya's last step is the one beginners skip: look back. Walk your build through four lenses and note both what you did and what you'd do next.
Security
- Done well: managed auth (Cognito), input validation on every handler, errors that never leak internals, least-privilege IAM per function.
- Next: rate limiting at the gateway, a WAF for common attacks, encryption of sensitive fields at rest.
Performance
- Done well: a read side backed by a search index, short-lived caching headers, indexes matched to your query patterns.
- Next: edge caching with a CDN, right-sized Lambda memory, pagination on every list endpoint.
Cost
- Done well: everything scales to zero; DynamoDB on-demand means no idle capacity to pay for.
- Next: budget alerts, provisioned capacity only where load is predictable, tiered storage for large files.
Resilience
- Done well: your chosen pattern (circuit breaker fallback, or saga compensations) keeps a partial failure from becoming a total outage.
- Next: dead-letter queues for failed events, structured logging with correlation IDs, an alarm on saga failure rate.
โ ๏ธ The honest trade-offs
Microservices and event-driven design buy you independence and resilience โ but they cost you eventual consistency (the search index lags the write DB by moments) and operational complexity (more moving parts to monitor). Patterns like CQRS and Saga are worth their overhead only when the problem genuinely needs them. For a tiny app, a single Lambda and one table is the right call. Naming that trade-off is senior-level thinking.
Completion Checklist
You've finished the weekend project when every box below is ticked. Print it, or copy it into your notes:
โ Definition of done
- โ A one-page scope note naming your service, endpoints, and non-functional targets
- โ An architecture diagram showing your service in the wider system
- โ
serverless.ymldeclaring your function(s) and data store as code - โ A handler that authenticates, validates, acts, and maps every error to a status code
- โ The service runs locally under
serverless offline - โ The service is deployed to AWS and reachable over HTTP
- โ One advanced pattern (CQRS, Saga, or circuit breaker) implemented
- โ The pattern's failure path demonstrated, not just its happy path
- โ At least one automated test covering a success and a failure case
- โ A reflection note scoring the build against your Milestone 1 targets
๐ก Stuck? Read this hint
If you're short on time, cut scope, not quality. A single well-built, well-tested createProduct endpoint with a circuit-breaker-guarded search beats six half-finished services. The circuit breaker is the lightest pattern to demonstrate โ it needs no extra AWS services, just the small class above and a dependency you can force to fail.
โ Reference solution shape
A complete minimal submission is: serverless.yml with a ProductsTable plus createProduct and searchProducts functions; services/products/create.js (the validated write handler); services/products/search.js wrapping the search call in lib/circuitBreaker.js with a DynamoDB-scan fallback; and a test file that (1) posts a valid product and asserts 201, (2) posts an invalid product and asserts 400, and (3) forces the search dependency to throw and asserts fallback results come back with status 200.
What Good Looks Like
Use this rubric to judge your own build honestly. Aim for "Solid" across the board before reaching for "Excellent" in any one row.
| Dimension | Needs work | Solid | Excellent |
|---|---|---|---|
| Correctness | Happy path only; crashes on bad input | Validates input; returns correct status codes | Idempotent writes; handles edge cases explicitly |
| Security | Open endpoint; errors leak stack traces | Auth enforced; validation; clean error bodies | Least-privilege IAM; rate limiting; secrets managed |
| Resilience | One dependency failure takes it all down | One pattern applied; failure path demonstrated | Fallbacks + DLQs + alarms on failure rate |
| Reproducibility | Clicked together in the console by hand | Defined in serverless.yml; one-command deploy |
Multi-stage config; CI deploy; teardown documented |
| Reflection | None | Scores build against stated targets | Names trade-offs and a prioritized next step |
โ The mark of a strong submission
It's not the number of services โ it's whether one service is trustworthy: it authenticates, validates, degrades gracefully, redeploys from code, and comes with a note that honestly weighs what you built against what you'd do next.
Summary & Quiz
๐ Key Takeaways
- Ship a slice, not the whole thing. One complete, tested, deployed service beats six half-built ones.
- Understand before you build. The Pรณlya loop โ scope, plan, build, reflect โ keeps you from coding the wrong thing.
- CQRS splits reads from writes; Saga makes multi-step transactions safe with compensations; the circuit breaker fails fast to stop cascades.
- A pattern is done when its failure path works, not just its happy path.
- Every advanced pattern has a cost. Reach for one only when the problem earns it.
๐ฏ Quick Quiz
Question 1: Why does CQRS let the marketplace use DynamoDB for writes but a search index for reads?
Question 2: An order's payment succeeds but granting the download fails. Which pattern is designed to undo the payment with a compensating action?
Question 3: When the search dependency starts failing, what does a circuit breaker in the OPEN state do?
๐ Further Reading
- microservices.io โ Microservices pattern catalog
- Martin Fowler โ CQRS
- AWS โ The Saga pattern with Lambda & Step Functions
- Serverless Framework documentation
๐ What's Next?
You've built and hardened a backend โ the natural next question is how to prove it keeps working as it grows. Module 27 opens with the principles of software testing: what to test, at what level, and why a good test suite is what lets you refactor and ship with confidence.
๐ Weekend well spent!
You designed a system, shipped a real slice of it, and made it resilient. That's exactly how senior engineers approach a new domain.