Build the mental model
This capstone project doesn't introduce a new tool — it's a worked example of the deployment decision guide the course builds toward: reason out which components are load-bearing and why, rather than starting from a finished diagram.
The scenario: a small subscription SaaS with file uploads, background email sending, and traffic that spikes unpredictably (a product launch, a newsletter mention). Every component below earns its place because of a specific requirement in that sentence.
| Component | Why it's here |
|---|---|
| DNS | Entry point; resolves the domain to the CDN rather than a server directly. |
| CDN | Caches static assets and terminates TLS close to users, reducing origin load. |
| Load balancer | Exists because of the traffic-spike requirement — spreads requests across app instances. |
| App instances | Stateless compute; can scale out horizontally when the load balancer sees more traffic. |
| Database | Every SaaS needs durable storage for subscription and user data — non-negotiable baseline. |
| Cache | Exists because of the traffic-spike requirement — absorbs repeated reads so the database doesn't get hammered. |
| Queue | Exists because of the background-email requirement — defers slow work out of the request path. |
| Object storage | Exists because of the file-upload requirement — durable home for files outside disposable app instances. |
| Monitoring | Non-negotiable at this scale — one place showing whether every component above is healthy. |
Monitoring wraps around everything, because a production system this many components deep needs a single place that shows whether each piece is healthy.
PRODUCTION SaaS ARCHITECTURE
----------------------------
[USERS]
|
v
[DNS] (yourapp.com resolves to the CDN)
|
v
[CDN] (caches static assets, terminates TLS at the edge)
|
v
[LOAD BALANCER] (spreads requests across app instances)
|
+-------------+-------------+
v v v
[APP #1] [APP #2] [APP N]
| | |
+-------------+-------------+
|
+------------+------------+------------+
v v v v
[DATABASE] [CACHE] [QUEUE] [OBJECT STORAGE]
(billing, (Redis: (email (uploaded files,
user data) hot reads) jobs) e.g. S3 bucket)
| | | |
+------------+------------+------------+
|
v
[MONITORING]
(metrics, logs, alerts for
every component above)Connect it to a real scenario
Baseline: DNS + CDN
Every production app needs these regardless of scenario specifics.
Add load balancer + app instances
Driven by the traffic-spike requirement — a single instance can't survive a spike.
Add database + cache
Database for subscription/user data; cache protects it from repeated-read pressure during a spike.
Add queue only if background work exists
Justified purely by the background-email requirement in scope.
Add object storage only if uploads exist
Justified purely by the file-upload requirement in scope.
Finish with monitoring across every component
Uptime/latency, query performance, queue depth, storage and cache metrics — non-negotiable at this scale.
Notice what didn't get added: nothing here exists because "it's best practice" in the abstract. Each piece traces to one line in the scenario.
Try the working example
function designArchitecture({ hasFileUploads, hasBackgroundEmail, expectsTrafficSpikes }) {
const components = [
{
name: "Load balancer + multiple app instances",
needed: expectsTrafficSpikes,
reason: "distributes traffic across replicas so a spike doesn't overwhelm a single server",
},
{
name: "CDN in front of the app",
needed: expectsTrafficSpikes,
reason: "caches static assets at edge locations, cutting origin load during a spike",
},
{
name: "Object storage (e.g. S3-compatible bucket)",
needed: hasFileUploads,
reason: "uploaded files need durable storage outside the app servers, which are disposable",
},
{
name: "Background job queue + worker process",
needed: hasBackgroundEmail,
reason: "sending email during the request would block the response; a queue defers it",
},
{
name: "Cache layer (e.g. Redis)",
needed: expectsTrafficSpikes,
reason: "absorbs repeated reads so the database isn't hit for every request during a spike",
},
{
name: "Managed production database",
needed: true,
reason: "every SaaS app needs durable, backed-up storage for user and subscription data",
},
{
name: "Monitoring and alerting",
needed: true,
reason: "you need to know about failures before your users report them, spike or not",
},
];
const chosen = components.filter((c) => c.needed);
console.log(`Scenario: uploads=${hasFileUploads}, email=${hasBackgroundEmail}, spikes=${expectsTrafficSpikes}`);
console.log(`Components needed: ${chosen.length} of ${components.length}\n`);
for (const c of chosen) {
console.log(`- ${c.name}\n reason: ${c.reason}`);
}
return chosen.map((c) => c.name);
}
designArchitecture({ hasFileUploads: true, hasBackgroundEmail: true, expectsTrafficSpikes: true });Scenario: uploads=true, email=true, spikes=true
Components needed: 7 of 7
- Load balancer + multiple app instances
reason: distributes traffic across replicas so a spike doesn't overwhelm a single server
- CDN in front of the app
reason: caches static assets at edge locations, cutting origin load during a spike
- Object storage (e.g. S3-compatible bucket)
reason: uploaded files need durable storage outside the app servers, which are disposable
- Background job queue + worker process
reason: sending email during the request would block the response; a queue defers it
- Cache layer (e.g. Redis)
reason: absorbs repeated reads so the database isn't hit for every request during a spike
- Managed production database
reason: every SaaS app needs durable, backed-up storage for user and subscription data
- Monitoring and alerting
reason: you need to know about failures before your users report them, spike or not5-minute try-it
Take the designArchitecture function and re-run it with hasFileUploads: false, hasBackgroundEmail: false, expectsTrafficSpikes: false — a tiny internal tool with steady, predictable traffic. Which components disappear, and does the resulting list still make sense as a real architecture?
One important caution
Adding every component from the full diagram to a small, low-traffic app 'just in case' — a queue or a separate cache layer nobody needs yet, adding cost and operational surface for no real benefit.
Skipping monitoring until after the first production incident, instead of wiring up basic uptime and error alerts from day one.
AWS Well-Architected Framework — Cloud & Deployment