Software Engineering
Level: 4 — Application (novel/unseen problems, no hints) Time limit: 60 minutes Total marks: 60
Answer all questions. Show reasoning. Where calculations are required, show working.
Question 1 — REST API & Pagination (12 marks)
You are designing a REST API for a music-streaming service. Clients need to fetch a user's playlists and the tracks in each playlist.
(a) Design the resource URLs and HTTP methods for: listing a user's playlists, creating a playlist, fetching a single track inside a playlist, and removing a track from a playlist. Present as a table. (4)
(b) A GET /users/42/playlists request returns 10,000 playlists. Design an offset-based pagination scheme and a cursor-based scheme. For a table with frequent inserts near the start of the ordering, explain which scheme avoids skipped/duplicated items and why. (4)
(c) For each scenario below, give the single most appropriate HTTP status code and justify in one line: (4)
- Client sends a
POSTto create a playlist but omits the requirednamefield. - Client requests
GET /playlists/9999where 9999 does not exist. - Client
PUTs an update but their token has expired. - Client successfully creates a playlist.
Question 2 — Git DAG & Operations (14 marks)
Consider this commit history on branch main (left = older):
A ── B ── C ── D (main)
\
E ── F (feature)
(a) A developer runs git rebase main while on feature. Draw the resulting DAG and state how many new commit objects are created and why the SHAs of E and F change. (4)
(b) Instead, the team does git merge feature into main. State how many new commit objects are created, how many parents the merge commit has, and one situation where this merge is a fast-forward (no new commit). (3)
(c) A bug was introduced somewhere between commit A (known good) and D (known bad). There are 6 commits in the suspect range. Using git bisect, what is the maximum number of commits you must test to isolate the faulty one? Show the calculation. (3)
(d) You committed a secret API key in commit C. It exists in every later commit. Explain why simply deleting the file in a new commit does NOT remove it from history, in terms of Git's object model (blob/tree/commit). (4)
Question 3 — Testing, Coverage & TDD (12 marks)
Consider this function:
def classify(n):
if n < 0:
return "neg"
if n == 0:
return "zero"
if n % 2 == 0:
return "even"
return "odd"(a) Give the minimum set of input values that achieves 100% branch coverage. State the expected output for each. (4)
(b) The control-flow has how many independent linear paths through it? Compute the cyclomatic complexity and explain the relationship to the number of test cases needed for path coverage. (4)
(c) You are asked to add a rule: negative even numbers return "neg-even". Describe the Red-Green-Refactor cycle you would follow, naming what happens in each phase. (4)
Question 4 — Architecture & Deployment (12 marks)
A startup runs a monolith handling image uploads, user auth, and email notifications. Traffic on image uploads spikes unpredictably.
(a) Propose whether to split into microservices and which boundaries you would draw. Give one concrete advantage and one concrete disadvantage of the split for this workload. (4)
(b) Explain the roles of a Kubernetes Deployment, Service, and Ingress when running the image-upload component with 5 replicas. (4)
(c) The notification component is bursty and idle most of the day. Argue whether serverless or an always-on container is more cost-appropriate, and name one latency drawback of your serverless choice. (4)
Question 5 — Security & Code Review (10 marks)
You receive this pull request for a login endpoint:
def login(request):
user = request.get("user")
pw = request.get("pw")
query = "SELECT * FROM users WHERE name='" + user + "' AND pw='" + pw + "'"
row = db.execute(query)
if row:
log.info("Login: user=%s pw=%s" % (user, pw))
return set_cookie("session", user)
return "fail"(a) Identify three distinct security vulnerabilities (map each to an OWASP Top 10 category or a named class) and give a fix for each. (6)
(b) Distinguish authentication from authorization and state which one this endpoint performs. (2)
(c) State two non-security items a reviewer should flag in this code. (2)
Answer keyMark scheme & solutions
Question 1 (12 marks)
(a) (4 marks — 1 each)
| Action | Method | URL |
|---|---|---|
| List user's playlists | GET | /users/42/playlists |
| Create a playlist | POST | /users/42/playlists |
| Fetch single track in playlist | GET | /playlists/{pid}/tracks/{tid} |
| Remove a track | DELETE | /playlists/{pid}/tracks/{tid} |
Why: resources are nouns; collection vs member URLs; method conveys the verb. Accept /users/42/playlists/{pid}/tracks/... variants.
(b) (4 marks)
- Offset:
GET /users/42/playlists?limit=50&offset=100— returns items 101–150. (1) - Cursor:
GET /users/42/playlists?limit=50&cursor=<opaque-id>where cursor encodes the last seen key; server returns items after that key. (1) - With frequent inserts near the start, offset-based skips/duplicates items because offset counts positions that shift when rows are inserted/deleted (an item that moves past the offset boundary is skipped or repeated). (1)
- Cursor-based avoids this because it anchors on a stable key value, not a positional count, so shifting positions don't affect the boundary. (1)
(c) (4 marks — 1 each)
- Missing
name→ 400 Bad Request (malformed/invalid request; client error, validation). - Non-existent playlist → 404 Not Found (resource does not exist).
- Expired token → 401 Unauthorized (authentication failed/missing valid credentials). (Accept discussion; 403 is wrong because identity, not permission, is the issue.)
- Successful creation → 201 Created (new resource created; ideally with
Locationheader).
Question 2 (14 marks)
(a) (4 marks)
A ── B ── C ── D ── E' ── F' (feature)
- E and F are replayed on top of D, producing 2 new commit objects (E', F'). (2)
- Their SHAs change because a commit's hash is derived from its content including its parent pointer and tree; the new parent (D instead of B) and possibly new tree change the hash. (2)
(b) (3 marks)
- A three-way merge creates 1 new commit object (the merge commit). (1)
- The merge commit has 2 parents (D and F). (1)
- It is a fast-forward (no new commit) if
mainhas no commits since the branch point — i.e., main still points at B and merely advances to F. (1)
(c) (3 marks)
- Bisect does binary search: max tests . (1)
- With : . (2)
(d) (4 marks)
- Each commit stores a snapshot via a tree object pointing at blob objects; commit C's tree still references the secret's blob. (1)
- Adding a new commit that deletes the file only changes the latest tree; older commits (C, D…) remain unchanged and still reference the blob. (2)
- History is immutable/append-only by hash, so the blob is reachable from old commits — you must rewrite history (e.g. filter-repo/rebase) to remove it. (1)
Question 3 (12 marks)
(a) (4 marks — need to exercise all four branches)
| Input | Output |
|---|---|
-1 |
"neg" |
0 |
"zero" |
4 |
"even" |
3 |
"odd" |
Minimum = 4 values (one per return path). (4; deduct for missing/redundant)
(b) (4 marks)
- Decision points: 3
ifs → cyclomatic complexity (predicates + 1). (2) - There are 4 independent paths. (1)
- Cyclomatic complexity gives an upper bound on the number of test cases needed for basis-path coverage; here 4 test cases suffice. (1)
(c) (4 marks)
- Red: write a failing test
assert classify(-4) == "neg-even"; run it — it fails (currently returns"neg"). (1.5) - Green: add minimal code (branch checking
n < 0 and n % 2 == 0) so the test passes. (1.5) - Refactor: clean up duplicated parity logic / reorder conditions without changing behaviour, keeping all tests green. (1)
Question 4 (12 marks)
(a) (4 marks)
- Split by bounded context: image-upload service, auth service, notification service. (1)
- Advantage: image-upload can scale independently to absorb spikes without over-provisioning the whole app. (1.5)
- Disadvantage: added operational/network complexity, distributed transactions/latency, deployment overhead. (1.5)
(b) (4 marks)
- Deployment: declares desired state (5 replicas of the pod), handles rollout/rollback and rescheduling failed pods. (1.5)
- Service: stable virtual IP/DNS that load-balances across the 5 pods (pods are ephemeral with changing IPs). (1.5)
- Ingress: L7 HTTP routing/host-path rules and TLS that exposes the Service to external clients. (1)
(c) (4 marks)
- Notifications are bursty/idle → serverless is more cost-appropriate: pay-per-invocation, scales to zero, no idle cost. (2.5)
- Drawback: cold-start latency on the first invocation after idle. (1.5)
Question 5 (10 marks)
(a) (6 marks — 2 each; 1 for identify, 1 for fix)
- SQL Injection (OWASP A03 Injection) — string concatenation into query. Fix: parameterised/prepared statements (
db.execute("... WHERE name=? AND pw=?", (user, pw))). - Plaintext password storage/comparison + logging secrets (A02 Cryptographic Failures / A09 Logging failures) — passwords compared in cleartext and written to logs. Fix: store salted hashes (bcrypt/argon2), verify hash, and never log credentials.
- Weak/insecure session handling (A07 Identification & Auth Failures) — session cookie is just the username, forgeable; no HttpOnly/Secure/signing. Fix: use a signed random session token, set
HttpOnly,Secure,SameSite.
(b) (2 marks)
- Authentication = verifying who you are; authorization = deciding what you may do. (1)
- This endpoint performs authentication (login). (1)
(c) (2 marks — any two)
- No input validation / missing-key handling (
request.getmay returnNone). - Magic string returns instead of proper status codes; poor error messaging; no rate limiting; not testable due to global
db/log.
[
{"claim":"Bisect max tests for N=6 range is 3 (ceil log2 6)","code":"import math; result = math.ceil(math.log(6,2)) == 3"},
{"claim":"Cyclomatic complexity = predicates+1 = 3+1 = 4","code":"predicates=3; result = (predicates+1)==4"},
{"claim":"Branch coverage of classify needs 4 distinct inputs","code":"paths={'neg','zero','even','odd'}; result = len(paths)==4"},
{"claim":"Rebase of E,F onto D creates 2 new commit objects","code":"commits_replayed=2; result = commits_replayed==2"}
]