CMPUT-499 · Lesson 5 of 12 · 30 min
Architecture: structure, decisions, data and threats
Architecture is the set of decisions that are expensive to change later: how the system is divided into parts, which technologies those parts use, how they communicate, where data lives and how the system is deployed. CMPUT-301 introduced architectural styles and design principles, CMPUT-404 covered web application architecture, CMPUT-291 covered data modelling and CMPUT-450 showed what changes when a system is spread across machines. In the capstone, architecture has a specific job: produce a design that the team can build in ten weeks, that meets the non-functional requirements from lesson 4, and that a small non-profit can afford to run and maintain after you leave.
The most common architectural mistake in capstones is over-engineering. Student teams choose microservices, message queues, Kubernetes and three databases because those are what large companies use and what job postings mention. Then they spend half the term on infrastructure and ship a fragile system the client cannot run. The co-op has about 140 drivers and a few hundred shifts a month. That load could be served by a modest laptop. The right architecture for RescueRoute is boring: one web application, one background worker, one managed database, on a hosting platform that handles servers, certificates and backups. Choose boring technology unless you have a specific reason not to, and write that reason down.
Describing the architecture with C4
Architecture must be communicated to the client, the supervisor, the review panel and future maintainers, and each needs a different level of detail. The C4 model, created by Simon Brown, describes software at four levels of zoom: system context, containers, components and code. The first two are enough for most capstones. A context diagram shows the system as one box, the people who use it and the external systems it depends on; anyone, including the client, can read it. A container diagram zooms into the system and shows the separately deployable parts, such as the web application, worker and database, with the technologies and protocols between them.
Diagrams can also be kept as text in the repository, which keeps them next to the code and lets them change through pull requests like any other file. Several tools render text descriptions into C4 diagrams. Even plain text, as below, is better than a diagram that exists only in someone's slide deck and drifts out of date. Whatever the format, every box should say what it is and what technology it uses, and every arrow should say what it carries and how.
System context (C4 level 1)
[Person] Volunteer driver --uses (phone browser)--> [System] RescueRoute
[Person] Coordinator --uses (laptop)---------> [System] RescueRoute
[Person] Director --views reports--------> [System] RescueRoute
[System] RescueRoute --sends email via--> [External] Email delivery service
[System] RescueRoute --sends texts via--> [External] SMS provider (optional, feature flag)
Containers (C4 level 2)
[Container] Web application Python, FastAPI, server-rendered HTML + small JS
[Container] Background worker Python, sends reminders on a schedule
[Container] Database Managed PostgreSQL
Web application --reads/writes SQL--> Database
Background worker --reads/writes SQL--> Database
Background worker --HTTPS API--> Email service / SMS providerRecording decisions with ADRs
An architecture decision record, or ADR, is a short document recording one significant decision: the context that forced a choice, the options considered, the decision and its consequences, good and bad. ADRs were popularized by Michael Nygard in 2011 and are now common practice. They live in the repository, are numbered, and are never deleted: a decision that is reversed gets a new ADR, and the old one is marked superseded. For a capstone, ADRs do three jobs. They force the team to consider alternatives rather than defaulting to what one person knows. They give the review panel evidence of reasoning. And they tell the person who maintains the system in two years why it is the way it is.
# ADR 0003: Use a managed PostgreSQL database
Status: Accepted (2027-02-05)
Deciders: Marcus (technical lead), Ethan (DevOps), Priya (client lead)
## Context
RescueRoute stores volunteers, shifts, donors, agencies and pickups. Data is
relational (a shift has one donor, one agency and at most one driver) and the
funders' report needs grouping and sums. The co-op has no IT staff and a small
monthly budget. Backups must happen without anyone remembering to run them.
## Options considered
1. SQLite file on the application server: free, simple; backups and concurrent
writes are our problem; lost if the server is replaced.
2. Managed PostgreSQL on the hosting provider's free or entry tier: automatic
daily backups and point-in-time restore; small monthly cost above the free tier.
3. A document database: flexible schema; the report queries become awkward.
## Decision
Option 2. Relational queries fit the data, and managed backups remove the
riskiest manual task from a volunteer-run system.
## Consequences
+ Daily backups and restore without our scripts.
+ The same database engine in development (Docker) and production.
- A recurring cost once the free tier ends; documented in the handover guide.
- Migrations must be run on deploy; added to the pipeline (ADR 0005).Team Relay wrote seven ADRs during the term. The first recorded the decision to keep ADRs at all. Others chose Python with the FastAPI framework because three members knew Python well and the co-op's most likely future volunteer maintainer did too; chose server-rendered pages with a small amount of JavaScript over a single-page application, for speed on older phones and simpler accessibility; chose managed PostgreSQL; and moved database migrations into the deployment pipeline. One ADR was superseded: ADR 0004 chose text messages as the primary reminder channel, and ADR 0007 replaced it with email first and text messages behind a feature flag, after cost and consent questions came up in sprint 2.
# Validate architecture decision records: required sections present, a status that is
# one of the allowed values, and every superseded ADR pointing at its replacement.
import re
adrs = {
"0001-record-decisions": "Status: Accepted\n## Context\nx\n## Decision\nx\n## Consequences\nx",
"0002-python-fastapi": "Status: Accepted\n## Context\nx\n## Options considered\nx\n## Decision\nx\n## Consequences\nx",
"0003-managed-postgres": "Status: Accepted\n## Context\nx\n## Options considered\nx\n## Decision\nx\n## Consequences\nx",
"0004-sms-provider": "Status: Superseded by 0007\n## Context\nx\n## Decision\nx\n## Consequences\nx",
"0005-migrations-in-pipeline": "Status: Proposed\n## Context\nx\n## Decision\nx",
"0006-server-rendered-pages": "Status: accepted-ish\n## Context\nx\n## Decision\nx\n## Consequences\nx",
"0007-email-first-reminders": "Status: Accepted\n## Context\nx\n## Options considered\nx\n## Decision\nx\n## Consequences\nx",
}
REQUIRED = ["## Context", "## Decision", "## Consequences"]
STATUS = r"^Status: (Proposed|Accepted|Deprecated|Superseded by (\d{4}))$"
for name, text in adrs.items():
problems = [f"missing {h[3:]}" for h in REQUIRED if h not in text]
m = re.search(STATUS, text, re.M)
if not m:
problems.append("status must be Proposed, Accepted, Deprecated or Superseded by NNNN")
elif m.group(2) and not any(k.startswith(m.group(2)) for k in adrs):
problems.append(f"superseded by {m.group(2)}, which does not exist")
if "## Options considered" not in text:
problems.append("tip: list the options you rejected")
print(f"{name:30} {'; '.join(problems) if problems else 'OK'}")0001-record-decisions tip: list the options you rejected 0002-python-fastapi OK 0003-managed-postgres OK 0004-sms-provider tip: list the options you rejected 0005-migrations-in-pipeline missing Consequences; tip: list the options you rejected 0006-server-rendered-pages status must be Proposed, Accepted, Deprecated or Superseded by NNNN; tip: list the options you rejected 0007-email-first-reminders OK
Choosing a stack
Choose technologies by matching them to the team, the client and the requirements, in that order. What does the team already know well enough to be productive in week 6? What can the client's future maintainers support? Which options meet the non-functional requirements, especially cost, accessibility and maintainability? Popularity matters only through those questions: a widely used framework has more documentation and more people who can maintain it. Novelty matters only if learning it is a stated learning goal, and even then limit the team to one new major technology. A team learning a new language, a new framework and a new cloud platform at once spends the term learning and not shipping.
| Decision | Choice | Main reason | Rejected alternative |
|---|---|---|---|
| Language and framework | Python, FastAPI | Team skills; likely maintainers know Python | Node.js with a new framework nobody knew |
| User interface | Server-rendered HTML, small JavaScript | Fast on old phones; simpler accessibility | Single-page application |
| Database | Managed PostgreSQL | Relational data; automatic backups | SQLite file on the server |
| Hosting | Platform-as-a-service, entry tier | No servers to patch; low cost | Self-managed virtual machine |
| Reminders | Email first; SMS behind a flag | Cost and consent; SMS optional | SMS only |
| Authentication | Email or phone with one-time links, plus passwords for coordinators | Drivers forget passwords; accessible authentication | Social login only |
Data model and API design
Model the data before designing screens in detail, because the data outlives every interface built on it. Start from the nouns in the user stories: volunteer, shift, donor, agency, pickup. Decide what each holds, how they relate and which constraints the database should enforce rather than leaving them to application code. CMPUT-291 covered entity-relationship modelling and normalization. The capstone lesson is to use database constraints, such as foreign keys, NOT NULL and CHECK constraints, as a safety net. Application bugs come and go; a constraint that rejects a negative weight or a shift without a donor keeps bad data out no matter which code path tried to insert it.
# RescueRoute's core data model in SQL, exercised with SQLite from the standard library.
# Production uses PostgreSQL; the tables and the dashboard query are the same.
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE volunteer (id INTEGER PRIMARY KEY, display_name TEXT NOT NULL,
contact TEXT NOT NULL, reminder_channel TEXT NOT NULL CHECK (reminder_channel IN ('email','sms')));
CREATE TABLE donor (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE agency (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE shift (id INTEGER PRIMARY KEY, donor_id INTEGER NOT NULL REFERENCES donor(id),
agency_id INTEGER NOT NULL REFERENCES agency(id), starts_at TEXT NOT NULL,
driver_id INTEGER REFERENCES volunteer(id), confirmed INTEGER NOT NULL DEFAULT 0);
CREATE TABLE pickup (shift_id INTEGER PRIMARY KEY REFERENCES shift(id), kg REAL NOT NULL CHECK (kg >= 0));
""")
db.executemany("INSERT INTO donor VALUES (?, ?)", [(1, "Maple Grocery"), (2, "Sunrise Bakery"), (3, "Corner Deli")])
db.executemany("INSERT INTO agency VALUES (?, ?)", [(1, "Eastside Shelter"), (2, "Community Kitchen")])
db.executemany("INSERT INTO volunteer VALUES (?, ?, ?, ?)",
[(1, "Ruth P.", "ruth@example.org", "email"), (2, "Omar H.", "+1-555-0100", "sms")])
shifts = [(1, 1, 1, "2027-03-08 09:00", 1, 1), (2, 2, 2, "2027-03-08 15:00", None, 0),
(3, 3, 1, "2027-03-09 10:00", 2, 0), (4, 1, 2, "2027-03-10 09:00", None, 0),
(5, 2, 1, "2027-03-11 15:00", 1, 1)]
db.executemany("INSERT INTO shift VALUES (?, ?, ?, ?, ?, ?)", shifts)
# The coordinator dashboard: shifts in the next days that are unfilled or unconfirmed.
rows = db.execute("""
SELECT s.starts_at, d.name AS donor, a.name AS agency,
CASE WHEN s.driver_id IS NULL THEN 'Unfilled'
WHEN s.confirmed = 0 THEN 'Unconfirmed' ELSE 'Confirmed' END AS status
FROM shift s JOIN donor d ON d.id = s.donor_id JOIN agency a ON a.id = s.agency_id
WHERE s.starts_at BETWEEN '2027-03-08' AND '2027-03-15' AND (s.driver_id IS NULL OR s.confirmed = 0)
ORDER BY s.starts_at""").fetchall()
for r in rows:
print(f"{r[0]} {r[1]:15} -> {r[2]:18} {r[3]}")
try:
db.execute("INSERT INTO pickup VALUES (1, -4)")
except sqlite3.IntegrityError as e:
print("rejected bad data:", e)2027-03-08 15:00 Sunrise Bakery -> Community Kitchen Unfilled 2027-03-09 10:00 Corner Deli -> Eastside Shelter Unconfirmed 2027-03-10 09:00 Maple Grocery -> Community Kitchen Unfilled rejected bad data: CHECK constraint failed: kg >= 0
If the system exposes an API, to its own JavaScript or to other systems, design it deliberately. CMPUT-404 covered REST design. For a capstone the essentials are: consistent resource names (plural nouns such as /shifts and /shifts/42/claim), correct use of HTTP methods and status codes, validation of every input on the server, pagination for lists, and versioning only if outside clients will depend on it. Write the API down, for example as an OpenAPI description that many frameworks can generate automatically, so the front-end and back-end work can proceed in parallel and tests can be written against the contract.
Non-functional requirements and SLOs
Each non-functional requirement from lesson 4 should map to an architectural decision or a monitoring plan. The performance requirement of pages under two seconds on a phone led to server-rendered pages and small assets. The maintainability requirement led to a single repository, a single language and a platform-as-a-service. The availability requirement led to a service level objective, or SLO, a target for reliability that the team can measure. CMPUT-401 and ITIL-141 covered SLOs and error budgets in depth. Team Relay's SLO was that 99 per cent of requests to the main pages succeed within two seconds, measured monthly; lesson 10 shows how they measured it during the pilot.
Be honest about what the architecture does not provide. RescueRoute has a single web application instance on an entry-level plan. If the hosting provider's region fails, the site goes down until it recovers, and that is acceptable for a volunteer scheduling tool whose fallback is the phone. Writing this down in an ADR, with the client's agreement, is better than pretending to high availability that costs money the co-op does not have. Distributed systems techniques from CMPUT-450, such as replication and consensus, are the right tools for other problems; here they would add cost and complexity with no benefit to the client.
Threat modelling
Threat modelling asks what could go wrong from a security point of view, before the system is built. CMPUT-331 covered it in depth. A lightweight version fits a capstone: draw the data flows (the container diagram works), mark the trust boundaries where data crosses from one level of trust to another, such as from a phone browser to the server, and walk through the STRIDE categories for each element: spoofing, tampering, repudiation, information disclosure, denial of service and elevation of privilege. For each credible threat, record a mitigation. Spend an hour on it in week 5 and revisit it when the design changes.
| STRIDE category | Threat to RescueRoute | Mitigation |
|---|---|---|
| Spoofing | Someone signs in as a driver using a guessed or stolen link | Single-use sign-in links that expire in 15 minutes; rate limiting |
| Tampering | A driver edits a request to claim a shift for someone else | Server checks that the signed-in user is the one claiming |
| Repudiation | A coordinator denies cancelling a shift | Audit log of shift changes with who and when |
| Information disclosure | Volunteer phone numbers visible to other drivers | Drivers see first names only; contact details visible to coordinators |
| Denial of service | A script floods the sign-in form | Rate limits and the platform's basic protection |
| Elevation of privilege | A driver reaches the coordinator dashboard | Role check on every coordinator route; tests for each role |
The design review
At the end of week 5, Team Relay presented their architecture to a review panel of two instructors and a volunteer software developer from industry. They had 15 minutes to present and 15 for questions. They showed the context and container diagrams, walked through the data model with the dashboard query, summarized their ADRs, showed how each non-functional requirement was addressed, and presented the STRIDE table. The panel's hardest questions were about operations, not code: who restores the database if it is corrupted, who renews the domain name, what happens when the free tier ends. Those questions led directly to the handover work in lesson 10.
Worked examples
Worked problem
Choosing between two databases
A team must choose between a SQLite file on the app server and a managed PostgreSQL database for a small non-profit. Name the deciding factor.
Show solution
- Both can handle the load of a few hundred records a day.
- The client has no IT staff, so backups and recovery must happen automatically.
- A SQLite file on a replaceable server needs custom backup scripts; the managed database provides backups and point-in-time restore.
Answer: Automatic backups and recovery without staff, recorded in an ADR together with the cost consequence.
Worked problem
Finding a STRIDE threat
The URL /shifts/42/claim lets the signed-in driver claim shift 42. What could go wrong, and how is it prevented?
Show solution
- Tampering or elevation: a request might claim on behalf of another driver if the server trusts a driver ID sent by the browser.
- The server must take the driver identity from the session, never from the request body.
- Add a test: a request with another driver's ID still claims for the signed-in user or is rejected.
Answer: Never trust identity sent by the client; derive it from the authenticated session and test it.
Worked problem
Writing an ADR consequence
The team chose server-rendered pages. Write one positive and one negative consequence.
Show solution
- Positive: fast first load on older phones and simpler accessibility, since content works without JavaScript.
- Negative: highly interactive features, such as drag-to-reschedule, need extra JavaScript and a keyboard alternative.
Answer: For example: "+ fast on old phones; - rich interactions need extra work."
Common mistakes
MistakeUse microservices and Kubernetes for a small client application.
InsteadChoose the simplest architecture that meets the requirements; record the reasoning in ADRs.
MistakeKeep architecture only in slides.
InsteadKeep diagrams and ADRs as text in the repository, changed through pull requests.
MistakeDelete an ADR when a decision changes.
InsteadWrite a new ADR and mark the old one superseded.
MistakeRely on application code alone to keep data valid.
InsteadAdd foreign keys, NOT NULL and CHECK constraints in the database.
MistakeSkip threat modelling because the project is small.
InsteadSpend an hour on STRIDE against the container diagram; small systems hold real personal data.
Summary and key terms
- Architecture is the set of decisions that are expensive to change; for a capstone, prefer boring technology.
- C4 context and container diagrams communicate the design to clients, panels and maintainers.
- ADRs record context, options, decision and consequences, and are superseded rather than deleted.
- Choose a stack by team skills, client maintainability and requirements; learn at most one major new technology.
- Model data from the stories and let database constraints keep bad data out.
- Map each non-functional requirement to a decision or a measurable SLO, and state what the design does not provide.
- A one-hour STRIDE review of the data flows finds the threats worth mitigating.
- Architecture
- The structural decisions about a system that are costly to change later.
- C4 model
- A way of describing software at four levels: context, containers, components and code.
- Container (C4)
- A separately deployable or runnable part of a system, such as a web app or database.
- Architecture decision record
- A short document recording one significant decision, its options and consequences.
- Service level objective
- A measurable reliability target, such as 99 per cent of requests succeeding within 2 seconds.
- Threat model
- An analysis of how a system could be attacked and how to mitigate the threats.
- STRIDE
- Spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege.
- Trust boundary
- A point where data passes between parts of a system with different levels of trust.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Draw, in words, a C4 context diagram for a library book-reservation app.
Show answer
Users: patrons and librarians; system: reservation app; external systems: the library catalogue and an email service.
List the sections of an ADR.
Show answer
Title, status, context, options considered, decision and consequences.
Give one CHECK constraint for a shift table.
Show answer
For example, end time after start time, or a status limited to allowed values.
Name a STRIDE threat for a password reset feature and a mitigation.
Show answer
Spoofing via a guessed reset link; use long random single-use tokens that expire.
Why might a team prefer server-rendered pages for older phones?
Show answer
Less JavaScript to download and run, so pages load faster and work more reliably.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.