Skip to main content

๐Ÿ› ๏ธ 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:

DomainResponsibility
UsersRegistration, authentication, profiles
ProductsCreate, read, update, delete digital listings
OrdersPurchase flow, payment, delivery of the download
ReviewsRatings and comments from verified buyers
SearchFast full-text discovery across products
AnalyticsSales 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.

flowchart LR M1[Milestone 1
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:

sequenceDiagram participant B as Buyer participant G as API Gateway participant P as Product Service participant O as Order Service participant Pay as Payment Processor B->>G: Search products G->>P: Query index P-->>B: Results B->>G: Purchase product G->>O: Create order O->>Pay: Charge card Pay-->>O: Confirmed O-->>B: Download link

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.

Serverless microservices architecture for the marketplace An API gateway fronts six independent services โ€” auth, products, orders, reviews, search, and analytics โ€” each backed by its own data store, with a queue fanning out to notification functions. API Gateway Auth Products Orders Reviews Search Analytics Products DB Orders DB Search index Queue โ†’ Notify
Figure 1 โ€” One gateway, six services, each owning its own data. Services stay loosely coupled by talking through events and a queue rather than reaching into each other's databases.

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:

ConcernChoiceWhy
GatewayAmazon API GatewayBuilt-in auth, rate limiting, and request validation
AuthAmazon Cognito + JWTManaged sign-up, sign-in, and token issuing
ComputeAWS LambdaScales to zero; pay only per request
DatabaseDynamoDB (on-demand)No capacity planning; low, predictable latency
SearchOpenSearch / ElasticsearchFast full-text queries the query side can lean on
DeployServerless FrameworkInfrastructure-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.

CQRS write and read paths A write command saves to DynamoDB and emits an event; an indexer updates the search index, which the read query serves. Write command DynamoDB Event โ†’ Indexer Search index Read query
Figure 2 โ€” In CQRS the write path and read path diverge. They reconverge eventually via an event-driven indexer, trading strict consistency for read speed.
// 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.

flowchart TD A[Process payment] -->|ok| B[Grant access] A -->|fail| F[Fail order] B -->|ok| C[Notify buyer] B -->|fail| R[Refund payment] C --> D[Complete order] R --> F
{
  "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.yml declaring 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.

DimensionNeeds workSolidExcellent
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

๐Ÿš€ 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.