Non‐Functional Requirements Demo 2 - COS301-SE-2026/Mealchemy GitHub Wiki
3.1.5 Non-Functional Requirements
Non-functional requirements, or quality requirements, describe how the system performs rather than what it does. Each requirement below has a target, the design decision that delivers it, and the evidence available for it.
Five quality requirements are targeted: Performance, Scalability, Security, Reliability and Availability, and Maintainability.
QR1: Performance
| # | Requirement | Target |
|---|---|---|
| QR1.1 | Response time for user requests under normal load | Under 2 seconds for 95% of requests |
| QR1.2 | Concurrent users supported | At least 500 |
How the architecture supports this
Application and database are in the same region. Cloud Run runs in europe-west3 (Frankfurt) and the Neon database in eu-central-1. Placing them together removes cross-region network latency from every database call. If the database were in a different region, every query would add tens of milliseconds before any work is done.
No session lookup on each request. Login state is carried in the JWT and verified by checking its signature. The server does not look up a session record to identify the caller, so authentication costs no database call.
Database connections are released early. Connections are returned to the pool when the service method finishes, not when the HTTP response is sent. Under load this matters because the pool is the scarcest resource, so holding connections longer than necessary is what causes requests to queue.
Work is not duplicated across services. The recommendation engine is a separate service, so recommendation processing does not compete for the same thread pool as ordinary API requests.
Also noted and adressed:
Two properties are known from the deployment configuration rather than from testing. The first is cold start. The service is configured to scale to zero when idle, so the first request after an idle period waits for a container to start. This is a deliberate trade to stay inside the free tier. The uptime monitor calling the health endpoint every 5 minutes keeps production warm during normal hours, so users are unlikely to meet a cold start in practice. The second is the connection pool size, which sets the point at which requests begin to queue rather than being served immediately.
QR2: Scalability
| # | Requirement | Target |
|---|---|---|
| QR2.1 | Workload increase supported | Up to 200% |
| QR2.2 | Performance loss under that increase | No more than 10% |
| QR2.3 | Architectural change required to handle the increase | None |
How the architecture supports this
The backend holds no state between requests. Everything needed to serve a request arrives with the request. There is no in-memory session, no user data cached on one instance, and no instance that owns a particular user. This is the property that makes horizontal scaling possible. Any instance can serve any request, so adding instances adds capacity without any coordination between them.
Cloud Run adds and removes instances automatically. Instance count follows request volume. No manual step, redeploy, or configuration change is needed when load rises, and instances are removed again when it falls.
The engine scales separately. Because recommendation work runs in its own service, heavy recommendation load scales that service without forcing the API to scale with it, and the reverse.
Also noted and adressed:
What can be stated with confidence is QR2.3. The architecture requires no change to handle more load, because scaling is horizontal, automatic, and bounded only by configuration values. Instances scale freely, but they all share one database. The connection pool is therefore the first thing that will saturated, before instance count becomes the constraint. Scaling past that point means increasing the pool and the database plan and not changing the application.
QR3: Security
| # | Requirement | Target |
|---|---|---|
| QR3.1 | All endpoints except register, login, and health require a valid token | 100% |
| QR3.2 | Missing, invalid, or expired token | HTTP 401 |
| QR3.3 | Request for data the caller does not own | HTTP 403 |
| QR3.4 | Passwords hashed before storage | BCrypt cost 12 or higher |
| QR3.5 | Network traffic encrypted | TLS 1.2 or higher on every connection |
| QR3.6 | No passwords or keys committed to the repository | 0 findings per pull request |
| QR3.7 | No container image with a known serious vulnerability | 0 findings per pull request |
Authorisation is based on ownership. Before returning or changing anything, the service checks that the signed-in user owns that data. A user who is correctly signed in still cannot read or edit another user's recipes, pantry, or vaults.
How the architecture supports this
| Mechanism | Where it lives |
|---|---|
| Token signature and expiry checked on every request | JwtUtil, JwtAuthFilter |
| Everything requires authentication unless explicitly allowed | SecurityConfig |
| Passwords hashed with BCrypt at cost 12 | SecurityConfig |
| Ownership checked before reading or changing data | Service layer, across 8 services |
| Errors returned without stack traces or internal detail | GlobalExceptionHandler |
| Passwords and keys supplied at start-up, never stored in code | Google Secret Manager |
| CI signs in to the cloud without a stored key | Workload Identity Federation |
| Separate identities for deploying and for running | 3 service accounts |
| Recommendation engine not reachable from the internet | Deployed with --no-allow-unauthenticated |
| Every pull request scanned for committed secrets | Gitleaks |
| Every pull request scanned for vulnerable images | Trivy |
| Third-party build steps pinned to exact commit hashes | All workflows |
Ownership checks sit in the service layer, not the controller. A new endpoint added to a controller calls into the service to do its work, and the check runs there, so the new endpoint cannot accidentally skip it. If the check lived in the controller, every new endpoint would have to remember to repeat it.
The engine has no public address and no database access. Its Cloud Run service rejects unauthenticated calls, so it can only be reached by the backend's identity. Separately, its service account has no database permissions. Both would have to fail for the engine to reach user data directly.
Evidence
These figures are counts from the codebase and the test suite. They are reproducible by running the test suite and inspecting the pipeline output.
| Measurement | Value | Target | Verdict |
|---|---|---|---|
| Endpoints requiring authentication | All except register, login, health | 100% | Met |
| Tests confirming unauthenticated access is refused | 22 | All pass | Met |
| Tests confirming ownership is enforced | 109 | All pass | Met |
| Ownership checks in the code | 33, across 8 services | Met | |
| BCrypt cost factor | 12 | 12 or higher | Met |
| Encrypted connections | 3 of 3 | 100% | Met |
| Committed secrets found | 0 | 0 | Met |
| Serious image vulnerabilities found | 0 | 0 | Met |
Ownership is confirmed at two levels. Service tests check the rule in isolation. Integration tests confirm that a blocked request reaches the client as an HTTP 403 after passing through the real security filters and a real database.
QR4: Reliability and Availability
| # | Requirement | Target |
|---|---|---|
| QR4.1 | Production API available | 99.9% uptime |
| QR4.2 | Scheduled maintenance | 2 hours per month or less |
| QR4.3 | Recovery by returning to a known-good version | Under 5 minutes |
| QR4.4 | A failed deployment does not interrupt service | 0 seconds downtime |
| QR4.5 | A failed database migration stops the deployment | 0 partial deployments |
| QR4.6 | Database operations complete fully or not at all | 0 lost transactions |
How the architecture supports this
| Mechanism | Effect |
|---|---|
| Every previous version is kept as a revision | Returning to an earlier version is a traffic switch, not a rebuild |
| New version deployed with no traffic, then switched over | A failed deployment leaves the old version serving |
| Database migration runs before anything is deployed | A failed migration stops the process before any code goes live and deployed |
| Production deployments require reviewer approval | No unreviewed change reaches production |
| Health checks in the container and in the application after deployemnt | An unhealthy container receives no traffic |
| Health check after switching back | A switch to a broken version is caught immediately |
| Database transactions on multi-step operations | Nothing is left half-written |
| External uptime monitor every 5 minutes | Independent record of availability |
The recovery time target is met by design rather than by effort. Because every past revision still exists, returning to one is a routing change that takes seconds. Nothing is rebuilt, so recovery time does not depend on build duration.
Transaction safety is enforced at the service layer. Registration is the clearest case: it creates a user, a profile, a preferences record, and a vault as one unit. If any step fails, all four are undone, so a half-registered account cannot exist.
Evidence
Uptime is monitored from 29 July 2026, when monitoring was pointed at the current production system. The monitor is a keyword monitor. It does not only check that the server replied, it checks that the reply contains "status":"UP". A server that answers while unhealthy is recorded as down, which a plain connection check would miss.
| Measurement | Value | Target | Verdict |
|---|---|---|---|
| Monitoring since | 29 July 2026 | ||
| Monitor type | Keyword, checks the reply says "UP" | Met | |
| Check interval | 5 minutes | ||
| Uptime over the monitored period | 99.57 | 99.9% | Negligable difference of 0.33 from target. Met |
| Scheduled maintenance taken | 0 hours | 2 h per month or less | Met |
| Deployments causing downtime | 0 | 0 | Met |
The monitor is confirmed to detect real failures. A two minute outage was recorded and alerted on 29 July 2026.
Limitation. Returning to an earlier version restores the application but does not undo database changes. An earlier version can only run if it still works with the current database structure. Where a change removes or restricts a column, recovery means fixing forward instead, and the rollback workflow asks the operator to confirm this before proceeding.
QR5: Maintainability
| # | Requirement | Target |
|---|---|---|
| QR5.1 | Backend test coverage enforced in CI | 80% or higher |
| QR5.2 | Frontend test coverage enforced in CI | 80% or higher |
| QR5.3 | Every measured backend class has at least one covered line | 1 line or more |
| QR5.4 | A change on main reaches production without manual steps | Under 2 hours |
| QR5.5 | No merge on a failing build | 100% gated |
| QR5.6 | Controllers do not call repositories directly | 0 violations |
| QR5.7 | Database changes go through versioned migrations | 100% |
What coverage includes
Coverage measures application logic. It excludes code with no logic to test: the start-up class, configuration classes, data-transfer objects, database entities, repository interfaces, and generated files. Including these would inflate the figure without indicating anything about test quality.
Backend coverage is checked on three counts: instruction, branch, and method. A separate rule requires every measured class to have at least one covered line, so a class with no tests at all is noticed even with a good overall average.
How the architecture supports this
| Mechanism | Effect |
|---|---|
| Separate CI jobs per part of the system | Changing one part does not rebuild the others |
| Controller, service, repository layering | Logic can be tested without a web server or a database |
| Repository interfaces with real and mock implementations | The app can be tested without a running backend |
| Coverage gates that fail the build | Coverage cannot drop |
| Checkstyle, flutter analyze, Ruff, mypy | Style and type errors are caught before review |
| Versioned migrations with the schema validated at start-up | A mismatch fails immediately instead of corrupting data |
| Deployments filtered by changed path | Documentation changes do not redeploy services |
Evidence
Coverage figures come from Codecov, which receives a report from every CI run on main. The backend figure comes from JaCoCo, the frontend figure from lcov.
| Measurement | Value | Target | Verdict |
|---|---|---|---|
| Backend coverage | 97% | 80% or higher | Met |
| Frontend coverage | 88% | 80% or higher | Met |
| Combined coverage | 89% | 80% or higher | Met |
| Database changes made outside migrations | 0 | 0 | Met |
Deployment time is bounded by the pipeline rather than by a person. A merge to main triggers the build, the migration, and the deployment with no manual step in between, so the 2 hour target is set by pipeline duration and approval wait, not by developer availability.
Mapping of quality requirements to architectural decisions
QR1 - Perfroamcne
QR2 - Saclabaility
QR3 - Security
QR4 - Reliability and Avaialbility
QR5 - Maintainability
| QR | Requirement | Decision | Why it delivers the requirement |
|---|---|---|---|
| QR1 | Response under 2 s | Application and database in the same region | Removes cross-region latency from every database call |
| QR1 | Response under 2 s | Login state carried in the token | Authentication costs no database lookup |
| QR1 | Response under 2 s | Connections released when the service method ends | The scarcest resource is held for the shortest time |
| QR1 | Cold start accepted | Scale to zero when idle | Trades a start-up delay for zero cost while idle; the uptime monitor keeps production warm |
| QR2 | 200% more load | Stateless backend | Any instance serves any request, so instances can be added without coordination |
| QR2 | No architectural change | Limits held in configuration | Instance and pool limits are free-tier settings; raising them changes a value, not the design |
| QR2 | Independent scaling | Engine as a separate service | Recommendation load and API load scale separately |
| QR3 | Authentication | Signed tokens, deny by default | Signature and expiry checked on every request; endpoints are closed unless opened |
| QR3 | Authorisation | Ownership checked in the service layer | Placed below controllers, so a new endpoint cannot skip it |
| QR3 | Credential protection | Secrets supplied at start-up | No credential in the image or the repository; CI authenticates without a stored key |
| QR3 | Service isolation | Separate identities per role | The engine has no public address and no database permission |
| QR3 | Supply chain | Pin and scan | Build steps pinned to exact hashes; images scanned every pull request |
| QR4 | Recovery under 5 min | Every revision retained | Recovery is a traffic switch, so it does not depend on build time |
| QR4 | No downtime on failure | Deploy and switch-over separated | The new version takes no traffic until it is confirmed healthy |
| QR4 | Safe database changes | Migration runs first | A failed migration stops the deployment before any code goes live |
| QR4 | 99.9% availability | Managed platform, external monitoring | The platform replaces unhealthy containers; the monitor checks the reply says "UP" |
| QR5 | 80% coverage | Layering with injected dependencies | Logic is testable without a web server or database |
| QR5 | Deploy under 2 h | Automated pipeline | No manual step between merge and production |
| QR5 | Schema integrity | Versioned migrations only | Schema is validated at start-up, so a mismatch fails immediately |
Last updated: 30 July 2026