Mock Exams — the Real Papers, Self-Marked
The three real assessments for this module — the two in-class assignments and the 2025 final paper — reproduced faithfully, question by question, mark allocation by mark allocation. There are no hint ladders here: a mock is not a tutorial. You sit the paper cold, on the clock, and only then open the model answers to mark yourself against a real rubric. Every model answer is written the way a top student would argue it — naming the principle is worth almost nothing; the marks live in the justification.
How to use this studio
This page only works if you use it in the right order. The temptation is to peek. Don't.
- Sit each paper TIMED, on paper or in a plain editor, with every reveal closed. Give the assignments and the final paper their full 2 hours each. Answer all parts as if it were the hall — no chapter tabs open, no searching, no model answers.
- Only then come back here. Transcribe what you actually wrote into the commit box under each sub-part (your text is saved locally in your browser, so you can close the tab and return).
- Self-mark against the rubric. Each model answer carries the real mark breakdown — award yourself the sub-marks honestly, line by line, then read the "common mistake" note to see if you fell into it.
- Send every dropped mark to a chapter. The model answers cross-link the exact section that fixes the weakness. The How you did section at the end turns your scores into a study plan.
The single most valuable thing on this page is the gap between what you wrote under pressure and the
full-marks answer. If you read the model first, that gap vanishes and so does the learning — you'll recognise
the right answer without being able to produce it in the exam. Recognition is not recall.
Commit your own answer, in full, before you open a single reveal. Treat the closed
details boxes as sealed until the clock stops.
Each rubric mimics a marking scheme: ✔ Full credit is what earns the whole allocation, ◐ Partial is what a half-answer scores, and ✘ Common mistake is the classic error that caps you. Where the exam asks you to compare two things (Factory Method vs Abstract Factory, SOA vs MSA, RMI vs CORBA, MVC vs 3-tier), the model answer is a table — because that is exactly how an examiner wants the difference laid out.
Assignment 1 — Parking Lot Management System
In-class · 2 hours · 100 marks. You design the business object model for a multi-level parking facility: vehicles enter and exit, spots of different sizes are allocated, fees are calculated. Part 1 (50 marks) tests OO fundamentals and object relationships; Part 2 (50 marks) tests the five SOLID principles. Assume the UI and persistence are separate concerns — model the business logic only.
Part 1 — OO Design Fundamentals & Object Relationships (50 marks)
Describe an example of an object relationship you would propose, based on the Parking Lot Management System, to model the object-oriented principle of Abstraction.
Model answer & rubric
Abstraction is about exposing what an object does while hiding how it does it. The cleanest
example in this domain is an abstract Vehicle (or an IParkable interface) that
declares the essential behaviour every vehicle must offer — GetSize(),
CalculateFee(Duration d) — without committing to any one concrete vehicle. Car,
Motorcycle and Bus are concrete realisations that fill in the detail.
The relationship that carries the abstraction is the dependency of ParkingLot on the
Vehicle abstraction: ParkingLot.ParkVehicle(Vehicle v) is written entirely against
the abstract type. The lot "knows" only the contract — that any vehicle can report its required spot size —
and is deliberately ignorant of engines, wheel counts or passenger capacity. That ignorance is the
abstraction: it lets the lot reason about a bus and a motorcycle through one simplified concept. A future
ElectricVehicle slots in without the lot changing, which is the payoff of abstracting at the
right seam. See the four pillars and
object relationships.
- ✔ Full credit (5): names a concrete abstraction from the domain (abstract
Vehicle/IParkable) and a relationship that uses it (ParkingLotdepends on the abstract type) and states clearly what implementation detail is hidden and why that helps. - ◐ Partial (2–3): defines abstraction correctly but the example is generic or the "what is hidden" argument is missing.
- ✘ Common mistake: confusing abstraction with encapsulation — writing about private fields and getters instead of an abstract type that clients depend on. Encapsulation hides data; abstraction hides a whole implementation behind a simplified contract.
ParkingSpot [5 marks]ParkingSpot should be encapsulated. Propose 3 attributes you would make private
within the class, and explain the reason for each selection.
Model answer & rubric
Encapsulation means the field is private because there is a rule about it that outsiders must not be able to break. Three good choices, each with its invariant:
private bool isOccupied— occupancy must only ever flip throughAssign()/Release(). If it were public, two vehicles could be parked in one spot by careless code setting the flag directly; the class could no longer guarantee its own consistency.private SpotSize size— the size is fixed at construction and determines which vehicles fit. Making it private and read-only stops anyone mutating a "large" bus bay into a "small" one at runtime, which would corrupt allocation logic.private Vehicle? occupant— the spot must reference at most one vehicle, and only while occupied. Hiding it prevents external code from pointing a spot at a vehicle that never parked there, keepingisOccupiedandoccupantin agreement.
The theme in every justification is the same: private + methods = the class enforces its invariants instead of trusting every caller. See encapsulation.
- ✔ Full credit (5): three sensible private fields, each with a reason tied to protecting internal state / an invariant — not just "it's good practice".
- ◐ Partial (2–3): three fields but reasons are generic ("hide data") or one field is really behaviour.
- ✘ Common mistake: listing fields with no invariant — the marks are in why uncontrolled external modification would be harmful, not in the list itself.
ParkingSpot [5 marks]Propose 2 public functions you would expose on ParkingSpot to let it interact
with external objects, and explain how these functions protect the internal state from direct, uncontrolled
modification.
Model answer & rubric
The two functions should be the only doors into the private state, and each should validate before it mutates:
public bool Assign(Vehicle v)— checks the invariant first: if the spot is already occupied, orv.RequiredSize > this.size, it refuses and returnsfalse; otherwise it setsoccupant = vandisOccupied = truetogether. External code can never leave the two fields inconsistent because it cannot set either directly.public void Release()— clearsoccupant = nullandisOccupied = falseas one atomic operation, so the spot cannot end up "free but still pointing at a car". A caller asks the spot to release; it does not reach in and null a field.
Protection comes from the fact that both methods are gatekeepers: they run validation and keep related fields in step, so the object's rules hold no matter who calls. This is the behavioural half of encapsulation — private data plus a controlled public API.
- ✔ Full credit (5): two methods that clearly mediate access, with the validation/guard described and an explicit statement that they keep private fields consistent.
- ◐ Partial (2–3): two plausible methods but the "how it protects state" is asserted, not shown (no validation or consistency argument).
- ✘ Common mistake: proposing naked getters/setters — a public
setthat assigns the field with no check is not protection; it's the exposed field with extra steps.
Assume Car, Motorcycle, Bus inherit from Vehicle. What
specific attributes (3) would each subclass uniquely define (not defined in Vehicle)? State any
assumptions.
Model answer & rubric
The discipline here: an attribute belongs on the subclass only if it makes no sense on every vehicle.
Assumption stated up front: Vehicle already holds the common data (see Q3·B), so these are the
genuinely type-specific extras.
Car:numberOfDoors,hasChildSeat,bootCapacityLitres.Motorcycle:hasSidecar,engineCc,requiresHelmetStorage.Bus:passengerCapacity,numberOfAxles,isDoubleDecker.
Each of these would be meaningless or wasteful if pushed up to Vehicle (a motorcycle has no
boot; a car has no axle count worth modelling), which is precisely the test for "belongs on the subtype".
See inheritance and
where inheritance is appropriate.
- ✔ Full credit (4): three plausible, genuinely subtype-specific attributes per class, with an assumption stated. Roughly 1 mark of the 4 rides on the attributes being truly specific (not common data in disguise).
- ◐ Partial (2): attributes given but several are actually shared (e.g. putting
colourorlicensePlateon a subclass). - ✘ Common mistake: duplicating a field that should live once on
Vehicle— that defeats the point of the superclass.
Give 2 attributes that would be shared among the superclass Vehicle and its
subclasses.
Model answer & rubric
A field belongs on Vehicle when every subtype needs it and none would define it
differently. Two clear cases:
licensePlate(string) — every vehicle entering the lot is identified this way; the ticket and exit lookup both rely on it.requiredSpotSize(enumSmall/Medium/Large) — every vehicle reports the size of spot it needs. Note this is declared on the base but its value is set per subtype, which is what makes the polymorphic allocation in Q3·D work.
entryTime is an equally valid answer. The reasoning that earns the marks: placing shared state
once on the base avoids duplication and lets ParkingLot treat all vehicles uniformly. See
inheritance and shared state.
- ✔ Full credit (4): two genuinely universal fields with a one-line justification that they are common to all vehicles.
- ◐ Partial (2): fields named but one is really subtype-specific, or no justification.
- ✘ Common mistake: choosing something a subtype would override in meaning (e.g. a fee rate, which differs by type) rather than shared data.
Provide 2 functions of the superclass Vehicle that would be overridden by the
subclasses.
Model answer & rubric
A method is worth overriding only when the subtypes genuinely do it differently:
CalculateFee(TimeSpan duration)— a bus is charged at a higher hourly rate than a car, a motorcycle lower. Same signature, different arithmetic per subtype. This is the override that Q3·E asks you to exploit.GetRequiredSpotSize()— returnsLargeforBus,MediumforCar,SmallforMotorcycle. The lot calls it without knowing the concrete type.
Both are declared virtual/abstract on Vehicle and
override in each subclass, so a Vehicle reference dispatches to the right body at
run time. See polymorphism.
- ✔ Full credit (4): two methods whose behaviour genuinely varies by subtype, with the difference named.
- ◐ Partial (2): methods given but they would realistically be identical across subtypes (nothing to override).
- ✘ Common mistake: proposing a getter for a field as an "override" — overriding is about differing behaviour, not exposing differing data.
parkVehicle() [4 marks]Explain how polymorphism lets parkVehicle(Vehicle vehicle) handle Car,
Motorcycle and Bus differently without explicit type checking
(no if (vehicle instanceof Car)).
Model answer & rubric
The method parameter is typed as the abstraction, Vehicle, so at compile time
parkVehicle knows only the contract. When it calls vehicle.GetRequiredSpotSize(),
the runtime looks at the actual object — a Bus, say — and dispatches to
Bus.GetRequiredSpotSize(). This is dynamic dispatch (late binding): the object
carries its own behaviour, so the method behaves differently for each subtype while its code is written once.
That is exactly why no instanceof ladder is needed. A switch on type would force
parkVehicle to be modified every time a new vehicle is added (an OCP violation);
polymorphism pushes the per-type decision into the subclasses, so adding ElectricVehicle means
adding a class, not editing the lot. The decision lives with the data that knows the answer. See
polymorphism and
OCP.
- ✔ Full credit (4): 2 for naming dynamic dispatch / the single
Vehicle-typed reference; 2 for explaining that overridden methods let one code path branch by object, so the type-check disappears and the design stays open to new types. - ◐ Partial (2): says "polymorphism handles it" but doesn't explain late binding or link it to avoiding the switch.
- ✘ Common mistake: describing overloading (many methods, chosen at compile time) instead of overriding (one method, chosen at run time). Overloading would reintroduce the type dependency.
Describe how the system would leverage polymorphism when calculating the parking fee, assuming fees vary by vehicle type.
Model answer & rubric
At exit, the lot holds a Vehicle reference and simply calls
vehicle.CalculateFee(duration). Because CalculateFee is overridden per subtype, a
Bus object runs the bus rate, a Car the car rate — the billing code never asks
"what kind of vehicle is this?" It relies on each object knowing its own pricing. Adding
ElectricVehicle with a discounted rate requires no change to the exit/billing path at all.
A stronger answer notes the design tension: baking the fee into the vehicle subtype couples pricing
policy to the vehicle hierarchy, so introducing surge pricing or long-term discounts (Part 2, Q2) would
multiply subclasses. The more flexible variant is to give Vehicle an injected
IFeeStrategy and delegate: polymorphism then varies the strategy, not the vehicle
class. Either answer earns full marks if the dispatch argument is clear; the Strategy remark shows
top-band judgement. See Strategy and
OCP.
- ✔ Full credit (4): explains that the lot calls an overridden
CalculateFeeon a base-typed reference and the correct per-type calculation runs via dynamic dispatch, with no type checks. - ◐ Partial (2): mentions polymorphism but ties it to fields rather than the overridden fee method.
- ✘ Common mistake: proposing a
switch(vehicle.Type)inside a centralFeeCalculator— that is the anti-pattern polymorphism is meant to remove.
ParkingLot, ParkingSpot, Vehicle [10 marks]Describe the relationships — ParkingLot↔ParkingSpot,
ParkingLot↔Vehicle, ParkingSpot↔Vehicle — using
Association, Aggregation and Composition.
Model answer & rubric
The deciding test throughout is lifetime ownership, not merely "does A hold a reference to B".
- ParkingLot — ParkingSpot: Composition. The spots are an intrinsic, permanent part of the
lot; they are created with it and have no meaning outside it. If the lot is demolished, its spots cease to
exist. Strong ownership + shared lifetime = composition (filled diamond at the
ParkingLotend, multiplicity1..*). - ParkingSpot — Vehicle: Association (temporary). While occupied, a spot references the
vehicle in it, but neither owns the other and the link is transient — it exists only for the duration of
the stay, then breaks on exit. This is a plain association (a "uses/occupies" link),
0..1on each side. It is not composition — the vehicle lives on after leaving — but note that alone wouldn't rule out aggregation too (aggregation is independent lifetime by definition); the real reason it isn't aggregation either is that a spot doesn't have a vehicle as a part, it just temporarily references one — there's no whole-part semantics here at all. - ParkingLot — Vehicle: Association. The lot deals with vehicles that pass through it; a vehicle exists entirely independently of the lot and is shared with the outside world. A transient "manages/parks" association, not aggregation of owned parts — the lot never owns the vehicles.
(If you argued ParkingLot–Vehicle as aggregation on the grounds that the lot holds
a collection of currently-parked vehicles, that is defensible and earns the marks provided you make
the independent-lifetime argument — the examiner rewards the justification, not the label.) See
association / aggregation / composition.
- ✔ Full credit (10): ~3 marks per pair (correct relationship + lifetime/ownership justification), ~1 mark for using the deciding test consistently. The justification is the bulk of the marks.
- ◐ Partial (5–6): correct labels but justified by "has-a" alone, with no lifetime argument.
- ✘ Common mistake: calling
ParkingSpot–Vehiclecomposition or aggregation and justifying it purely by "the vehicle outlives the parking event, so it isn't owned" — independent lifetime is exactly what aggregation means, so that argument alone doesn't rule it out. The real reason it's plain association is the lack of whole-part semantics: a spot doesn't have a vehicle as a part, it temporarily references one. Swapping composition and aggregation for the lot–spot link (spots don't outlive the lot) is the other classic slip.
Draw a UML class diagram representing the relationships identified in Q3·F. Sketch it on paper first — decide diamond ends and multiplicities before you look.
Model diagram & common drawing mistakes
What a full-marks diagram shows: a filled diamond at the ParkingLot end of the
lot–spot line (composition) with 1..* multiplicity; plain association lines (no diamond) for
both links to Vehicle, the spot–vehicle one labelled 0..1; and hollow-triangle
inheritance arrows from Car/Motorcycle/Bus up to the abstract
Vehicle.
- ✘ Putting the composition diamond at the spot end instead of the owning
ParkingLotend — the diamond always sits with the whole/owner. - ✘ Drawing the spot–vehicle or lot–vehicle links with a diamond (implying ownership) instead of a plain association line.
- ✘ Using a filled/solid arrowhead for inheritance — inheritance is a hollow triangle.
- ✘ Omitting multiplicities entirely (the
1..*and0..1carry marks).
- ✔ Full credit (5): correct relationship notation for all three pairs (2), correct diamond placement and multiplicities (2), inheritance arrows drawn correctly (1).
- ◐ Partial (2–3): right classes and links but wrong notation (diamond ends, arrowheads) or missing multiplicities.
Part 2 — SOLID Design Principles (50 marks)
A monolithic ParkingLot handles parking/unparking, fee calculation, report generation and
payment. Identify at least 2 distinct responsibilities that could be extracted to adhere to
the Single Responsibility Principle.
Model answer & rubric
The SRP test is "one reason to change". ParkingLot currently has four:
- Fee calculation — changes when pricing policy changes (surge, discounts). A distinct reason to change.
- Report generation / formatting — changes when the report layout or output format changes. Nothing to do with parking rules.
- Payment processing — changes when a payment provider or gateway changes.
Each of these is an independent axis of change entangled in one class today, which is why a change to the
report format risks breaking parking logic. The core responsibility that stays in
ParkingLot is allocation/occupancy (parking and unparking). See
SRP.
- ✔ Full credit (6): two (or more) genuinely distinct responsibilities, each articulated as a separate reason to change — 3 marks each.
- ◐ Partial (3): two responsibilities named but justified vaguely ("it does too much") rather than as independent change axes.
- ✘ Common mistake: listing two things that are really one responsibility (e.g. "parking" and "unparking") — those change for the same reason.
For each responsibility identified in Q1·A, propose a new class that encapsulates it, and explain how this improves the design.
Model answer & rubric
FeeCalculator(behind anIFeeCalculator) — owns pricing. Now a new fee rule is a change in one small class the parking logic doesn't touch, and the calculator can be unit-tested in isolation with no lot involved.ReportGenerator— owns formatting/output of reports. Changing to PDF or a new layout affects only this class; it can be reused by other subsystems that need the same reports.PaymentProcessor(behind anIPaymentProcessor) — owns the payment gateway interaction, so swapping providers or mocking payments in tests never disturbs allocation.
ParkingLot retains only allocation and collaborates with these via interfaces. The
improvement to state explicitly: each class now has one reason to change, so changes are localised, the
pieces are independently testable, and they become reusable. This is SRP feeding straight into
DIP (the lot depends on abstractions).
- ✔ Full credit (6): one well-named class per responsibility (3) and a concrete design benefit — localised change, testability, or reuse — not just "cleaner" (3).
- ◐ Partial (3): classes proposed but the benefit is generic.
- ✘ Common mistake: extracting the class but leaving the logic duplicated in
ParkingLot, or having the lot stillnewthe concrete class (misses the DIP payoff).
The lot must support new vehicle types (Electric, Bicycle) and new fee structures (day pricing, surge,
long-term discounts). Explain, with examples, how you would design the Vehicle and fee-calculation
hierarchies to be open for extension but closed for modification. Name the primary SOLID
principle complied with.
Model answer & rubric
Principle: the Open/Closed Principle (OCP) — software entities should be open for extension but closed for modification.
Vehicle hierarchy. Keep Vehicle abstract with the stable contract
(GetRequiredSpotSize(), and a fee hook). A new type — ElectricVehicle,
Bicycle — is a new subclass that supplies its own behaviour. Crucially,
ParkingLot.ParkVehicle(Vehicle v) is written against the abstraction, so it is never edited when
a type is added: the new class plugs in and dynamic dispatch does the rest. That is "closed to modification"
— the core allocation logic is sealed against change — while remaining "open to extension" through
subclassing.
Fee structure. Do not fold pricing into the vehicle subtypes (that would explode
into SurgeCar, LongTermBus…). Instead extract an IFeeStrategy with
decimal Calculate(TimeSpan duration, Vehicle v), and implement
HourlyFeeStrategy, DayPricingStrategy, SurgeFeeStrategy,
LongTermDiscountStrategy. A new rule is a new strategy class; the parking and billing code that
holds an IFeeStrategy reference never changes. This is OCP realised through
Strategy and injected via
DIP.
The unifying argument: both hierarchies push the point of variation behind an abstraction, so extension means adding a class and the existing, tested core logic is untouched — fewer regressions, safer growth. See OCP.
- ✔ Full credit (12): ~2 for naming OCP correctly; ~4 for the vehicle extension mechanism (abstract base + new subclass, lot coded to the abstraction); ~4 for the fee mechanism (Strategy/interface, new rule = new class); ~2 for explaining why core logic stays closed (coded against abstractions, no edits to tested code, no combinatorial explosion).
- ◐ Partial (5–7): names OCP and handles one hierarchy well but leaves the other as a
switchor type-check. - ✘ Common mistake: proposing
if/elseorswitch(vehicleType)in a central calculator — that is the exact "modify existing code" that OCP forbids. Also: baking every fee rule into vehicle subclasses, which multiplies the hierarchy instead of composing it.
With Car and Motorcycle inheriting Vehicle, describe a scenario where
violating the Liskov Substitution Principle causes unexpected behaviour in the parking system. Explain the
guidelines you would follow so Car and Motorcycle can always substitute a
Vehicle reference without breaking functionality.
Model answer & rubric
A concrete violation. Suppose Vehicle declares
virtual decimal CalculateFee(TimeSpan d) with the implied contract "returns a non-negative fee
for any positive duration". Now Motorcycle overrides it to throw for stays over 24
hours ("bikes can't stay overnight"). The billing loop iterates a List<Vehicle> and calls
CalculateFee on each; when it reaches a long-staying motorcycle it crashes, even though the code
was correct against the Vehicle contract. The subtype broke a promise the supertype made — a
textbook LSP failure. Another example: a Bicycle subclass whose
GetRequiredSpotSize() returns null/none, so ParkVehicle — which
assumes every vehicle needs some spot — dereferences it and fails.
Guidelines to preserve substitutability:
- Don't strengthen preconditions. A subclass must accept every input the base accepts (a motorcycle can't reject durations the base allows).
- Don't weaken postconditions. It must still deliver everything the base promised (return
a valid fee, a valid spot size) — no surprising
nulls. - Don't throw new exceptions the base contract didn't sanction, and don't silently do nothing where the base does something.
- Honour invariants of the base type; keep the behaviour a caller would reasonably expect.
- If a would-be subtype genuinely can't fulfil the contract, it isn't a subtype — model the capability separately (composition / a narrower interface) rather than forcing the inheritance.
Follow these and any Vehicle reference can hold a Car or Motorcycle
interchangeably, which is what lets the lot's polymorphic code (Q3·D/E) stay correct. See
LSP.
- ✔ Full credit (12): ~5 for a concrete, in-domain violation that shows how substituting the subtype breaks working code; ~5 for the substitutability guidelines (preconditions, postconditions, no new throws, invariants); ~2 for the "if it can't honour the contract, don't inherit" judgement.
- ◐ Partial (6): defines LSP and gives a vague example, but no contract rules or an example that isn't really a substitution failure.
- ✘ Common mistake: describing an override that merely differs (e.g. a different fee rate) as an LSP violation — differing behaviour is fine; breaking the promised contract is the violation.
The ParkingLot interacts with PaymentGateway, SecurityCameraSystem and
DisplayBoard. Instead of one monolithic ParkingLotInterface, describe how you would
apply the Interface Segregation Principle to design granular interfaces, and explain a benefit over the single
interface.
Model answer & rubric
The problem with the monolith. A single ParkingLotInterface bundling
ProcessPayment(), StreamCamera(), UpdateDisplay(),
RecordEntry()… forces every client to depend on methods it doesn't use. The
DisplayBoard must "implement" payment methods (throwing or leaving them empty), and — worse —
any change to the payment part of the fat interface forces the display and camera implementers to recompile
and be re-tested even though nothing about them changed. ISP says: no client should be forced to depend on
methods it does not use.
The segregated design — small, role-focused (client-specific) interfaces:
IPaymentProcessor { ProcessPayment(); Refund(); }— implemented byPaymentGateway.ISurveillance { StartRecording(); CaptureSnapshot(); }— implemented bySecurityCameraSystem.IDisplay { ShowAvailability(); ShowMessage(); }— implemented byDisplayBoard.
Each component implements only the interface for its role; ParkingLot depends on the specific
abstractions it actually needs for a given collaboration.
Benefits (state at least one clearly): (1) implementers are no longer burdened with
irrelevant methods or forced to write empty/throwing stubs; (2) changes are isolated — altering
IPaymentProcessor doesn't ripple into the camera or display code, so there's less coupling and
fewer needless recompilations/re-tests; (3) each interface is easier to mock and test in isolation. See
ISP.
- ✔ Full credit (14): ~4 for articulating the fat-interface problem (clients forced to depend on unused methods); ~6 for a clean split into role interfaces with the right implementer for each; ~4 for a concrete benefit argued (decoupling / no empty stubs / isolated change / testability).
- ◐ Partial (7): splits the interface but the mapping is muddled, or states "it's better" without a specific benefit.
- ✘ Common mistake: splitting by class rather than by role/client need, or producing one interface per method (over-segregation) instead of grouping cohesive operations.
Assignment 2 — Design Patterns
In-class · 2 hours · answer all questions. This paper is pure GoF pattern judgement: name the category difference, distinguish look-alike patterns, and map patterns onto concrete scenarios (a configurable computer builder, a legacy financial system, heterogeneous product catalogs). The high-mark questions want a pattern and a UML sketch and a "why this over that" argument.
Software design patterns are broadly categorised into three fundamental types. Identify these three categories and clearly explain the main conceptual difference that distinguishes each from the others.
Model answer & rubric
The three categories are organised by what aspect of a design the pattern is concerned with:
- Creational — object creation. These abstract the instantiation process, so client code doesn't hard-wire concrete classes or complex construction. They answer "how and when is an object made, and by whom?" Examples: Factory Method, Builder, Singleton.
- Structural — object composition. These concern how classes and objects are assembled into larger structures while keeping them flexible — the shape of the relationships. They answer "how are objects composed/related?" Examples: Adapter, Composite, Decorator.
- Behavioural — object interaction and responsibility. These concern how objects communicate and distribute responsibility at run time — the flow of control and messages. They answer "how do objects collaborate and who does what?" Examples: Observer, Strategy, Template Method.
The clean one-line contrast that earns the marks: creational is about making objects, structural about arranging them, behavioural about how they interact.
- ✔ Full credit (10): all three named (roughly 2 each = 6) plus a genuine conceptual distinction for each — creation vs composition vs interaction (4). An example per category strengthens but isn't required.
- ◐ Partial (5–6): three categories named but "explained" only by listing patterns, with no conceptual difference stated.
- ✘ Common mistake: defining each category by its patterns rather than by what the category is about — the question explicitly asks for the conceptual difference.
Both belong to the creational category yet address different aspects of creation. Explain the fundamental difference between Factory Method and Abstract Factory. Give a clear, distinct example for each and show why one would be chosen over the other.
Model answer & rubric
Fundamental difference: Factory Method defers the creation of one product to a subclass via a single overridable method; Abstract Factory provides an interface for creating a whole family of related products that are guaranteed to be consistent with one another, via several such methods on one factory object.
| Dimension | Factory Method | Abstract Factory |
|---|---|---|
| What it creates | One product, chosen by subclass | A family of related products, kept consistent |
| Mechanism | Inheritance — override a single creation method | Object composition — a factory object with several creation methods |
| Structure | One method (CreatePage()) | Many methods on one interface (CreateButton(), CreateCheckbox()) |
| Example | Document.CreatePage(): PdfDocument makes a PdfPage, WordDocument a WordPage | IGuiFactory: WinFactory makes WinButton + WinCheckbox, MacFactory makes MacButton + MacCheckbox |
| Choose when | Subclasses vary a single product | You must produce a matching set and mixing families is a bug |
The choosing rule to state explicitly: use Factory Method when there is one product whose concrete type varies; reach for Abstract Factory when you need several related products created together and guaranteed to match (a Windows checkbox must never pair with a Mac button). Abstract Factory is often built from several factory methods. See Factory Method, Abstract Factory, and the factories comparison.
- ✔ Full credit (10): ~4 for the core "one product vs a family/set" distinction; ~4 for a distinct, correct example of each; ~2 for the "why choose one over the other" rule (consistency of a family).
- ◐ Partial (5): distinction stated but examples are the same shape, or only one pattern is exemplified.
- ✘ Common mistake: saying Abstract Factory is "just a bigger Factory Method" without the family consistency point, or giving a Builder-style example (assembling one object's parts) for Abstract Factory.
A company builds highly customisable computers (CPU, RAM, storage, GPU, OS, peripherals). A single
constructor new Computer(cpu, ram, storage, gpu, os, monitor, keyboard, mouse) telescopes
badly. Propose a suitable creational pattern to construct customised computers (Office / Gaming / Workstation),
draw the class composition in UML, and describe the class relationships.
Model answer, UML & common drawing mistakes
Pattern: Builder. The telling symptoms — many optional parts, a telescoping constructor, "Gaming PC vs Office PC vs Workstation" presets, and the desire to add a component without breaking every caller — are the textbook cue for Builder. It separates the construction of a complex object from its representation, so the same step-by-step process can assemble different configurations.
Roles:
Computer— the Product being assembled (holds cpu, ram, storage, gpu, os, peripherals).IComputerBuilder— the Builder interface declaring the steps:SetCpu(),SetRam(),SetStorage(),SetGpu(),SetOs(),AddPeripherals(),Build().GamingPcBuilder,OfficePcBuilder,WorkstationBuilder— ConcreteBuilders, each supplying the parts for that configuration.ComputerDirector— the optional Director that knows the recipe (the order of steps) and drives a builder to produce a standard preset, e.g.Construct(IComputerBuilder b).
Relationships: the concrete builders realise (implement)
IComputerBuilder; the Director holds an association to
IComputerBuilder (it is configured with one and calls its steps); each builder
creates a Computer (a dependency, drawn dashed); the Computer
composes its parts.
A student answer typically also shows the fluent usage:
new GamingPcBuilder().SetCpu("i9").SetGpu("RTX").Build(), and notes that adding a new component
means adding one step to the interface/builders, not editing every caller.
- ✘ Drawing concrete builders → interface with a solid inheritance triangle instead of a dashed realization triangle (it's an interface).
- ✘ Making the
Directorinherit the builder rather than hold one (association). - ✘ Omitting the
Computerproduct entirely, or drawing the "creates" link as a solid association instead of a dashed dependency. - ✘ Confusing this with Abstract Factory (families of products) — Builder assembles one complex product step by step.
- ✔ Full credit (20): ~4 name + justify Builder against the telescoping-constructor symptoms; ~6 describe the four roles (Product, Builder, ConcreteBuilders, Director) with responsibilities; ~6 a correct UML with realization + association + dependency notation; ~4 describe the relationships in words and connect to OCP (new component = new step).
- ◐ Partial (10): names Builder and lists roles but the UML notation is wrong or relationships aren't explained.
- ✘ Common mistake: proposing Abstract Factory or a plain Factory — they don't solve the many-optional- parts assembly problem the scenario describes.
The already-built Computer objects now need to be represented, enhanced, or organised in a larger
system. (1) Identify and describe two distinct structural patterns relevant here (hint:
dynamic feature addition; hierarchical composition). (2) For each, explain how it applies to the
Computer with a concrete post-assembly problem it solves. (3) Articulate the main conceptual
difference between them and when you'd choose one over the other.
Model answer & rubric
Pattern 1 — Decorator (dynamic feature addition).
Decorator wraps an object in another object of the same
interface to add behaviour at run time, stackably. Applied to Computer: after assembly you need to
add optional post-build enhancements — an extended warranty, pre-installed software bundle, RGB lighting,
overclock profile — in any combination. Rather than a subclass per combination
(GamingPcWithWarrantyAndRgb… — a combinatorial explosion), wrap:
new RgbDecorator(new WarrantyDecorator(gamingPc)). Each decorator implements
IComputer, forwards to the wrapped computer, and augments the result (e.g. adjusts
GetPrice()/GetDescription()). The concrete problem solved: enhancing a built machine
without touching its class or exploding the hierarchy.
Pattern 2 — Composite (hierarchical composition).
Composite lets clients treat individual objects and
compositions of objects uniformly through one interface. Applied to Computer: a corporate order is a
tree — a "server rack" contains several workstations, each of which is itself a Computer; or a
Computer is composed of components that themselves contain sub-components. You want
GetTotalPrice() or GetPowerDraw() to work identically whether called on a single
machine or a whole rack. A ComputerBundle (composite) and a leaf Computer both
implement IComputerComponent, and the composite recurses over its children. The concrete problem
solved: aggregating over an arbitrarily deep part-whole structure without the client special-casing leaves vs
groups.
Conceptual difference & choice:
| Decorator | Composite | |
|---|---|---|
| Intent | Add responsibilities to one object dynamically | Compose objects into part-whole trees, treated uniformly |
| Shape | Wraps exactly one child (a linear chain) | Holds many children (a tree) |
| Purpose of recursion | Layering behaviour | Representing a hierarchy |
| Choose when | You need to add/stack features on a finished machine | You need to manage groups of machines/parts as one |
Choose Decorator when the requirement is "add optional features to an assembled computer"; choose Composite when it is "manage internal parts / groups of computers as a single unit". Both keep the same interface (unlike Adapter, which changes it). See the Composite vs Decorator comparison.
- ✔ Full credit (20): ~4 identify + describe two distinct structural patterns (Decorator + Composite are the intended pair); ~8 a concrete Computer application for each with a real post-assembly problem; ~8 the conceptual difference (one child/layering vs many children/hierarchy) and the choosing rule.
- ◐ Partial (10): both patterns named and applied, but the difference is stated weakly or one example is generic.
- ✘ Common mistake: picking Adapter or Facade — neither matches the hints "dynamic feature addition" and "hierarchical composition". Also: describing Decorator as wrapping many children (that's Composite) — blurring the very distinction the question tests.
A legacy reporting module consumes ILegacyDataSource.processFinancialRecordsInXml(...) and cannot
be modified. A new transaction service exposes retrieveTransactionsJson(): List<Map> but is
expensive and access-restricted. Problem 1 — format incompatibility (XML expected, JSON
provided). Problem 2 — controlled, efficient access (lazy loading, caching, authorisation).
Identify the best structural pattern for each, justify it, then articulate the fundamental difference between
the two.
Model answer & rubric
Problem 1 — Adapter. The legacy module and the new service have incompatible
interfaces: one expects an XML string, the other returns a list of JSON maps, and the legacy side can't
change. Adapter is the exact fit: write a
TransactionServiceAdapter that implements ILegacyDataSource (the Target the
legacy module already speaks) and internally calls retrieveTransactionsJson() on the new service
(the Adaptee), translating the JSON maps into the XML string the legacy module expects. The adapter's whole
job is interface translation so two things built to different contracts can work together without
modifying either.
Problem 2 — Proxy. The concerns are lazy loading, caching, and authorisation on the
same interface — a surrogate that controls access to the real object.
Proxy covers all three of its classic variants at once: a
virtual proxy defers the expensive retrieveTransactionsJson() call until data is
genuinely needed; a caching layer stores the result so repeat calls in a short window don't hit the
costly service again; and a protection proxy checks the caller is authenticated/authorised before
forwarding. The TransactionServiceProxy implements the same interface as the real
service and stands in front of it, so clients are unaware they're not talking to the real thing.
Fundamental difference:
| Adapter (Problem 1) | Proxy (Problem 2) | |
|---|---|---|
| Primary goal | Make two incompatible interfaces work together | Control access to an object behind the same interface |
| Interface | Changes it (XML target ← JSON adaptee) | Keeps it identical to the real subject |
| Problem solved | Interface mismatch / integration | Cost, laziness, caching, security |
| Client awareness | Client uses the target interface knowingly | Client thinks it's talking to the real object |
In this scenario: the Adapter exists because the shapes don't match; the Proxy exists because access must be governed. Different problems, hence different patterns — even though both are "wrappers". See the wrapper-family litmus.
- ✔ Full credit (20): ~6 Adapter for P1 with the XML↔JSON translation role explained; ~8 Proxy for P2 covering lazy loading + caching + authorisation as proxy variants; ~6 the fundamental difference (interface translation vs access control; changes interface vs keeps it).
- ◐ Partial (10): both patterns named correctly but one justification is thin (e.g. Proxy stated without tying to all three concerns).
- ✘ Common mistake: calling Problem 2 a Decorator — Decorator adds features; here the wrapper's intent is controlling access (lazy/secure/cached), which is Proxy. Or using a Facade for Problem 1 — a Facade simplifies many subsystems; it doesn't translate one incompatible interface.
Suppliers store products in different internal structures (array, linked list, tree). The Product Display Module must traverse, filter and total inventory across any catalog without coupling to the internal representation. (1) Identify the most appropriate behavioural pattern. (2) Explain its core purpose and how it decouples the client. (3) Map its components to the scenario. (4) Articulate the benefits.
Model answer & rubric
Pattern: Iterator. The problem is "traverse heterogeneous collections uniformly without exposing their internal structure" — the definition of Iterator.
Core purpose & decoupling: Iterator provides a way to access the elements of an
aggregate sequentially without exposing its underlying representation. Each supplier's catalog
exposes a standard GetIterator() returning an IIterator<Product> with
HasNext() / Next(). The Product Display Module programs against that interface
only, so it walks an array-backed, a linked-list-backed, and a tree-backed catalog with identical code. The
traversal logic lives inside each concrete iterator, not in the client, which is what breaks the
coupling: when a supplier changes its internal structure, only that supplier's iterator changes; the display
module is untouched (Open/Closed).
Component mapping:
| Iterator role | Scenario element |
|---|---|
| Iterator (interface) | IProductIterator with HasNext()/Next() |
| ConcreteIterator | ArrayIterator, LinkedListIterator, TreeIterator — one per supplier structure |
| Aggregate (interface) | ICatalog with CreateIterator() |
| ConcreteAggregate | Electronics (array), Boutique (linked list), Crafts (tree) catalogs |
| Client | The Product Display Module (traverse / filter / total) |
Benefits: (1) the display module is decoupled from every internal structure — one traversal code path for all suppliers; (2) adding a new supplier with a novel structure means adding a concrete iterator, with zero change to the display module (extensibility / OCP); (3) traversal, filtering and totalling are written once; (4) multiple simultaneous traversals are possible since each iterator holds its own position. See Iterator.
- ✔ Full credit (20): ~4 identify Iterator; ~6 core purpose + the decoupling argument (traversal hidden behind a uniform interface); ~5 correct component mapping to the scenario; ~5 the benefits, especially future extensibility / OCP.
- ◐ Partial (10): Iterator named and purpose stated, but mapping or benefits are thin.
- ✘ Common mistake: proposing Strategy or Visitor — those vary an algorithm or add operations; the requirement is uniform traversal across differing structures, which is Iterator. Exposing the underlying collection (e.g. returning the raw array) defeats the whole point.
Final Exam 2025
Second Year, Semester I · 2 hours · answer ALL four questions. The paper spans the whole module: SOLID and OO relationships on an Uber-Eats-style order system (Q1), the three pattern families on a library system (Q2), composition-over-inheritance plus clean code and TDD (Q3), and distributed/enterprise architecture — SOA vs MSA, MVC vs 3-tier, RMI vs CORBA, MSA communication styles (Q4).
Question 1 — Online Order Management System (Uber Eats-style)
Payment methods (CreditCardPayment, CashOnDelivery,
DigitalWalletPayment) share a processPayment() behaviour with unique
implementations. Explain how the Open/Closed Principle lets new payment types (e.g.
CryptocurrencyPayment) be added without modifying existing code that processes
payments.
Model answer & rubric
Define an abstraction — abstract class Payment or interface IPayment — declaring
processPayment(). Each method is a subclass/implementer overriding processPayment()
with its own logic. The order-processing code holds the abstraction:
void Checkout(IPayment payment) => payment.processPayment(); — it depends only on the
contract, never on a concrete type.
Because of that, adding CryptocurrencyPayment means writing one new class that
implements IPayment; the checkout code, being coded against the interface and dispatching
polymorphically, is not touched, recompiled logic-wise, or re-tested. The module is
open to extension (new payment classes) and closed to modification (existing, tested
payment-processing code stays sealed). Contrast the anti-pattern: a
switch(payment.Type){ case Crypto: … } would force an edit to working code for every new method
— precisely what OCP prevents. See OCP and
polymorphism.
- ✔ Full credit (6): 2 for naming OCP and the abstract
IPayment/processPayment()contract; 2 for the extension mechanism (new type = new subclass, selected polymorphically); 2 for why core logic stays closed (checkout coded against the abstraction, no edits to tested code). - ◐ Partial (3): names OCP and the hierarchy but doesn't explain how the processing code avoids modification.
- ✘ Common mistake: describing inheritance for its own sake without the "closed to modification" half — or proposing a type switch, which violates the very principle asked about.
Describe how the Dependency Inversion Principle would guide your design so a high-level module depends on an abstraction, not a specific concrete class.
Model answer & rubric
DIP has two clauses: high-level modules should not depend on low-level modules — both should depend on
abstractions; and abstractions should not depend on details — details depend on abstractions. Here the
high-level module is the order/checkout service; the low-level modules are the concrete payment classes. Left
alone, checkout would new CreditCardPayment() and be welded to it.
DIP inverts that: introduce IPayment as the abstraction that both sides depend on.
Checkout depends on IPayment; CreditCardPayment also depends on (implements)
IPayment. The concrete payment is then injected into checkout (constructor or
method parameter) rather than constructed inside it. Now the direction of the source-code dependency points
towards the abstraction, not towards a detail: checkout can be tested with a fake
IPayment, and swapping providers never edits the high-level policy. This is exactly what a DI
container's constructor injection provides. See DIP.
- ✔ Full credit (4): 2 for stating both modules depend on the
IPaymentabstraction (the inversion); 2 for the mechanism — inject the concrete payment rather than instantiate it — and a benefit (testability / swappability). - ◐ Partial (2): mentions "depend on an interface" but not injection or the inversion of direction.
- ✘ Common mistake: conflating DIP with DI — DI (injection) is the technique; DIP is the principle that the dependency should point at an abstraction. Full marks name the principle and use the technique.
Identify (Association, Composition or Aggregation) and justify each pair: (i) Restaurant and its
MenuItems; (ii) Order and the MenuItems; (iii) Customer and
an Order.
Model answer & rubric
- (i) Restaurant — MenuItems: Aggregation. A menu item is defined by the restaurant, but it is a reference-able catalogue entry that can exist and be reasoned about independently of any single order, and conceptually could be shared/duplicated across menus. The restaurant is a "whole" that groups items it doesn't destroy-on-delete in the strong sense. (Composition is also defensible if you argue a menu item has no meaning outside its restaurant and dies with it — award marks for either with a lifetime argument.)
- (ii) Order — MenuItems: Aggregation / Association. An order refers to menu items
the customer selected; those items (the catalogue entries) outlive the order and are shared with the
restaurant's menu. The order does not own them — deleting the order must not delete the menu item — so this
is aggregation, not composition. (If you model the order as holding its own OrderLine copies, those
lines would be composition; the catalogue
MenuItemreference is aggregation.) - (iii) Customer — Order: Composition, if you make the lifetime assumption explicit. This is the expected exam answer, but "the order exists for that customer" is not by itself a lifetime argument — composition requires you to actually assume the order's lifecycle is bound to the customer's, i.e. deleting the customer deletes their orders too. State that assumption and composition is defensible: strong ownership, and an order arguably has no meaning without the customer who placed it. Be aware, though, that most real systems keep orders around after a customer account is deleted, for auditing and legal reasons — under that (arguably more realistic) assumption the order outlives the customer, which makes this association or aggregation instead. Either answer earns marks, provided you state the lifetime assumption you're relying on rather than asserting the label.
Across all three the examiner is marking the lifetime/ownership justification, not the label alone. See association / aggregation / composition.
- ✔ Full credit (6): 2 per pair — a defensible relationship with a lifetime/ownership justification. Either side of the genuinely ambiguous calls scores full marks if justified — for (iii) that means naming the assumption (does deleting the customer delete the order, or not?) rather than just asserting "composition" because the order "belongs to" the customer.
- ◐ Partial (3): correct labels but justified only by "has-a".
- ✘ Common mistake: calling Order–MenuItem composition on the grounds "an order contains items" — the menu item outlives the order and is shared, so ownership/lifetime rules it out. Likewise, calling Customer–Order composition purely because "the order belongs to the customer" without ever stating whether the order's lifetime is actually bound to the customer's.
Model FoodItem and DrinkItem, which share name/price but
have specific attributes (spiceLevel, volume). Propose at least 2 properties per
class in the hierarchy, and methods that could be overridden.
Model answer & rubric
abstract class MenuItem— shared:Name(string),Price(decimal); declaresabstract string GetDescription()and maybevirtual decimal GetPreparationTime().FoodItem : MenuItem— specific:SpiceLevel(enum),IsVegetarian(bool); overridesGetDescription()to include spice/dietary info andGetPreparationTime()(cooking takes longer).DrinkItem : MenuItem— specific:Volume(ml),IsCarbonated(bool); overridesGetDescription()to include size, and might overrideGetPreparationTime()to near-zero.
The design point to state: common state lives once on the abstract base; the subtype-specific attributes and
the differing behaviour (GetDescription) justify the inheritance. See
inheritance.
- ✔ Full credit (5): a base
MenuItemholdingname/price(1); ≥2 specific properties per subclass (2); at least one sensibly overridden method with a reason it differs (2). - ◐ Partial (2–3): subclasses given but shared data duplicated instead of pulled to the base, or no overridable method.
- ✘ Common mistake: putting
spiceLevelon the base (drinks don't have one) or forgetting the overridden method the question explicitly asks for.
Draw a UML class diagram showing the inheritance hierarchy and how Order interacts with the
abstract MenuItem and its subclasses. Sketch it first.
Model diagram & common drawing mistakes
Full marks: abstract MenuItem (italic/«abstract») with the two concrete
subclasses joined by hollow-triangle inheritance arrows, and Order linked to
MenuItem by an aggregation (open diamond at the Order end) with
1..* — Order interacts with the abstraction, not the concrete subclasses.
- ✘ Linking
OrdertoFoodItem/DrinkItemdirectly instead of to the abstractMenuItem(the whole point is polymorphic interaction with the base). - ✘ Solid arrowhead for inheritance instead of a hollow triangle.
- ✘ Filled diamond (composition) at Order — the items outlive the order, so aggregation (open diamond).
- ✔ Full credit (4): correct inheritance triangles (2),
Order→abstractMenuItemrelationship with correct diamond/multiplicity (2). - ◐ Partial (2): hierarchy correct but Order wired to concretes or wrong notation.
Question 2 — Library Management System
The library creates resources (Books, Journals, DVDs; later E-Books, Audiobooks) without client code
(Librarian, CatalogManager) knowing each exact class. Propose a design using the most
suitable creational pattern.
Model answer & rubric
Use a Factory — Factory Method (or a Simple Factory) — producing an abstract
LibraryResource. Define abstract class LibraryResource (or
IResource) with the common contract, and a ResourceFactory.Create(ResourceType type)
(or a CreateResource() factory method) that returns the right concrete
Book/Journal/DVD behind that abstraction. The Librarian
and CatalogManager call the factory and receive a LibraryResource reference — they
never name a concrete class. See Factory Method.
- ✔ Full credit (4): names a Factory (Method/Simple), defines the abstract product, and shows the client depends only on the abstraction.
- ◐ Partial (2): names "factory" but leaves clients depending on concrete types.
- ✘ Common mistake: proposing Builder (there's no complex multi-part assembly here) or Abstract Factory (no family of matching products — just one resource at a time).
Justify your pattern choice and explain how it solves the problem of creating new objects flexibly, adhering to the Open/Closed Principle.
Model answer & rubric
The factory centralises and encapsulates the instantiation decision, so the knowledge of which
concrete class to build lives in exactly one place instead of being scattered across the
Librarian and CatalogManager. Because those clients depend on the abstract
LibraryResource and delegate creation to the factory, they are decoupled from the
concrete types.
OCP link: adding an EBook or Audiobook means writing a new
subclass and registering it with the factory — the client code that uses resources is not modified.
The system is open to extension (new resource classes) and closed to modification (existing catalogue/lending
code untouched). A pure Factory Method goes further: a new resource can arrive via a new creator subclass
with no edit to the factory's existing branches at all. Contrast the naive approach — a
switch(type){ new Book()… } pasted wherever resources are created — which must be edited in many
places for every new type, the OCP violation the factory removes. See
OCP and
Factory Method.
- ✔ Full credit (6): 2 for the justification (encapsulates creation, clients depend on the abstraction); 2 for the OCP mechanism (new type = new class, clients unchanged); 2 for contrasting the scattered-switch anti-pattern / naming why existing code stays closed.
- ◐ Partial (3): says "it's flexible" without tying flexibility to OCP and unchanged client code.
- ✘ Common mistake: claiming full OCP while leaving a
switchinside the factory that must be edited per type — acknowledge the trade-off, or use a registry/Factory Method to truly close it.
Resources sit on shelves, shelves group into sections, and a section or shelf can contain other sections, shelves or individual resources. Propose the most suitable structural pattern.
Model answer & rubric
Composite. This is a part-whole hierarchy where a container (Section, Shelf) can hold both
leaves (individual Resources) and other containers, arbitrarily deep — the defining shape of
Composite. Define
interface ILibraryComponent { Display(); GetItemCount(); }; Resource is a
leaf, while Shelf and Section are composites holding a list of
ILibraryComponent and recursing into their children. The client then treats a single book and a
whole section uniformly — GetItemCount() works the same on either, with the composite
summing over its children.
- ✔ Full credit (4): names Composite, identifies leaf (Resource) vs composite (Shelf/Section), and states the uniform-treatment benefit / recursion.
- ◐ Partial (2): names Composite but doesn't distinguish leaf from composite or explain uniform treatment.
- ✘ Common mistake: proposing Decorator — it wraps one child to add behaviour; here a container holds many children in a tree, which is Composite.
The system notifies users when a requested book becomes available. Propose and explain a suitable behavioural pattern; identify the classes and describe their interaction.
Model answer & rubric
Observer. One-to-many "notify interested parties when state changes" is exactly
Observer. The Book (or a
BookAvailability wrapper) is the Subject; users who requested it are
Observers (implementing IObserver.Update()) that subscribe via
Attach(). When the book's status flips to available, the subject iterates its observer
list and calls Update() on each, pushing a notification. The subject knows only the
IObserver interface, so users can be added/removed at run time without the book changing —
decoupled one-to-many notification.
- ✔ Full credit (3): names Observer (1), identifies Subject=Book and Observers=users (1), describes the subscribe/notify interaction (1).
- ◐ Partial (1–2): names Observer but the roles or the notify flow are vague.
- ✘ Common mistake: proposing Mediator (that coordinates peers) or a Publish/Subscribe broker — for an in-process, one-subject-many-watchers case, Observer is the precise fit.
borrowResource() [3 marks]Borrowing has fixed steps — check eligibility, verify availability, update status. Propose a behavioural
pattern for borrowResource(); identify the base class and the steps concrete subclasses implement
(e.g. for different resource types).
Model answer & rubric
Template Method. A fixed sequence of steps where the order is invariant but some
steps vary by resource type is the signature of
Template Method. The base
abstract class ResourceLoan defines the public void BorrowResource() — the
template — which calls CheckEligibility(), VerifyAvailability(),
UpdateStatus() in that order. The skeleton is written once and sealed; the steps that differ
(e.g. a DVD has a shorter loan period, a reference book can't be borrowed) are abstract/
virtual hooks overridden by BookLoan, DvdLoan, etc. The base controls
the algorithm's shape; subclasses fill in the variable steps — inversion of control ("don't call us, we'll
call you").
- ✔ Full credit (3): names Template Method (1), identifies the base class holding the fixed
borrowResource()skeleton (1), names the overridable steps in subclasses (1). - ◐ Partial (1–2): names the pattern but doesn't separate the fixed skeleton from the varying steps.
- ✘ Common mistake: proposing Strategy — Strategy swaps a whole algorithm at run time; here the algorithm's skeleton is fixed and only steps vary, which is Template Method. See Template Method vs Strategy.
Draw a complete UML class diagram integrating the creational, structural and behavioural patterns from A, B and C, showing inheritance, composition, aggregation and association. Sketch first.
Model diagram & common drawing mistakes
Full marks: the diagram must make all three patterns legible at once — the
ResourceFactory creating an abstract LibraryResource (creational); a
Section/Shelf composite over ILibraryComponent with resources as
leaves (structural); and a Book subject notifying User observers plus an abstract
ResourceLoan template (behavioural) — with correct notation for inheritance (hollow triangle),
realization (dashed triangle), composition/aggregation (diamonds) and association.
- ✘ Showing only one or two patterns — the question explicitly wants all three collaborating.
- ✘ Mixing realization (dashed) and inheritance (solid) triangles.
- ✘ Diamonds on the wrong end for the composite (the diamond sits at the container Section/Shelf).
- ✘ No multiplicities on the composite's child collection.
- ✔ Full credit (5): all three patterns present and correctly notated (~1.5 each) plus correct relationship types tying them together (~0.5).
- ◐ Partial (2–3): patterns present but notation errors, or one pattern missing.
Question 3 — Composition over Inheritance, Clean Code & TDD
A team lead proposes a base PaymentMethod class with each method (CreditCardPayment,
PayPalPayment…) as a subclass. Analyse this from a class-inheritance perspective — what are the
potential issues, especially as new payment methods are added?
Model answer & rubric
Inheritance-only here is workable for the simplest case but reveals classic weaknesses as the hierarchy grows:
- Combinatorial explosion / rigidity along a second axis. The moment a second dimension
appears — e.g. payment method × currency, or method × fraud-check policy — pure subclassing multiplies:
CreditCardUsdWithFraudCheck,PayPalEurNoFraud… The tree balloons. - Tight coupling to the base class. Every subclass inherits the base's implementation;
a change to
PaymentMethodripples into all subclasses (the fragile base class problem), and behaviour is fixed at compile time — you can't change how an object pays at run time. - Weak reuse / duplication. Behaviour shared by some but not all methods has nowhere clean to live — it gets duplicated across subclasses or forced up into a bloated base.
- LSP risk. If a future "method" can't honour part of the base contract (e.g. a store-credit
method that can't refund), forcing it under
PaymentMethodinvites Liskov violations.
The through-line: inheritance binds behaviour statically and along a single axis, so it doesn't flex as requirements grow in more than one direction. See composition vs inheritance and LSP.
- ✔ Full credit (4): at least two distinct, well-argued issues (rigidity/explosion, base-class coupling, compile-time binding, duplication or LSP risk).
- ◐ Partial (2): one issue, or issues listed without explaining why adding methods worsens them.
- ✘ Common mistake: answering "inheritance is bad" with no concrete failure mode — the marks are in the specific weaknesses.
Propose an alternative that favours object composition over class inheritance, using an interface/abstract class for payment processing. Justify why it is more flexible and maintainable, directly addressing "favour object composition over class inheritance".
Model answer & rubric
Design: define interface IPaymentStrategy { Result Pay(decimal amount); }.
Each method — CreditCardStrategy, PayPalStrategy,
DigitalWalletStrategy — implements it. A high-level PaymentProcessor
(or the Order) holds an IPaymentStrategy and delegates:
_strategy.Pay(total). The concrete strategy is injected, so it can be chosen — and
changed — at run time (Strategy pattern). The
processor "has-a" payment behaviour rather than "is-a" payment.
Why more flexible/maintainable (address the maxim directly): composition assembles behaviour from interchangeable parts instead of freezing it in a class hierarchy. (1) Run-time flexibility — a customer can switch from card to wallet on the same order by swapping the strategy object; inheritance would need a different subclass instance. (2) Open/Closed — a new method is a new strategy class the processor never knows about; nothing existing is modified. (3) No base-class coupling / no explosion — a second axis (currency, fraud policy) becomes another injected collaborator, not a multiplied subclass. (4) Testability — inject a fake strategy to test the processor. Composition keeps the pieces small, independent and recombinable, which is precisely why the GoF advise favouring it over inheritance. See composition over inheritance and DIP.
- ✔ Full credit (6): 2 for the composition design (interface + injected strategy, "has-a" delegation); 2 for run-time flexibility / swappability; 2 for tying it to OCP + maintainability and explicitly contrasting with the inheritance rigidity from A(i).
- ◐ Partial (3): shows an interface but keeps
new ConcreteStrategy()inside the processor (no real composition/injection), or doesn't justify against the maxim. - ✘ Common mistake: presenting the interface but still selecting the implementation with a
switchin the processor — that reintroduces the coupling composition was meant to remove.
Consider the code below. It is given in Python in the paper; the equivalent C# is provided alongside so you can reason in either language — the deficiencies are identical in both. Read it, then answer (i) and (ii).
1.
2. def process_data_and_save(data_list, user_id, file_path, log_status):
3. """
4. Processes a list of data, validates it, calculates a sum, saves it to a file,
5. and updates a global status.
6. """
7. if len(data_list) > 0:
8. total_sum = 0
9.
10. # Data validation
11. if all(isinstance(item, (int, float)) for item in data_list):
12. for value in data_list:
13. total_sum += value
14.
15. # File I/O
16. try:
17. with open(file_path, 'w') as file:
18. file.write(f"User: {user_id}\n")
19. file.write(f"Data: {data_list}\n")
20. file.write(f"Total Sum: {total_sum}\n")
21.
22. global_status_record = {}
23. global_status_record['last_user'] = user_id
24. global_status_record['last_sum'] = total_sum
25. global_status_record['log_path'] = file_path
26. log_status.update(global_status_record)
27.
28. print("Data processed and saved successfully.")
29. return True
30. except IOError as e:
31. print(f"Error saving file: {e}")
32. return False
33. else:
34. print("Error: All data items must be numbers.")
35. return False
36. else:
37. print("Error: Data list cannot be empty.")
38. return False
39.
40. # Example usage
41. global_log = {}
42. sample_data = [10, 20, 30]
43. process_data_and_save(sample_data, 123, 'report.txt', global_log)
44.
45. print("\nGlobal Log:", global_log)
// Faithful C# translation — same structure, same deficiencies.
public bool ProcessDataAndSave(
List<object> dataList, int userId, string filePath,
Dictionary<string, object> logStatus)
{
if (dataList.Count > 0)
{
double totalSum = 0;
// Data validation
if (dataList.All(item => item is int || item is double))
{
foreach (var value in dataList)
totalSum += Convert.ToDouble(value);
// File I/O
try
{
using var file = new StreamWriter(filePath);
file.WriteLine($"User: {userId}");
file.WriteLine($"Data: {string.Join(\",\", dataList)}");
file.WriteLine($"Total Sum: {totalSum}");
var record = new Dictionary<string, object>
{
["last_user"] = userId,
["last_sum"] = totalSum,
["log_path"] = filePath
};
foreach (var kv in record) logStatus[kv.Key] = kv.Value; // ← mutates caller's dictionary
Console.WriteLine("Data processed and saved successfully.");
return true;
}
catch (IOException e)
{
Console.WriteLine($"Error saving file: {e}");
return false;
}
}
else
{
Console.WriteLine("Error: All data items must be numbers.");
return false;
}
}
else
{
Console.WriteLine("Error: Data list cannot be empty.");
return false;
}
}
List and briefly explain three deficiencies related to clean-code principles, stating the line numbers (of the Python listing) that contain them.
Model answer & rubric
Any three of the following, each named as a principle with lines cited:
- Function does too many things (SRP for functions / low cohesion). One function validates, sums, does file I/O, mutates a status record and prints — at least four jobs (lines 11–13 validation/sum, 17–20 I/O, 22–26 status update, 28/31/34/37 printing). A clean function does one thing.
- Deep nesting / arrow anti-pattern. Nested
if/if/trydrive the code four levels deep (lines 7, 11, 16, and the trailingelses at 33/36). Guard clauses (early return on empty/invalid) would flatten it. - Poor / vague naming and a misleading name.
process_data_and_saveannounces it does two things (the "and" is a smell);log_status,data_listare vague; the docstring even admits "updates a global status" (lines 2, 5, 26). - Long parameter list. Four parameters, one of them an output parameter (lines 2, 26) — hard to call correctly and a sign the function should be split or take an object.
- Dead / unreachable code & wrong scope. The "Example usage" (lines 40–45) is indented
inside the function body after all branches
return, so it can never run — it also calls the function recursively-looking at module level but sits in the wrong scope. Confusing and dead. - Magic values / booleans-for-errors. Returning
True/Falseto signal outcomes (lines 29, 32, 35, 38) instead of raising exceptions mixes control flow with error handling.
See clean functions (do one thing, small, few arguments) and naming/structure.
- ✔ Full credit (4): three distinct deficiencies, each named as a principle, briefly explained, with line numbers. Roughly 1.3 marks each — and the line-number citation is explicitly required, so an answer with no lines is capped.
- ◐ Partial (2): three issues but no line numbers, or two of them are really the same issue.
- ✘ Common mistake: listing three flavours of one problem (e.g. three naming complaints) — the examiner wants three distinct clean-code principles.
The function has a significant side effect. Identify the specific line(s) where it occurs, explain why it is a side effect, and discuss the main problems it introduces.
Model answer & rubric
Where: line 26 — log_status.update(global_status_record)
(built up on lines 22–25). This mutates the log_status dictionary that was passed in by the
caller. (Writing to the file, lines 17–20, and the prints are also side effects; but the
"significant" one the question targets is the hidden mutation of the caller-owned argument.)
Why it's a side effect: a function's observable output should ideally come through its return value. Here the function also reaches out and modifies state that lives outside its local scope — an object owned by the caller — as a hidden consequence of being called. The function's name promises "process and save"; nothing signals that it will silently rewrite the caller's dictionary. Its result is not conveyed solely by what it returns.
Problems it introduces:
- Hidden coupling & surprise. Callers who don't read the body won't know their dictionary changed — a source of bugs at a distance.
- Harder to test & reason about. The function isn't referentially transparent: the same inputs also depend on and alter external state, so you must set up and inspect that state to test it. Order of calls now matters.
- Concurrency hazards. Shared mutable state touched as a side effect is unsafe if two callers share the dictionary.
- Reduced reusability. The mutation entangles this function with the caller's data model.
The fix: return the status record and let the caller decide whether to merge it — keep the function free of hidden state changes. See side effects.
- ✔ Full credit (4): 1 for the correct line (26, the
log_statusmutation; build-up 22–25 accepted); 1.5 for a correct definition of side effect (modifies external/caller state, output not via return); 1.5 for at least two real problems (hidden coupling, testability, concurrency, reasoning). - ◐ Partial (2): points at file I/O or prints only, or names "side effect" without the caller-mutation insight.
- ✘ Common mistake: saying the side effect is "it returns True/False" — a return value is the opposite of a side effect. The side effect is the out-of-scope mutation.
Explain the three main steps of the TDD cycle. For each, describe the primary goal and what a developer does.
Model answer & rubric
Red → Green → Refactor, for the password-validator example:
- Red — write a failing test. Goal: specify the next tiny piece of behaviour before any production code. The developer writes a test for one rule (e.g. "rejects a password under 8 characters"), runs it, and watches it fail — proving the test actually exercises missing behaviour (and isn't a false pass).
- Green — make it pass. Goal: get to passing as quickly as possible. The developer writes the minimum production code needed to satisfy that one test — no more — and runs the suite until it is green. Resisting the urge to over-build is the discipline.
- Refactor — clean up. Goal: improve the design now that tests protect you. The developer removes duplication and improves names/structure in both test and production code, re-running the tests to confirm nothing broke. Behaviour is unchanged; quality improves.
Then repeat for the next rule. See the TDD cycle.
- ✔ Full credit (3): all three steps named, each with its goal and the developer action — 1 mark per step. Key nuances: Red must fail first; Green is the minimum code; Refactor keeps tests green.
- ◐ Partial (1–2): steps named but goal/action blurred (e.g. "write code then test").
- ✘ Common mistake: writing code first and adding tests after — that's test-after, not TDD. The test comes first and must fail.
Beyond "the code works", discuss two benefits of TDD that improve overall software design. How does TDD encourage more modular, maintainable code?
Model answer & rubric
Two design-level benefits (not just "fewer bugs"):
- Testability pressure forces good structure (modularity + loose coupling). To write a test first, the unit must be instantiable and its dependencies substitutable — which pushes you towards small classes, single responsibilities, and dependency injection (depending on interfaces so you can pass fakes). Hard-to-test code is a design smell TDD surfaces immediately, so the design comes out more modular and decoupled almost as a by-product.
- A safety net that enables fearless refactoring (maintainability). The growing test suite captures intended behaviour, so you can restructure and clean up continuously without fear of silent breakage. That keeps the design from rotting over time — maintainability is preserved because change is cheap and safe. The tests also serve as executable documentation of how each unit is meant to be used.
A third, if you have room: YAGNI/minimalism — writing only enough code to pass keeps the design lean, avoiding speculative complexity. See TDD.
- ✔ Full credit (4): two design benefits, each explained with the mechanism — 2 each. The strongest pair is "testability forces decoupling/modularity" and "tests enable safe refactoring / documentation".
- ◐ Partial (2): benefits stated but tied to "catches bugs" rather than to design quality.
- ✘ Common mistake: giving only correctness/QA benefits — the question explicitly asks about design, so modularity, coupling and maintainability must feature.
Question 4 — Enterprise & Distributed Architecture
Compare Service-Oriented Architecture and Microservices Architecture across: service granularity; communication style; data management; technology heterogeneity.
Model answer & rubric
| Dimension | SOA | Microservices (MSA) |
|---|---|---|
| Service granularity | Coarse-grained — larger, often enterprise-wide services covering broad business functions | Fine-grained — small services each owning a single business capability |
| Communication | Often via a centralised smart ESB (Enterprise Service Bus); heavier protocols (SOAP/WS-*); "smart pipes" | Decentralised, lightweight — "smart endpoints, dumb pipes"; REST/HTTP and asynchronous messaging; no central bus |
| Data management | Tends to share data / a common database across services | Database-per-service — each service owns its data privately (decentralised data) |
| Technology heterogeneity | More standardised; often a shared platform/protocol stack | Polyglot — each service can choose its own language, framework and datastore |
One-line summary worth stating: MSA is essentially SOA taken to a fine-grained, decentralised extreme — small independently-deployable services, private data, dumb pipes. See microservices vs SOA and SOA.
- ✔ Full credit (5): a correct SOA-vs-MSA contrast on each of the four dimensions (~1 each) plus coherence (~1) — granularity coarse vs fine, ESB vs dumb pipes, shared vs per-service data, standardised vs polyglot.
- ◐ Partial (2–3): some dimensions correct but data management or communication muddled.
- ✘ Common mistake: claiming SOA and MSA are "the same thing" — they share SOA principles but differ sharply on granularity, the ESB, and data ownership.
Explain how MVC's components map to the layers of the 3-Tier Architecture. Which tier does MVC primarily focus on, and why is that distinction important for web applications?
Model answer & rubric
The mapping (with a key caveat):
| MVC component | 3-Tier layer | Role |
|---|---|---|
| View | Presentation tier | Renders the UI the user sees |
| Controller | Presentation tier | Handles input, orchestrates the response |
| Model | Business-logic tier (and reaches into the Data tier) | Domain logic, rules, and the data it operates on |
Which tier MVC primarily focuses on: the Presentation tier. MVC is fundamentally a presentation-layer pattern — its concern is organising how a request is received, how logic is invoked, and how output is rendered. The View and Controller live entirely in presentation; the Model is MVC's link to the business/data tiers but MVC itself doesn't prescribe how those deeper tiers are built.
Why the distinction matters for web apps: the two are different kinds of decomposition, not rivals — 3-Tier is a physical/deployment layering (presentation ▸ logic ▸ data, often on separate hosts for scalability), while MVC is a logical organisation within the presentation tier. Recognising that MVC sits inside the presentation tier means you don't mistake "the Model" for the whole business/data tier: for a responsive, scalable web app you still layer the system into tiers (so the data and logic tiers scale independently) and apply MVC in the presentation tier to keep UI, input-handling and view rendering cleanly separated. Conflating them leads to fat controllers stuffed with business logic that should live in a separate tier. See MVC, 3-tier, and the MVC vs 3-tier comparison.
- ✔ Full credit (6): 3 for a correct component→layer mapping (View/Controller ▸ presentation, Model ▸ business/data); 1 for naming presentation as the primary focus; 2 for why it matters — MVC is logical organisation within a tier vs 3-tier's physical layering, and the consequence for scalable web apps.
- ◐ Partial (3): maps the components but says MVC "replaces" 3-tier or misses the primary-tier point.
- ✘ Common mistake: equating Model with the entire data tier and treating MVC and 3-tier as the same three-way split — they operate at different levels (logical vs physical).
Compare RMI and CORBA across: (i) language dependency; (ii) platform interoperability; (iii) interface definition; (iv) ease of use.
Model answer & rubric
| Dimension | Java RMI | CORBA |
|---|---|---|
| (i) Language dependency | Java-specific — both ends are (traditionally) Java | Language-independent — designed for multi-language systems (C++, Java, Python…) via language mappings |
| (ii) Platform interoperability | Runs anywhere a JVM runs, but effectively Java-to-Java; interoperability comes from the JVM, not from crossing languages | Strong cross-platform and cross-language interoperability via the ORB and the IIOP wire protocol |
| (iii) Interface definition | Interfaces are plain Java interfaces (extend
java.rmi.Remote) — no separate IDL |
Interfaces defined in a neutral IDL (Interface Definition Language), compiled to stubs/skeletons per target language |
| (iv) Ease of use | Generally easier for a typical developer — if you're already in Java it's simple, no separate IDL, natural object model | More complex — you must write IDL, run an IDL compiler, and deal with the ORB; heavier but more powerful for heterogeneous systems |
The essence: RMI trades interoperability for simplicity within Java; CORBA pays extra complexity (IDL + ORB) to buy genuine language/platform independence. See distributed objects / RPC.
- ✔ Full credit (8): 2 marks per dimension — Java-only vs language-neutral; JVM-bound vs ORB/IIOP cross-platform; Java interfaces vs neutral IDL; easier-in-Java vs more-complex-but-portable.
- ◐ Partial (4): two or three dimensions correct; IDL vs no-IDL is the one most often missed.
- ✘ Common mistake: saying RMI is language-independent (it isn't, traditionally) or forgetting that CORBA's defining feature is IDL-based, ORB-mediated interoperability.
For each scenario, recommend the most suitable communication style (one-to-one/one-to-many, synchronous/asynchronous) and justify: (i) Driver accepting a trip — request one driver, need an immediate accept/decline; (ii) Customer requesting a ride — notify many nearby drivers at once, no immediate response needed; (iii) Trip completion — charge Payment then notify the customer, must know payment succeeded before proceeding.
Model answer & rubric
- (i) Driver accepting a trip — One-to-one, Synchronous (request/response). Trip Management sends a request to a single Driver service and blocks for an immediate accept/decline. The need for one specific recipient and a prompt reply that gates the next step is the definition of synchronous request/response.
- (ii) Customer requesting a ride — One-to-many, Asynchronous (publish/notify). The system must notify multiple nearby drivers simultaneously and does not need an immediate reply from all — a broadcast/publish. Asynchronous one-to-many (e.g. publish/subscribe) fits: fire the notification to all nearby drivers and let acceptances arrive later, without blocking.
- (iii) Trip completion — One-to-one, Synchronous for the payment; then notify. Trip Management must charge Payment and know it succeeded before proceeding — that ordering/confirmation requirement demands a synchronous one-to-one request/response to the Payment service. Only after the success confirmation does it send the customer notification (which itself can be asynchronous fire-and-forget). The "must confirm before continuing" phrase is the tell for synchronous.
The deciding questions throughout: one recipient or many? and do I need the answer before I can continue? "Need an immediate/confirmed response" ⇒ synchronous; "notify many, don't wait" ⇒ asynchronous one-to-many. See event-driven / async messaging and microservices communication.
- ✔ Full credit (6): 2 per scenario — correct style and a justification tied to the "one vs many" and "need immediate response?" cues. (i) sync 1:1; (ii) async 1:many; (iii) sync 1:1 for payment (must confirm first).
- ◐ Partial (3): correct styles but justified only by restating the scenario, not the underlying cue.
- ✘ Common mistake: making (ii) synchronous (it explicitly needs no immediate response) or (iii) asynchronous for the payment (it explicitly must confirm success before proceeding).
Bonus paper — MSA patterns (written for this site, not a past paper)
Everything above this point is a real assessment, reproduced faithfully. This section is not. The 2025 final paper predates the Architectural and Design Patterns used in the Microservices Architecture lecture, so nothing above tests API Gateway code, sagas, CQRS, sidecars, idempotency or dead letter queues. These five questions are written in the same house style and mark allocation as Question 4 of the real paper, to give that material somewhere to be examined. Treat them as a well-informed guess at the shape, not as leaked questions. Source material: Chapter 11. 25 marks · 30 minutes.
(i) Name and describe the three states of the Circuit Breaker pattern, including what happens to a request
in each and what causes each transition. (ii) A developer implements the breaker with a single failure counter
and an isOpen flag stored as static fields shared by the whole process, set to true
once five failures have ever occurred. Identify three defects in this implementation.
Model answer & rubric
(i) The three states. Closed — requests flow normally and failures are counted; when the failure count/rate crosses the threshold the breaker trips to Open. Open — requests fail immediately without any network call being attempted, so no connection or thread is held; after a cooldown period elapses the breaker moves to Half-Open. Half-Open — exactly one trial request is admitted: if it succeeds the breaker returns to Closed and counters reset, if it fails the breaker returns to Open and the cooldown restarts.
(ii) Three defects. (1) The state is global. One flaky dependency trips the breaker for every other dependency in the process, so a failing payment provider also blocks calls to a perfectly healthy inventory service. Breaker state must be per-dependency. (2) There is no half-open state and no cooldown — once open, always open, so a two-second outage becomes a permanent one and the implementation contradicts the pattern it claims to implement. (3) The counter is cumulative, not windowed. Five failures spread over a month trip it exactly as surely as five in a row; real breakers track a failure rate over a rolling window and reset on success.
Creditworthy extras: no thread-safety around the shared counter; no fallback, so the caller still has no
answer for the user; hand-rolling it at all when Microsoft.Extensions.Http.Resilience/Polly
provides per-client policies. See Chapter 11 · Circuit
Breaker and Chapter 9.
- ✔ Full credit (6): 3 marks for the three states with their transitions (not just the names), 3 marks for three distinct defects — the global state and the missing half-open/cooldown are the two that must appear.
- ◐ Partial (3): states listed correctly but described as a list of names, or only one real defect found (usually "it's not thread-safe", which is true but the least important of the three).
- ✘ Common mistake: describing Open as "requests are queued until the service recovers" — the whole point is that they fail fast, with no call attempted at all.
An online pharmacy places an order across four services: Order, Prescription-Verification, Payment and Dispatch. Verification can take minutes (a pharmacist reviews it), payment must be refunded if dispatch finds the medicine unavailable, and the compliance team must be able to show, for any order, exactly which step it reached. Recommend choreography or orchestration and justify your recommendation. State one disadvantage of your choice.
Model answer & rubric
Orchestration. A central Order Saga Orchestrator issues commands (VerifyPrescription, ChargePayment, Dispatch) and drives the compensations (RefundPayment, CancelOrder) when a step fails. Justification tied to this scenario: (1) The compliance requirement is decisive. Orchestration persists the saga's state per order, so "which step did order 7781 reach?" is a single row to query; under choreography the answer only exists as a trail of events across four services' logs. (2) Verification takes minutes. A long-running step needs the saga's progress to survive restarts and deploys, which means persisted saga state and something that resumes it — exactly what an orchestrator provides. (3) Compensation ownership is explicit. When Dispatch reports the medicine unavailable, the orchestrator's saga state records which steps ran and therefore which undos are owed. Under choreography both Payment and Order subscribing to that failure event and each running its own compensation is perfectly correct — but nothing writes down who owns which undo, so with at-least-once redelivery and a growing workflow it is easy for a compensation to run twice or be dropped.
Disadvantage. The orchestrator is an additional service to build, deploy and monitor; it is a single point of failure for placing new orders, and a change bottleneck because every new business rule edits it. (Choreography's opposite trade — no new component, loose coupling — is what you are giving up.)
- ✔ Full credit (6): 4 marks for the recommendation with at least two mechanism-level reasons drawn from the scenario (auditability/queryable state, long-running step, compensation ownership); 2 marks for a genuine disadvantage of the chosen style.
- ◐ Partial (3): correct choice justified with adjectives ("easier to manage", "more reliable") rather than mechanisms, or a disadvantage of the rejected style instead of the chosen one.
- ✘ Common mistake: proposing a distributed transaction/2PC across the four databases — it requires every participant available at commit time, which is precisely the coupling microservices remove.
(i) Define idempotency in the context of a microservice endpoint, and explain why
POST /accounts/42/charge requires special handling while GET /orders/7781 does not.
(ii) Describe the four steps of the idempotency-key workflow. (iii) State one implementation mistake that
would leave the endpoint vulnerable despite using keys.
Model answer & rubric
(i) An operation is idempotent if calling it more than once with the same input has no
additional effect beyond the first call. This matters because distributed messaging is
at-least-once, not exactly-once: brokers redeliver, and clients whose request timed out
retry even when the server processed the original perfectly and merely lost the response. A GET
changes nothing, so repetition is harmless. A POST … /charge creates a new effect each time —
two deliveries charge the customer twice — so it needs a key that carries the caller's intent: "this is the
same request, not another one."
(ii) The workflow. ① Check — is this key already in the store? ② Executing — if it is new, claim it with status "Processing" and run the business logic. ③ Finished — store the response body against the key with status "Completed" (typically with a TTL). ④ Replay — if the same key arrives again, return the stored response without running the logic again.
(iii) One mistake (any of these). Implementing steps ① and ② as separate operations —
"check whether it exists, then insert" is a race that two simultaneous duplicates both win, so both charge;
the claim must be atomic (a unique-constraint insert, or Redis SET NX). Alternatively: letting
the claim, the side effect and the stored response commit separately, so a crash between "charged"
and "recorded" strands the key half-finished and refuses every retry while the money has already moved; not
scoping keys per caller, so two tenants collide; storing only "done" instead of the status code and the
response body, so the retrying client gets a different answer from the original; or keys with no expiry, so
the store grows forever.
- ✔ Full credit (5): 2 for the definition plus the at-least-once reason for the GET/POST contrast, 2 for all four workflow steps in order, 1 for a genuine implementation mistake.
- ◐ Partial (2–3): defines idempotency correctly but explains the contrast as "GET is read-only" without mentioning retries/at-least-once delivery; or gives three of the four steps.
- ✘ Common mistake: claiming idempotency means "the request only arrives once" — it means repeated arrivals cause no additional effect; you cannot prevent the duplicates, only neutralise them.
Explain what CQRS separates and why. State one benefit and one cost, and name the specific user-visible problem that the cost causes.
Model answer & rubric
CQRS separates the model used to change state (commands: create/update/delete, returning success or an id rather than data) from the model used to read state (queries, returning data and changing nothing). The two jobs want opposite things: writes want normalisation, validation and enforced invariants; reads want denormalised, pre-joined shapes tuned for one screen — and reads usually outnumber writes by orders of magnitude.
Benefit: each side can be modelled, optimised and scaled independently — for example a relational write store with real transactions alongside a document store or cache tuned purely for read speed. Cost: the read model is kept up to date by a projector consuming events, so it is only eventually consistent, and there are now two stores plus a projector to build and monitor. The user-visible problem is read-your-own-writes: a user saves a change, the query hits a read model that has not caught up, and they are shown the old value — "I saved it and it didn't appear." Standard fixes are to return the new state directly from the command, or to serve that user's own recent reads from the write model.
Creditworthy: noting that CQRS does not require event sourcing, and that a separate read database is an optional escalation rather than part of the definition.
- ✔ Full credit (4): 2 for what is separated and why the two workloads conflict, 1 for a benefit, 1 for the cost named as eventual consistency / read-your-own-writes.
- ◐ Partial (2): "separates reads from writes" with no account of why that helps or what it costs.
- ✘ Common mistake: equating CQRS with event sourcing, or claiming it removes the need for transactions.
(i) Explain the Sidecar pattern and give two responsibilities typically moved into a sidecar. (ii) The slides say your service must be "sidecar-aware" by exposing health checks. A team puts a database connectivity check into their liveness probe. Explain what happens during a 30-second database outage, and what they should have done instead.
Model answer & rubric
(i) A sidecar is a helper component deployed alongside the main service — in the same pod/deployment unit, sharing its lifecycle and local network — that takes over peripheral, cross-cutting concerns so the service contains only business logic. Typical responsibilities: mutual TLS between services, retries and timeouts on outgoing calls, metrics and distributed tracing, service discovery and load balancing. It is provided at the infrastructure level (Envoy, Istio) rather than written into the application, and the main benefit is that changing, say, the retry policy is one configuration change rather than a library bump and a redeploy of every service in every language.
(ii) A failing liveness probe means "this process is broken beyond recovery — restart it." So during the 30-second database outage every pod fails liveness simultaneously and the platform restarts all of them at once, turning a brief dependency blip into a full application outage — and the restarted pods still cannot reach the database, so they may crash-loop. Dependency checks belong in the readiness probe, which means "don't send me traffic right now": failing it removes the pod from the load balancer and puts it back automatically once the database recovers, with no restarts. Liveness should check only that the process itself is functioning.
- ✔ Full credit (4): 2 for the pattern with two genuine cross-cutting responsibilities, 2 for the liveness failure mode (mass simultaneous restart, blip becomes outage) and the readiness fix.
- ◐ Partial (2): describes the sidecar correctly but answers (ii) as "the health check fails" without explaining that liveness triggers a restart while readiness only removes traffic.
- ✘ Common mistake: listing business logic as something to move into the sidecar — cross-cutting infrastructure moves out; the domain rules are the one thing that must stay.
How you did — self-grading & where to go next
Mark each paper out of its total, then read the band you landed in. A mock is only useful if a dropped mark turns into a chapter you re-read — the pointers below map every question topic to the exact section that fixes it.
- Assignment 1 (Parking Lot, /100) — ≥70: you name each relationship and win the marks on the lifetime/ownership justification (Q3·F is 10 marks of pure justification); your SOLID answers give the mechanism (abstraction + new subclass, injected strategy, role interfaces), not just the principle's name; and your UML uses correct diamond ends and inheritance triangles. Below 70 usually means labels without justification, or OCP/ISP answered by naming rather than by showing the extension mechanism.
- Assignment 2 (Patterns, /100) — ≥70: you pick the right pattern and can argue it against the look-alike (Builder not Abstract Factory; Composite vs Decorator; Adapter vs Proxy; Iterator not Strategy), your UML is correctly notated, and the comparison questions are laid out as genuine contrasts. Below 70 typically means correct pattern, thin "why not the alternative".
- Final Exam 2025 (/100, four questions) — ≥70: you handle the full breadth — SOLID + UML (Q1), all three pattern families on one system (Q2), composition-over-inheritance + the clean-code/side-effect critique with line numbers + TDD (Q3), and the four architecture comparisons in crisp table form (Q4). Losing a whole sub-question (e.g. the side effect, or RMI-vs-CORBA's IDL point) is what drops you under 70.
- Bonus MSA paper (/25) — ≥70: your circuit-breaker answer describes the transitions and not just the three names, your saga recommendation is justified by mechanisms drawn from the scenario rather than adjectives, and you can state the cost of CQRS as read-your-own-writes. Below 70 is almost always the same thing: naming patterns correctly but justifying them with vibes. Fix it at Chapter 11 and the saga drills in Studio 16 · §5.
Turn weak spots into a reading list:
- Object relationships / diamonds wrong (A1 Q3·F–G, Ex Q1·B) → Ch 1 · relationships and UML notation.
- Encapsulation / abstraction (A1 Q1, Q2) → Ch 1 · the four pillars.
- Polymorphism & overriding (A1 Q3·C–E, Ex Q1·A) → Ch 1 · polymorphism.
- SOLID — SRP/OCP/LSP/ISP/DIP (A1 Part 2, Ex Q1, Q3·A) → SRP · OCP · LSP · ISP · DIP.
- Composition over inheritance (Ex Q3·A) → Ch 1 + Strategy.
- Creational patterns (A2 Q1–Q3, Ex Q2·A) → Factory Method · Abstract Factory · Builder, and FM vs AF.
- Structural patterns (A2 Q4–Q5, Ex Q2·B) → Adapter · Proxy · Decorator · Composite, and the wrapper litmus / Composite vs Decorator.
- Behavioural patterns (A2 Q6, Ex Q2·C) → Observer · Template Method · Iterator, and Template vs Strategy.
- Clean code / side effects / TDD (Ex Q3·B–C) → clean functions · side effects · TDD cycle.
- Architecture — SOA/MSA, MVC/3-tier, RMI/CORBA, comms (Ex Q4) → 3-tier · MVC · SOA · MSA vs SOA · async messaging, and MVC vs 3-tier.
Notice how often the marks live in the second half of the answer: not "it's aggregation" but "because the item outlives the order"; not "use OCP" but "so the tested processing code is never edited"; not "Proxy" but "because access must be lazy, cached and authorised". Naming is table stakes. Across all three papers, the examiner is paying for justification against the tempting alternative — the same judgement the module quiz drills. When you can produce that argument unprompted, under time, you're ready.