STUDIO 16 · DO THE WORK

Critique & Architecture Studio

The exam's second half doesn't ask you to recite — it hands you flawed code and says "list the deficiencies with line numbers", hands you a proud inheritance tree and says "critique it", and hands you an architecture decision and says "justify it". This studio trains exactly those muscles: smell-spotting on numbered listings, anti-pattern naming, composition redesign, test-driven development, and architecture articulation — each with a commit-before-reveal drill, and three of them backed by real .NET labs you drive to green.

ℹ️ How this studio works

Every step is one of three kinds. Decide (purple): commit an answer in the box before opening hints or the model — the box saves locally, so be honest. Build (green): open the matching lab in Labs-Code/labs/ and make the tests pass. Articulate (blue): write the exam-style justification, then compare against the model answer and marking rubric. Background refreshers: Chapter 7 (Clean Code), Chapter 2 (SOLID), Chapter 8 (Architectural Patterns) and Chapters 910 (Microservices).

1 · Code-critique drills — deficiencies, with line numbers

The past paper gives you a numbered listing and asks: "List and briefly explain three deficiencies related to clean code principles. State the line numbers." Then it follows up: "identify the specific line(s) where a side effect occurs and explain why it is a side effect." The skill is the same every time — read the code as a suspicious reviewer, name each smell with its Chapter 7 rule, and pin it to a line. Three fresh drills follow, hardest last. For full marks a deficiency needs three parts: the line number, the name of the smell, and the rule it breaks.

Drill 1 — the gym sign-up validator

Decide List at least four deficiencies — line numbers + smell + rule

A junior teammate submits this member-validation function for a gym-management system. Review it exactly as the exam asks: every deficiency you list must cite line numbers.

1public static List<string> Errs = new(); 2 3// checks the member 4public static bool Chk(string n, string e, int a, bool flag) 5{ 6 Console.WriteLine("checking " + n); 7 if (n != null && n.Length > 0) 8 { 9 if (a >= 16 && a <= 100) 10 { 11 if (e.Contains("@")) 12 { 13 if (flag == true) 14 { 15 // premium members skip the waiting list 16 Errs.Clear(); 17 return true; 18 } 19 return true; 20 } 21 else { Errs.Add("bad email"); return false; } 22 } 23 else { Errs.Add("bad age"); return false; } 24 } 25 Errs.Add("bad name"); 26 return false; 27}
Your answer is saved locally in your browser.
Hint 1 — nudge

Read the signature alone (line 4). How many of Chapter 7's naming and argument rules can one line break? Then ask what a function called Chk has any business doing to the console or to a field it doesn't own.

Hint 2 — stronger

Trace what happens to Errs across two consecutive calls: a failing call followed by a premium success. Who else reads Errs, and what did line 16 just do to them? That's your side effect. Also count nesting levels at line 13.

Model answer & rubric

Annotated review — each finding is line → smell → rule:

  • L4Cryptic, non-intention-revealing names (Chk, n, e, a, flag) — breaks use intention-revealing names. Should be IsEligibleMember(name, email, age, isPremium).
  • L4, L13Flag argument bool flag — breaks avoid flag arguments; the function loudly admits it does more than one thing. Split the premium path out or make it data, not a branch.
  • L1, L16, L21, L23, L25Hidden side effect via shared mutable static state: a function named like a pure query mutates the public global Errs, and line 16 silently erases errors recorded by earlier calls. Breaks no side effects and command–query separation. Results should be returned, not smuggled through a global.
  • L6Console I/O inside validation logic — a second side effect and a Single-Responsibility (SRP) violation: validation is now welded to a UI concern and untestable without capturing stdout.
  • L7–L24Arrow-shaped deep nesting (4 levels) — breaks small functions / minimal indenting. Invert to guard clauses: if (string.IsNullOrEmpty(name)) return Fail("bad name");
  • L9Magic numbers 16, 100 — breaks searchable names; extract MinimumAge/MaximumAge constants.
  • L13flag == true — redundant comparison noise; write if (isPremium).
  • L3, L15Noise / misleading comments — line 3 restates nothing usefully; line 15 "documents" behaviour (skipping a waiting list) that the code doesn't even implement. Breaks comments must not lie.
Marking rubric
  • ✔ Full credit: four+ deficiencies, each with line number(s), smell name, and the rule broken; the Errs global identified as a side effect (not just "bad style") with the two-calls corruption argument.
  • ◐ Partial: smells named but no line numbers, or lines cited with vague labels ("messy", "bad naming") and no rule.
  • ✘ Common mistake: calling line 6 the only side effect and missing that the real damage is the mutable static Errs — printing is visible; corrupted shared state is not.

Drill 2 — the invoice god-function (this one you also fix)

Decide Critique it — then you'll refactor it under a test harness

This billing function ships in a point-of-sale system. Same exam format: deficiencies with line numbers, and pay special attention to the question "where does a side effect occur, and why is it a problem?" — this listing has a textbook one.

1public static class Billing 2{ 3 public static List<string> AuditTrail = new(); 4 5 public static string GenerateReport(List<LineItem> items, string customer, 6 decimal taxRate, bool applyLoyalty) 7 { 8 if (items == null || items.Count == 0) return "ERROR: no items"; 9 10 decimal t = 0m; 11 for (int i = 0; i < items.Count; i++) 12 { 13 t = t + items[i].Price * items[i].Qty; 14 } 15 16 decimal d = 0m; 17 if (applyLoyalty == true) { d = t * 0.10m; } 18 decimal sub = t - d; 19 decimal tax = sub * taxRate; 20 decimal total = sub + tax; 21 22 string r = ""; 23 r += "INVOICE for " + customer + "\n"; 24 for (int i = 0; i < items.Count; i++) 25 { 26 r += items[i].Name + " x" + items[i].Qty + " = " 27 + (items[i].Price * items[i].Qty).ToString("0.00") + "\n"; 28 } 29 r += "Subtotal: " + t.ToString("0.00") + "\n"; 30 if (applyLoyalty == true) { r += "Loyalty -10%: -" + d.ToString("0.00") + "\n"; } 31 r += "Tax: " + tax.ToString("0.00") + "\n"; 32 r += "TOTAL: " + total.ToString("0.00") + "\n"; 33 34 AuditTrail.Add(customer + ":" + total.ToString("0.00")); 35 36 return r; 37 } 38}
Your answer is saved locally in your browser.
Hint 1 — nudge

Count the distinct jobs this one method performs: validating, summing, discounting, taxing, formatting… is that all? What does line 34 do that a caller reading the signature string GenerateReport(...) would never expect?

Hint 2 — stronger

A side effect is any observable state change beyond the returned value. Line 34 writes to state that outlives the call and is shared by everyone. Now imagine calling GenerateReport twice to preview an invoice — what is in AuditTrail afterwards, and which principle in Chapter 7 §Functions names this?

Model answer & rubric
  • L5–L37God function / does more than one thing: validates, calculates, formats and audits — four responsibilities in one method. Breaks functions do one thing and SRP.
  • L3 + L34The hidden side effect: a method whose name promises "generate a report string" also mutates public global state. Why it's a side effect: the observable change (a new AuditTrail entry) is not part of the return value and not hinted at by the name — it's a lie of omission. Problems it introduces: (1) temporal coupling — calling the function twice audits twice, so "preview" and "commit" are indistinguishable; (2) untestability — tests must reset global state or they interfere with each other; (3) thread-unsafety — a shared mutable List with no locking; (4) callers can never reason locally about the function.
  • L10, L16, L18, L22Mentally-mapped single-letter names (t, d, sub, r) — breaks intention-revealing names.
  • L17, L30applyLoyalty == true noise, flag argument steering two code paths (avoid flag arguments), and the loyalty rule (10%) duplicated as a magic number.
  • L8Stringly-typed error handling: returning "ERROR: no items" in the same channel as a valid report forces every caller to string-match. Prefer an exception or result type (prefer exceptions to error codes).
  • L22–L32Mixed abstraction levels: penny-level string concatenation sits beside business policy. Breaks one level of abstraction per function.
Marking rubric
  • ✔ Full credit: L34 (with L3) named as the side effect plus the "why" (state change invisible in name/return) plus at least two concrete problems (repeat-call corruption, test interference, thread-unsafety).
  • ◐ Partial: side effect line found but justified only as "it uses a global variable" without the consequences.
  • ✘ Common mistake: naming L8's early return or L23's concatenation as "the" side effect — neither mutates state that outlives the call.
Build Lab C01 — refactor it without changing its output

Critiquing is half the mark; now do the fix. Lab C01-cleanup-kata contains exactly this function plus a safety net: behaviour tests that pin its current output byte-for-byte. Heads-up — this lab deviates from the usual red-first rule: the behaviour tests already pass in the Starter (they pin what the code does today), while three design tests start red. Your job is to extract InvoiceCalculator (pure maths) and InvoiceFormatter (string building) and delete AuditTrail — keeping green green while turning red green.

cd Labs-Code/labs/C01-cleanup-kata/Starter && dotnet test
Reference shape & what to notice
// after the refactor: three small pieces, no globals
public static class InvoiceCalculator          // pure arithmetic — trivially testable
{
    public static decimal CalculateSubtotal(List<LineItem> items) { /* ... */ }
    public static decimal CalculateTotal(List<LineItem> items, decimal taxRate, bool applyLoyalty) { /* ... */ }
}

public static class InvoiceFormatter           // numbers -> report string, nothing else
{
    public static string Format(List<LineItem> items, string customer, decimal taxRate, bool applyLoyalty) { /* ... */ }
}

public static class Billing                    // thin composition root
{
    public static string GenerateReport(List<LineItem> items, string customer, decimal taxRate, bool applyLoyalty)
    {
        if (items == null || items.Count == 0) return "ERROR: no items";
        return InvoiceFormatter.Format(items, customer, taxRate, applyLoyalty);
    }
}

What to notice: the behaviour tests never went red — that is what makes this a refactoring (behaviour-preserving) rather than a rewrite. And auditing didn't get "lost": deleting AuditTrail forces the caller who actually needs an audit log to ask for one explicitly — the dependency becomes visible instead of hidden. That is the Boy Scout Rule (Chapter 7) executed with a harness.

Drill 3 — the weather cache (hardest: two smells hide behind a try)

Decide List deficiencies with line numbers — there are at least six
1static Dictionary<string, double> cache = new(); 2 3/// <summary>Gets the temperature.</summary> 4public static double GetTemp(string station, bool refresh) 5{ 6 double t = 0; 7 try 8 { 9 if (refresh == false && cache.ContainsKey(station)) 10 { 11 return cache[station]; 12 } 13 var raw = File.ReadAllText("/data/" + station + ".txt"); 14 t = double.Parse(raw) * 1.8 + 32; // convert 15 cache[station] = t; 16 File.AppendAllText("/data/log.txt", station + "\n"); 17 } 18 catch (Exception) 19 { 20 } 21 return t; 22}
Your answer is saved locally in your browser.
Hint 1 — nudge

The name says Get. Count the things this "getter" writes. Then ask: if the file for a station doesn't exist, what number does the caller receive, and how would they ever know something went wrong?

Hint 2 — stronger

Line 14's comment says "convert" — convert what to what? And notice the listing's formatting itself (line 14's indentation) — inconsistent formatting is a listed clean-code deficiency too.

Model answer & rubric
  • L18–L20Swallowed exception: an empty catch (Exception) turns every failure (missing file, bad data, I/O error) into a silent, plausible-looking 0. Breaks don't return/accept null-ish error codes; use exceptions honestly — this is worse than an error code, it's no signal at all. 0 °F is also a valid temperature, so failures are indistinguishable from data.
  • L16Hidden side effect #1: a query named GetTemp appends to a log file. Command–query separation broken (no side effects).
  • L1, L15Hidden side effect #2: mutation of shared static cache — global mutable state with the same testability/threading problems as Drill 2's AuditTrail.
  • L4, L9Flag argument refresh (plus == false noise) — two functions pretending to be one.
  • L14Magic numbers with a useless comment: * 1.8 + 32 is Celsius→Fahrenheit, but "// convert" explains nothing. Also a silent unit decision buried in a line — name it (CelsiusToFahrenheit(...)) instead of commenting it. Bonus: the line's broken indentation is a formatting deficiency in its own right (Chapter 7).
  • L3Noise doc-comment that restates the name and hides the two side effects — a comment that actively misleads.
  • L6 + L21Misleading default: initialising t = 0 just so the catch path has something to return is the mechanism that makes the swallowed exception dangerous.
  • L13Hard-coded path + string-built path — environment coupling that makes the function untestable without a real file system (a seam an injected IFileStore would fix — see DIP).
Marking rubric
  • ✔ Full credit: six findings with lines; the swallowed exception explained via the "0 looks like data" argument; both side effects (L15 cache, L16 log) found.
  • ◐ Partial: side effects found but the empty catch labelled merely "bad practice" without explaining the consequence.
  • ✘ Common mistake: calling the cache read (L11) a side effect — reading isn't; the writes on L15/L16 are.

2 · Anti-pattern gallery — name it, then plan the escape

Patterns are reusable solutions; anti-patterns are reusable mistakes. The tutorials call out a family of them, and the exam loves "what is wrong with this design and how would you fix it?". For each specimen: commit (1) the anti-pattern's name and (2) a concrete escape plan. Two more to know by name, no specimen needed: Spaghetti Code (no discernible structure — control flow jumps everywhere, everything touches everything) and Premature Optimization (complexity added for speed nobody measured — "the root of all evil" until a profiler says otherwise).

Specimen A

Decide Name the anti-pattern, propose the escape
public class ClinicManager
{
    // patients
    public void RegisterPatient(string name) { /* ... */ }
    public void UpdateMedicalHistory(int id, string entry) { /* ... */ }
    // appointments
    public void BookAppointment(int patientId, DateTime when) { /* ... */ }
    public void SendReminderSms(int patientId) { /* ... */ }
    // billing
    public decimal CalculateInvoice(int patientId) { /* ... */ }
    public void ChargeCard(string cardToken, decimal amount) { /* ... */ }
    // persistence + reporting + config...
    public void SaveAll() { /* ... */ }
    public string ExportMonthlyReport() { /* ... */ }
    public void ReloadSettings() { /* ... */ }
}
Your answer is saved locally in your browser.
Hint 1 — nudge

How many reasons to change does this class have? Count the stakeholder groups (medical staff, reception, finance, IT) who could each demand an edit to it.

Model answer & rubric

God Object (a.k.a. The Blob): one class that knows everything and does everything — patients, scheduling, SMS, billing, persistence, reporting, config. It is the walking negation of SRP: at least five reasons to change, so every team edits the same file, every change risks every feature, and nothing can be tested in isolation. Escape: split along responsibilities — PatientRegistry, AppointmentScheduler, BillingService, ReportExporter — each behind an interface. If clients enjoyed the one-stop convenience, keep a thin Facade that delegates to the parts; the facade owns no logic. Refactor incrementally: extract one responsibility at a time behind a characterisation-test net (exactly the C01 move, scaled up).

Marking rubric
  • ✔ Full credit: names God Object, argues via "reasons to change"/SRP, escape = decompose by responsibility + optional Facade.
  • ◐ Partial: "it's too big, split it" without a principle or a target shape.
  • ✘ Common mistake: proposing to split by method count (Manager1, Manager2) rather than by responsibility.

Specimen B

Decide Name the anti-pattern, propose the escape

A team that recently learned Singleton ships this codebase:

public sealed class Logger        { public static Logger Instance { get; } = new(); /* ... */ }
public sealed class Config        { public static Config Instance { get; } = new(); /* ... */ }
public sealed class DbConnection  { public static DbConnection Instance { get; } = new(); /* ... */ }
public sealed class Cache         { public static Cache Instance { get; } = new(); /* ... */ }
public sealed class EmailSender   { public static EmailSender Instance { get; } = new(); /* ... */ }
public sealed class TaxCalculator { public static TaxCalculator Instance { get; } = new(); /* ... */ }
// ...14 more Instance properties. "It worked for Logger, so..."
Your answer is saved locally in your browser.
Hint 1 — nudge

The problem isn't any single class — it's the habit. "When all you have is a ___, everything looks like a ___."

Model answer & rubric

Golden Hammer: one familiar tool (here Singleton) applied to every problem regardless of fit. TaxCalculator has no single-instance requirement at all — it's stateless maths. The failure mode: twenty pieces of global state, hidden dependencies everywhere (any method can grab any Instance), impossible test isolation, and invisible coupling — the same disease as Drill 2's static, times twenty. Escape: re-derive each choice from the problem: keep a single instance only where one is genuinely required, and even then prefer letting a DI container manage lifetime while classes receive dependencies through constructors (DIP; see the Singleton caveats in Chapter 3). Culturally: add "what problem does this pattern solve here?" to code review.

Marking rubric
  • ✔ Full credit: names Golden Hammer (not "Singleton abuse" alone), identifies pattern-before-problem thinking, escape = constructor injection + per-case justification.
  • ◐ Partial: correctly attacks the global state but frames it as "Singleton is always bad".
  • ✘ Common mistake: escaping to a different hammer — "make everything static classes instead".

Specimen C

Decide Diagnose using the three decay symptoms

A reporting module. The team reports three complaints: adding a PDF format means editing this class and its switch; last month a change to CSV quoting broke the XML output; and they can't reuse the totalling logic in a new mobile app because it drags the database and SMTP code with it.

public class ReportService
{
    private readonly SqlDb _db = new SqlDb("Server=PROD01;...");
    private readonly SmtpMailer _mailer = new SmtpMailer("smtp.corp.local");

    public void SendMonthlyReport(string format)
    {
        var rows = _db.Query("SELECT * FROM sales");
        decimal total = 0;
        foreach (var r in rows) total += r.Amount;   // totalling logic, trapped here

        string body;
        switch (format)
        {
            case "csv": body = ToCsv(rows, total); break;
            case "xml": body = ToXml(rows, total); break;   // shares helpers with ToCsv
            default: throw new ArgumentException(format);
        }
        _mailer.Send("boss@corp.com", body);
    }
    /* ToCsv and ToXml share fragile private helpers... */
}
Your answer is saved locally in your browser.
Hint 1 — nudge

The three symptoms rhyme: hard to change, easy to break, impossible to move.

Model answer & rubric

The three classic decay symptoms, one per complaint:

  • Rigidity — hard to change: adding PDF forces edits to existing, working code (the switch), because the variation isn't encapsulated (OCP violation).
  • Fragility — breaks in unrelated places: CSV quoting change broke XML because both formats share tangled private helpers.
  • Immobility — can't be reused: the totalling logic is welded to a hard-coded SqlDb and SmtpMailer (DIP violation), so extracting it drags infrastructure along.

Escape: encapsulate what varies — an IReportFormatter strategy per format (Strategy) kills the switch; pure SalesTotaller for the maths; inject IDataSource and IReportSender abstractions so the service composes parts it doesn't construct. Each symptom then disappears for a named reason: new format = new class (rigidity), formats isolated (fragility), pure logic portable (immobility).

Marking rubric
  • ✔ Full credit: all three symptoms named and matched to the right complaint, escape cites OCP/DIP/Strategy.
  • ◐ Partial: symptoms listed but not mapped, or a generic "use interfaces" escape.
  • ✘ Common mistake: swapping rigidity and fragility — rigidity is cost to change, fragility is breakage elsewhere.

Specimen D

Decide The subtle one — the "clean" habit that became the smell

Three years ago, three price calculations looked identical, so a diligent developer unified them. Today the shared function looks like this:

// used by checkout, refunds, AND loyalty-points — "don't repeat yourself!"
public static decimal CalcAmount(decimal baseAmt, bool isRefund, bool isLoyalty,
                                 bool applyTax, bool roundDown, int tier)
{
    var amt = baseAmt;
    if (isLoyalty) amt = tier >= 2 ? amt * 1.5m : amt;        // points multiplier
    if (isRefund) amt = amt - (applyTax ? 0m : amt * 0.02m);  // refund fee... unless taxed?
    if (applyTax && !isRefund && !isLoyalty) amt *= 1.15m;
    return roundDown ? Math.Floor(amt) : Math.Round(amt, 2);
}
Your answer is saved locally in your browser.
Hint 1 — nudge

DRY says don't repeat knowledge. Were checkout, refunds and loyalty-points ever the same piece of knowledge — or did they merely look alike for a moment in time?

Hint 2 — stronger

The counter-principle is an acronym: Avoid Hasty Abstractions. Its slogan: prefer duplication over the wrong abstraction.

Model answer & rubric

Over-DRY — the wrong abstraction. Three coincidentally similar computations were merged as if they were one piece of knowledge. They then evolved apart, and every divergence was patched with another boolean parameter — the flag arguments are the tell (five parameters steering one function = three functions in a trench coat). Now every caller pays the complexity of all three domains, and changing refund rules risks checkout. The counter-principle is AHA — Avoid Hasty Abstractions: duplication is cheaper than the wrong abstraction, because duplication is visible and mechanical to unify later, while a wrong abstraction spreads conditionals everywhere. Escape: inline the shared function back into three honest ones — CalcCheckoutTotal, CalcRefundAmount, CalcLoyaltyPoints — let each keep only the branches it uses, and re-extract only what remains genuinely common (perhaps just the rounding policy). DRY real knowledge (the tax rate constant); don't DRY resemblance.

Marking rubric
  • ✔ Full credit: names over-DRY/wrong abstraction, cites AHA, spots flag-argument proliferation as the symptom, escape = un-merge then re-extract the truly common core.
  • ◐ Partial: "too many parameters, split it" without the DRY-vs-AHA tension.
  • ✘ Common mistake: concluding "DRY is wrong" — DRY is about knowledge; the mistake was abstracting a coincidence.

3 · Inheritance-abuse critique — from explosion to composition

Past-paper Q3.A in one line: a team lead proposes an inheritance hierarchy; analyse its problems, then propose a composition-based alternative and justify it. Here is a fresh hierarchy to attack. A courier startup prices deliveries with subclasses, one per service option — and one per combination of options:

public class Delivery
{
    protected decimal BaseCost;
    public Delivery(decimal baseCost) => BaseCost = baseCost;
    public virtual decimal Cost() => BaseCost;
}
public class ExpressDelivery       : Delivery { /* Cost() => BaseCost * 1.5m       */ }
public class RefrigeratedDelivery  : Delivery { /* Cost() => BaseCost + 8m         */ }
public class InternationalDelivery : Delivery { /* Cost() => BaseCost + 15m        */ }
// and then reality arrived:
public class RefrigeratedExpressDelivery              : Delivery { /* 1.5x + 8       */ }
public class InternationalExpressDelivery             : Delivery { /* 1.5x + 15      */ }
public class RefrigeratedInternationalExpressDelivery : Delivery { /* 1.5x + 8 + 15  */ }
Decide Analyse the proposed design — what exactly goes wrong as it grows?

Play the examiner's game precisely: analyse this from a class-inheritance perspective. What happens when the business adds a fourth option, HazmatDelivery (+$25)? Give the growth argument with numbers.

Your answer is saved locally in your browser.
Hint 1 — nudge

Count classes for 3 options. Now count for 4. The formula hiding in there is not linear.

Hint 2 — stronger

Also look at where the pricing knowledge lives: how many classes repeat the "×1.5" rule? What happens when Express changes to ×1.6?

Model answer & rubric

Class explosion through combinatorial inheritance. Options are independent axes, but inheritance forces every combination to be a named class: 3 options already cost 6 subclasses; with Hazmat it's up to 24−1 = 15; n options → 2n−1. Each addition breaks OCP (you extend the codebase by writing yet more near-identical classes and touching client code that dispatches on type). Knowledge is duplicated — the ×1.5 rule appears in four classes, so a price change is a shotgun edit (fragility). Combinations are frozen at compile time: a parcel can't become refrigerated at runtime (the explosion problem, Chapter 2). And the hierarchy misuses is-a: "RefrigeratedInternationalExpressDelivery" is not a kind of delivery, it's a delivery with three add-ons — has-a wearing an is-a costume.

Marking rubric
  • ✔ Full credit: names class explosion with the 2n argument, plus two of: duplication/shotgun edits, OCP violation, no runtime composition, is-a misuse.
  • ◐ Partial: "too many classes" without quantifying, or only the duplication point.
  • ✘ Common mistake: critiquing inheritance in general ("inheritance is bad") — the flaw is using it for independent, combinable options, not its existence.
Articulate Propose the composition-based redesign — and defend it

Exam voice: "Propose an alternative design that favours object composition over class inheritance. Describe how it is structured using an interface, and justify why it is more flexible and maintainable." Sketch the UML on paper before revealing the model diagram.

Your answer is saved locally in your browser.
Hint 1 — nudge

Stop asking "what kinds of delivery exist?" and ask "what varies?" Each option is just a rule for extra cost. Give that rule a name and an interface.

Model answer & rubric

Redesign: one interface IShippingSurcharge { decimal Apply(decimal baseCost); }; one small class per option (ExpressSurcharge, RefrigeratedSurcharge, InternationalSurcharge); a single Shipment that has a list of surcharges and sums them over the base cost. Any combination is a list, not a class.

Shipment - baseCost: decimal - surcharges: List + TotalCost(): decimal «interface» IShippingSurcharge + Apply(baseCost): decimal has 0..* ExpressSurcharge RefrigeratedSurcharge InternationalSurcharge new option = one new class implementing the interface — nothing else changes
Figure: the composed redesign — options plug into Shipment; combinations are data, not classes.

Justification (this is where the marks are): options become composable at runtime — any subset is a list, so n options need n classes, not 2n−1. Adding Hazmat = one new class, zero edits to Shipment or existing surcharges — OCP satisfied. Each pricing rule lives in exactly one place (no shotgun edits). Shipment depends only on the abstraction (program to an interface, DIP), and each tiny class is testable alone. That is the concrete content of "favour composition over inheritance": inheritance freezes a static taxonomy at compile time; composition assembles behaviour per object at runtime. Common drawing mistakes: using a closed triangle (inheritance) instead of the dashed open-triangle realization to the interface; drawing Shipment→surcharge as inheritance instead of an aggregation/association with multiplicity 0..*; forgetting the «interface» stereotype.

Marking rubric
  • ✔ Full credit: interface + concrete strategies + composed container, and a justification citing runtime flexibility, OCP and single-place knowledge — explicitly contrasted against the 2n tree.
  • ◐ Partial: correct structure but justification limited to "it's cleaner".
  • ✘ Common mistake: replacing the subclass tree with an enum + switch inside Shipment — that re-centralises the variation and re-breaks OCP.
Build Lab C03 — make the tests demand your redesign into existence

Lab C03-composition-redesign hands you the flawed tree (Legacy.cs, for critique only) and red tests that specify the composed design. Implement IShippingSurcharge, the three surcharges and Shipment until green — the final test asserts the extension point stays an interface, so the design itself is under test.

cd Labs-Code/labs/C03-composition-redesign/Starter && dotnet test
What green proves

Step5_ArbitraryCombination_JustComposes is the money test: express+refrigerated+international priced by one class fed a three-element list. When it passes, you have physically demonstrated the sentence the exam wants you to write. Note also what you deleted: all six legacy subclasses, with no behaviour lost — the strongest possible evidence that the taxonomy carried no real knowledge.

4 · TDD kata — red, green, refactor, for real

The exam asks two things about TDD (Chapter 7 refresher): describe the three steps of the cycle, and explain how TDD improves design, not just correctness. Both answers are easier to write after you've felt the rhythm — so this section is mostly a lab. The three laws first:

💡 The Three Laws of TDD
  • Law 1: write no production code except to pass a failing test.
  • Law 2: write no more of a test than is sufficient to fail (not compiling counts as failing).
  • Law 3: write no more production code than is sufficient to pass the currently failing test.

The laws force a loop measured in minutes: RED (a failing test states the next requirement) → GREEN (minimum code to pass — resist cleverness) → REFACTOR (clean up code and tests under a green bar).

Decide What is the first test?

You must build a ride-fare calculator: base fare $2.50, $1.20/km, $0.30/min, a surge multiplier, a $5.00 minimum fare, and rejection of negative inputs. You are about to write the first test. Which rule does it cover, and — just as important — which rules does it deliberately ignore? Write the test (xUnit, roughly) in the box.

Your answer is saved locally in your browser.
Hint 1 — nudge

Law 2: no more test than is sufficient to fail. The simplest business-meaningful slice is base+distance. Surge, floors and validation are later reds.

Model answer & rubric
[Fact]
public void Fare_IsBasePlusDistance()
{
    Assert.Equal(14.50m, FareCalculator.Fare(distanceKm: 10m, minutes: 0m, surge: 1.0m));
}

One rule, one assertion, neutral values for everything else (0 minutes, surge 1.0). It ignores time, surge, the minimum and validation on purpose — each of those deserves its own red. Starting with the error cases ("negative distance throws") is legal but weak: it forces you to design the signature around exceptions before the happy path has shaped it.

Marking rubric
  • ✔ Full credit: a single-rule failing test plus the reasoning that other rules wait their turn (Law 2).
  • ◐ Partial: a correct test but bundling three rules into it.
  • ✘ Common mistake: writing the whole FareCalculator mentally first, then a test that merely confirms it — that's test-after in disguise.
Build Lab C02 — five stages, uncommented one at a time

Lab C02-tdd-kata stages the whole kata for you: only Stage 1's test is active; Stages 2–5 (time, surge, minimum fare, input validation) are commented out. The discipline: make the active stage green with minimum code, refactor, then uncomment exactly one more. Never two reds at once.

cd Labs-Code/labs/C02-tdd-kata/Starter && dotnet test
What to notice while you work

At Stage 1 the honest minimum is almost embarrassing (return 2.50m + distanceKm * 1.20m;) — that's correct TDD, not laziness; generality must be demanded by a test. Around Stage 3 you'll feel the pull to restructure into (base + distance + time) * surge — do it in the refactor step, under green. Stage 4's minimum-fare rule will fight the naive formula and force an explicit policy line — notice how the test drove that decision into the open. The finished shape (small, pure, guard-clauses first) was never "designed"; the tests squeezed it into existence.

Articulate The exam question: how does TDD improve the design?

"Beyond simply ensuring the code works, discuss two key benefits of practising TDD that impact the overall quality of the software design. How does TDD encourage code that is more modular and easier to maintain?" Use evidence from the kata you just did.

Your answer is saved locally in your browser.
Hint 1 — nudge

Benefit 1 hides in the phrase "test-first means client-first". Benefit 2 hides in what the green bar let you do at every refactor step.

Model answer & rubric

1 — Testability pressure forces modularity. Because the test is written first, you design the API from the caller's side, and anything hard to test (hidden globals, file I/O, tangled dependencies) hurts immediately — so it gets designed out. FareCalculator came out pure and dependency-free because a static, side-effecting version would have been painful to test from the first red. At scale, this same pressure pushes toward small units, injected dependencies (DIP) and single responsibilities — TDD is a design tool wearing a testing costume. 2 — The green bar makes refactoring (and all future change) safe. The suite is a regression net built as a free by-product: at Stage 3 you restructured the whole formula with zero fear, because five seconds later the bar said "still correct". That is the exact mechanism that keeps code maintainable for years — the Boy Scout Rule is only practical when a net exists (you used the same trick in Lab C01, where pinned tests made a scary refactor routine). Secondary credit: the tests are executable, always-current documentation of every fare rule.

Marking rubric
  • ✔ Full credit: both mechanisms (design pressure → modularity; regression net → fearless change) each tied to a concrete moment in the kata.
  • ◐ Partial: "you find bugs early and have documentation" — true, but those are correctness benefits, and the question explicitly asks beyond correctness.
  • ✘ Common mistake: restating the red-green-refactor steps instead of answering why the design improves.

5 · Architecture articulation drills — the Q4 gauntlet

Question 4 is pure articulation: no code, just "compare X and Y across these dimensions" and "recommend a communication style and justify". The trap is answering with adjectives ("MSA is more scalable") instead of mechanisms. Commit full written answers here — these drills mirror the exam's question shapes. Refreshers: SOA vs MSA, MVC & 3-tier, IPC technologies, interaction styles.

Articulate Drill A — SOA vs MSA on four axes

A retail bank runs a monolith and is choosing between SOA and MSA. Compare them across service granularity, communication style, data management and technology heterogeneity — one crisp contrast per axis, mechanisms not vibes.

Your answer is saved locally in your browser.
Hint 1 — nudge

For communication, name the middlebox: what sits between SOA services that MSA deliberately removed? For data, whose database is it?

Model answer & rubric
DimensionSOAMSA
Service granularityCoarse-grained: services wrap whole business domains or applications ("Loans service"), often shared enterprise-wideFine-grained: one service ≈ one narrowly-scoped capability ("Loan-Approval", "Loan-Statement"), independently deployable, owned by one small team
Communication styleCentralised through an Enterprise Service Bus (ESB); heavyweight standards (SOAP, WS-*); the bus does routing/transformation — smart pipesDecentralised, direct service-to-service: lightweight REST/gRPC or async message brokers — "smart endpoints, dumb pipes"; no single mediator
Data managementTypically shared enterprise databases / canonical data models across servicesDatabase-per-service: each service owns its schema privately; sharing happens via APIs/events, never via joins into someone else's tables
Technology heterogeneityConstrained by enterprise-wide standards (the ESB's protocols, canonical schemas) — mostly homogeneous stacksFree per service: each team picks the language/store fitting its job (polyglot programming and polyglot persistence), because the only contract is the API
Marking rubric
  • ✔ Full credit: all four axes, each a genuine contrast naming mechanisms (ESB vs dumb pipes; shared DB vs DB-per-service).
  • ◐ Partial: correct direction but mechanism-free ("SOA is bigger, MSA is smaller").
  • ✘ Common mistake: writing "MSA is better" conclusions — the question asks for comparison; SOA's centralised governance is a legitimate trade-off, not a defect.
Articulate Drill B — map MVC onto the 3-tier architecture

Exam phrasing: "Explain how the components of MVC map to the layers of 3-Tier. Which layer does MVC primarily focus on, and why is this distinction important for web applications?" Careful — this is a mapping question, not a definition dump.

Your answer is saved locally in your browser.
Hint 1 — nudge

Two of the three MVC letters live in the same tier. The third straddles a boundary. And remember the shapes: 3-tier is a line, MVC is a triangle.

Model answer & rubric
MVC component3-tier layerWhy
ViewPresentation tierRenders UI, displays model data to the user
ControllerPresentation tier (its input/coordination edge)Interprets user input and orchestrates — it contains no business rules itself
ModelBusiness tier — and it straddles into the data tierHolds business state + rules; persists itself via the data-access layer

The key sentence: MVC is primarily a pattern for organising the presentation tier — the whole triangle refines how UI concerns are separated, while the Model reaches down into the business (and, through it, data) tiers. The distinction matters for web apps because the presentation tier is where change is fastest and most parallel: many views (HTML, JSON API, mobile) must reuse one set of business rules, and UI specialists must work without touching domain logic. MVC gives the presentation tier internal structure that plain 3-tier ("everything UI-ish goes in tier 1") does not. They compose rather than compete: a typical web app is 3-tier and MVC — V and C in/near tier 1, M across tiers 2–3. (Full walkthrough: Chapter 8 §MVC.)

Marking rubric
  • ✔ Full credit: all three mapped, Model's straddling noted, "MVC ⊂ presentation-tier organisation" stated, and a web-specific reason (multiple views / parallel UI work).
  • ◐ Partial: naive one-to-one mapping (M→data, V→presentation, C→business) with no nuance — the C→business leg is the classic error.
  • ✘ Common mistake: treating MVC and 3-tier as competitors and "choosing" one.
Articulate Drill C — RMI vs CORBA (vs the modern analogue, gRPC)

Compare RMI and CORBA across language dependency, platform interoperability, interface definition and ease of use. Then add one sentence on where gRPC sits — the examiner asks the classic pair, but the modern mapping proves understanding.

Your answer is saved locally in your browser.
Hint 1 — nudge

One of the two is a single-vendor, single-language convenience; the other is a standards-committee, any-language contract system. Everything on all four axes follows from that split.

Model answer & rubric
DimensionRMICORBAgRPC (modern analogue)
Language dependencyJava-only — both ends must be JVMsLanguage-neutral by design (C++, Java, Ada, Python…)Language-neutral — codegen for a dozen+ languages
Platform interoperabilityWherever a JVM runs, but only JVM↔JVM; uses Java serialization + JRMPCross-OS/vendor via the standard IIOP wire protocol between ORBsCross-everything over HTTP/2 + Protocol Buffers
Interface definitionOrdinary Java interface extends Remote — no separate IDLSeparate IDL file compiled into stubs/skeletons per languageSeparate .proto file compiled into client/server stubs — CORBA's IDL idea, modernised
Ease of useEasier for a typical (Java) developer: one language, no IDL step, natural Java objectsHarder: IDL compilers, ORB configuration, vendor quirks, heavyweight specDeliberately easy: one .proto, generated code, wide tooling

The one-line summary the marker wants: RMI trades universality for simplicity (Java end-to-end, no IDL); CORBA trades simplicity for universality (any language, but through an IDL and ORB machinery). gRPC is the modern synthesis — CORBA's language-neutral IDL contract with RMI-grade developer ease, on commodity web infrastructure (see the comparison table in Chapter 9 §IPC technologies).

Marking rubric
  • ✔ Full credit: four axes each contrasted with the concrete artefact named (JVM/JRMP, IDL/IIOP/ORB), plus the trade-off summary.
  • ◐ Partial: right directions, no artefacts ("CORBA works with more languages").
  • ✘ Common mistake: saying RMI has an IDL — its whole point is that plain Java interfaces are the contract.
Articulate Drill D — choose the communication style, three scenarios

You are architecting TicketWave, a microservices event-ticketing platform (services: Booking, Seat-Inventory, Payment, Notifications, Fan-Alert). For each scenario choose along both dimensions — one-to-one vs one-to-many, synchronous vs asynchronous — and justify:

  1. Seat hold: when a fan taps a seat, Booking must ask Seat-Inventory to place a 5-minute hold and needs the yes/no before the UI can proceed.
  2. Tickets on sale: when an artist releases a new date, every fan who follows that artist should eventually get an app push — thousands of them; nothing waits on the result.
  3. Checkout completion: on purchase, Booking must charge Payment and must not issue the ticket unless the charge succeeds; afterwards Notifications emails the receipt — the fan shouldn't wait on the email.
Your answer is saved locally in your browser.
Hint 1 — nudge

Two questions decide everything: how many receivers? and can the caller usefully continue before the answer arrives? Scenario 3 needs a different answer for each of its two halves.

Model answer & rubric
  • Seat hold → one-to-one, synchronous (request/response, e.g. REST or gRPC). A single, known receiver (Seat-Inventory) and the caller is blocked by the business, not just the code — the UI literally cannot proceed without the yes/no. Async here would add latency and complexity for nothing. Guard the call with a timeout and circuit breaker, since a hung Seat-Inventory must not hang Booking.
  • On-sale announcement → one-to-many, asynchronous (publish/subscribe via a broker, e.g. Kafka/RabbitMQ). The publisher can't and shouldn't know its receivers — thousands of follower notifications, plus tomorrow maybe Analytics and Pricing, all consume the same TicketsOnSale event. No response is needed; the broker absorbs the fan-out spike; new subscribers appear without touching the publisher (event-driven style, Chapter 10).
  • Checkout → split it. Booking→Payment: one-to-one, synchronous — the outcome gates the business decision (no ticket without a successful charge), so Booking must wait for the result. Then Booking→Notifications: one-to-one, asynchronous (notification/message queue) — exactly one receiver but no reply needed, and the email must not delay or fail the purchase; a queued message survives a Notifications outage and is retried. (Publishing a PurchaseCompleted event instead is also creditworthy — argue it lets receipt-email, analytics and fraud all subscribe.)
Marking rubric
  • ✔ Full credit: both dimensions chosen per scenario, justified by receiver-count and does-the-caller-need-the-answer; scenario 3 explicitly split into a sync leg and an async leg.
  • ◐ Partial: right styles, justification restates the scenario instead of the criteria.
  • ✘ Common mistake: making checkout's payment call async "for resilience" — Booking cannot decide whether to issue the ticket without the charge result; resilience there comes from timeouts/breakers, not fire-and-forget.
Decide Drill E — pick a saga coordination style, and make it safe

Scenario. A university's course-enrolment platform runs six microservices: Enrolment, Prerequisites, Seat-Inventory, Billing, Timetable and Notifications. Enrolling a student touches five of them, with two conditional branches (scholarship students skip Billing; lab courses need a second Seat-Inventory reservation). Registration week generates thousands of enrolments an hour over flaky campus wifi, and the registry office phones support constantly asking "why is this student's enrolment stuck?"

Recommend choreography or orchestration and justify it on mechanisms, not adjectives. Then name the two further patterns this system needs to survive registration week, and say precisely which failure each one prevents. Refresher: Chapter 11 · saga coordination.

Your answer is saved locally in your browser.
Hint 1 — nudge

Two details in the scenario are doing all the work: the number of steps (with branches), and who has to answer the phone at 4pm on Thursday. Which style makes "where is enrolment 7781 right now?" a query rather than an investigation?

Hint 2 — stronger

"Flaky campus wifi" means requests time out and get retried — what does a retried enrolment do to seat counts without protection? And when a compensation itself fails at 2am, where does that message go so nobody loses a student's enrolment silently?

Model answer & rubric

Recommend orchestration. Three mechanism-level reasons, all grounded in the scenario: (1) Five services with conditional branches. In choreography the workflow exists only as an emergent property of who subscribes to what — the scholarship and lab-course branches would be encoded as subscription conditions scattered across services, with no single artefact stating the enrolment rules. An orchestrator holds the workflow, branches included, in one readable place. (2) The support phone call. Orchestration persists saga state per enrolment, so "step 3 of 5, awaiting Seat-Inventory reply" is one row to query; choreography would mean correlating events across six services' logs and hoping a correlation id was attached. (3) Compensation ownership is explicit. When Billing fails after seats are held, the orchestrator's compensation stack records exactly which steps ran and therefore which undos are owed. Choreography can do the same job correctly — several services each running their own compensation in response to one failure event is normal — the broker just delivers that event once per subscriber — but nothing states who owns which undo, so as the workflow grows it becomes easy for a compensation to be claimed twice or by nobody. (Note the risk is unclear ownership, plus at-least-once redelivery of the failure event — not the mere fact that two services subscribe to it.)

The two supporting patterns. Idempotency keys — flaky wifi means at-least-once delivery, so a retried "enrol" request must not consume a second seat or bill the student twice; the orchestrator sends a key with every command and each participant claims it atomically before acting. Dead letter queues — when a step fails past its retry ladder, or a compensation itself fails, the message goes to a DLQ with an alert, so a stuck enrolment becomes a visible item for a human rather than silently vanishing during the busiest week of the year.

The honest cost. The orchestrator is a new service to build, deploy, scale and monitor, and it is both a single point of failure and a change bottleneck — every new enrolment rule edits it. Sagas also give up isolation: a partially-enrolled student is visible to other queries mid-saga, which is why the enrolment record needs an explicit PENDING status rather than pretending the intermediate state doesn't exist.

Marking rubric
  • ✔ Full credit: orchestration chosen and justified with at least three mechanisms tied to this scenario (step count/branches, queryable state, compensation ownership), both supporting patterns named with the specific failure each prevents, and one honest cost conceded.
  • ◐ Partial: right choice justified only by adjectives ("easier to manage", "more scalable"), or supporting patterns named without saying what they prevent.
  • ✘ Common mistake: proposing a distributed transaction / two-phase commit — it requires every participant to be online and responsive at commit time, which is exactly the coupling microservices exist to avoid. Also wrong: "choreography, because loose coupling is always better" — coupling is a real advantage of choreography, but here it is outweighed by six services, branches, and the operational need to answer "where is it stuck?".
Build Lab M01 — make the saga survive failure, retries and poison messages

Now build the thing you just argued for. Labs-Code/labs/M01-saga-idempotency-dlq/ gives you three participants, an audit log and a toy broker; sixteen failing tests are the specification.

cd Labs-Code/labs/M01-saga-idempotency-dlq/Starter dotnet test # 16 red — that's your to-do list

Steps 1–4 build the orchestrator and its compensation stack (including the detail that a declined card must refund nothing). Step 5 is the one that ties the pillars together: when a compensation itself throws, a transient failure must be retried and a permanent one must not, it must then be dead-lettered by name with the real attempt count, the remaining undos must still run, and the saga must report CompensationFailed rather than pretending it compensated cleanly. Step 6 adds the fourth state most implementations miss: a duplicate that arrives while the first attempt is still running must answer "in progress", not guess. Step 6 adds the idempotency store, and steps 7–9 add the retry ladder, poison-message handling and redrive.

What green proves

That your compensations unwind in reverse order and only for steps that actually happened; that a failed undo is escalated to a human rather than swallowed, that the compensations below it on the stack still run, and that the result says CompensationFailed — because "we tried to undo it" and "we undid it" are the same outcome only if you never look at the data afterwards; that a duplicate request replays the stored outcome instead of charging twice — including when that outcome was a failure, which is the case almost every implementation forgets; and that a malformed payload is dead-lettered on attempt 1 rather than burning the whole retry ladder to reach the same conclusion. The reference implementation is in Solution/ — attempt first.

Articulate Drill F — "a compensation is not a rollback"

Exam voice: "Explain why a saga uses compensating transactions rather than rolling back, and give one example of a compensation that cannot fully undo its original action." Three or four sentences.

Your answer is saved locally in your browser.
Hint 1 — nudge

Each local transaction has already committed in its own database. What is there left to roll back? And which of the enrolment steps above can never truly be taken back once it happens?

Model answer & rubric

Each step of a saga commits locally in its own service's database, so by the time a later step fails there is nothing left to roll back — the earlier transactions are durable and visible, and no global transaction manager exists to undo them. A compensation is therefore a new business transaction that offsets an old one, and both remain in the record: refunding a payment is not the same as never charging it, since the customer saw the charge and the statement shows two lines. Some compensations are only partial — cancelling a booking may keep a cancellation fee — and some are impossible: once Notifications has emailed "you are enrolled in CS3400", no compensation un-sends it; the best you can do is send a correction, which is itself a new action with its own consequences.

Marking rubric
  • ✔ Full credit: explains that local transactions have already committed and no global transaction manager exists; defines a compensation as a new offsetting transaction that leaves history intact; gives a genuinely irreversible or partial example.
  • ◐ Partial: says "you undo the previous steps" without distinguishing that from a rollback.
  • ✘ Common mistake: describing a compensation as "rolling back the other service's database" — you cannot reach into another service's database at all, which is the premise of the whole architecture.

6 · Capstone — StreamArchitect

The course's final capstone: StreamArchitect, a backend for an on-demand video streaming service. It must handle diverse media (movies, TV series with seasons/episodes, live events), subscription tiers with different privileges (resolution, screen count, downloads), region locking, and pluggable recommendation algorithms — and it must let new content types and plans arrive without rewriting core logic. You'll design it phase by phase in C#, committing each design before the reveal, then prove the whole thing in code. This is where everything above converges: composition, Strategy, DI, ISP, LSP.

Phase 1 — the content library

Decide Genres without an inheritance tree; ratings that defend themselves

Requirements: movies and episodes share metadata (title, description, maturity rating, release year); the WatchList must treat a Movie and an Episode identically; a title can belong to multiple genres — and you are strictly forbidden from class HorrorMovie : Movie. The maturity rating must be more than a string: it must itself prevent an under-13 user opening a TV-MA stream. Design the types: what is the abstraction, where do genres live, what shape is the rating?

Your answer is saved locally in your browser.
Hint 1 — nudge

Why is genre-by-inheritance impossible here, not just ugly? Try to make one class both Horror and Comedy with single inheritance. Then re-read Section 3 of this page.

Hint 2 — stronger

For the rating: a string can be read; an object can refuse. Which clean-code idea says validation logic belongs next to the data it guards?

Model answer & rubric
// Encapsulated value object — the rating enforces its own rule
public class MaturityRating
{
    public string Code { get; }
    private readonly int _minAge;
    public MaturityRating(string code, int minAge) { Code = code; _minAge = minAge; }
    public bool IsAppropriateFor(int userAge) => userAge >= _minAge;
}

public class Genre { public string Name { get; } public Genre(string name) => Name = name; }

// The abstraction the WatchList depends on (NFR: abstraction)
public interface IWatchable
{
    string Title { get; }
    MaturityRating Rating { get; }
    IReadOnlyList<Genre> Genres { get; }
    int GetDurationMinutes();
}

public class Movie : IWatchable      { /* has-a List<Genre>; single duration + stream URL */ }
public class Episode : IWatchable    { /* season/episode numbers; owned by a Season       */ }
public class Season                  { /* composition: has Episodes                        */ }
public class TvSeries                { /* composition: has Seasons                         */ }

The arguments: genres are composition — a Movie has a list of Genre tags, so "Horror and Comedy simultaneously" is just two list entries; single inheritance makes it structurally impossible (a class has one parent) — this is Section 3's lesson applied. IWatchable is the abstraction that lets the WatchList hold movies and episodes uniformly (program to an interface). Series→Season→Episode is composition (an episode does not outlive its series — Chapter 1 lifetime argument). MaturityRating is an encapsulated value object: the rule (min age) lives with the data (the code), so the check can't be forgotten or duplicated as scattered string comparisons.

Marking rubric
  • ✔ Full credit: interface for watchables, genres as composed tags with the "impossible with single inheritance" argument, rating as behaviour-carrying value object.
  • ◐ Partial: right structure but rating left as an enum/string with checks elsewhere.
  • ✘ Common mistake: IWatchable as a base class holding genre logic AND making TvSeries implement it — a series isn't directly watchable; its episodes are.

Phase 2 — membership & billing

Decide Tiers without if (planType == "Premium")

Basic (SD, 1 screen), Standard (HD, 2 screens), Premium (4K, 4 screens, downloads). Users upgrade at runtime with immediate effect. The BillingService must total a billing cycle without knowing any plan's details, and adding a Family Plan must not change it. The User class is forbidden from containing plan-name conditionals. Which pattern, which interface, and where does the "can I stream 4K?" question get answered?

Your answer is saved locally in your browser.
Hint 1 — nudge

The user shouldn't ask "am I Premium?" — the user should ask its plan "can I…?". Which behavioural pattern is "a family of interchangeable algorithms/policies behind one interface"?

Model answer & rubric
public interface ISubscriptionPlan          // the Strategy interface
{
    decimal MonthlyCost { get; }
    int MaxScreens { get; }
    bool CanStream4K { get; }
    bool CanDownload { get; }
}
public class BasicPlan : ISubscriptionPlan    { /* 8.99, 1, false, false */ }
public class StandardPlan : ISubscriptionPlan { /* 12.99, 2, false, false */ }
public class PremiumPlan : ISubscriptionPlan  { /* 19.99, 4, true, true  */ }

public class User                            // the Context
{
    public ISubscriptionPlan Plan { get; private set; }
    public void ChangePlan(ISubscriptionPlan newPlan) => Plan = newPlan;  // runtime swap
    public bool TryWatch4K() => Plan.CanStream4K;   // ask the plan — no type checks
}

Strategy (Chapter 5): each tier is an interchangeable policy object behind ISubscriptionPlan; the User delegates every privilege question to its current plan, so upgrading is one reference assignment and takes effect on the very next call — that's the required "immediate benefit change". BillingService iterates users summing user.Plan.MonthlyCost — it depends only on the abstraction, so Family Plan is a new class and nothing else (OCP). The payment token stays private inside User/billing — the ContentPlayer never sees it (encapsulation NFR).

Marking rubric
  • ✔ Full credit: names Strategy, interface carries the privileges, User delegates, runtime swap shown, OCP argument for BillingService.
  • ◐ Partial: interface right but privileges decided by switch (plan.Name) somewhere — the conditional just moved house.
  • ✘ Common mistake: subclassing User (PremiumUser : User) — that's the Section 3 explosion again; the user's plan varies, not the user's type.

Phase 3 — the playback engine

Decide Testable region locks; interfaces that don't overpromise

The VideoPlayer must enforce region locking and log analytics — but it must be testable with a fake validator ("always allowed") and must not construct its own dependencies. Separately: movies can be streamed and downloaded, but a live sports event can only be streamed — and the compiler, not a runtime check, should stop a LiveSportEvent reaching the DownloadManager. Name the two principles and sketch the types.

Your answer is saved locally in your browser.
Hint 1 — nudge

Principle 1: "don't call us, we'll construct you" is the wrong slogan — flip who supplies whom. Principle 2 is the I in SOLID: no client should be forced to depend on members it cannot use.

Model answer & rubric
public interface IRegionValidator { bool IsAllowed(string contentId, string region); }
public interface IAnalyticsLogger { void LogStart(string title); }

// ISP: two capabilities, separately promised
public interface IStreamable   { string GetStreamUrl(); }
public interface IDownloadable { double GetDownloadSizeMb(); }

public class StandardMovie  : IStreamable, IDownloadable { /* both */ }
public class LiveSportEvent : IStreamable                { /* stream only */ }

public class VideoPlayer
{
    private readonly IRegionValidator _validator;
    private readonly IAnalyticsLogger _logger;
    public VideoPlayer(IRegionValidator validator, IAnalyticsLogger logger)  // DI
    { _validator = validator; _logger = logger; }

    public void Play(IStreamable content, string region) { /* validate, log, play */ }
}

public class DownloadManager
{
    public void Queue(IDownloadable content) { /* compile-time safety: */ }
    // DownloadManager.Queue(liveSportEvent)  ← does not compile. ISP did its job.
}

Dependency Injection (the mechanism serving DIP): VideoPlayer receives IRegionValidator/IAnalyticsLogger through its constructor and never news them, so a test injects MockRegionValidator (always-allow) to simulate a traveller without any real geo service. Interface Segregation (ISP): splitting IStreamable from IDownloadable means LiveSportEvent never carries a lying GetDownloadSizeMb() that throws — and DownloadManager.Queue(IDownloadable …) makes the illegal call unrepresentable at compile time, which beats any runtime is-check.

Marking rubric
  • ✔ Full credit: constructor injection with the mock-test argument, ISP split with the compile-time-safety argument.
  • ◐ Partial: one fat IContent with CanDownload flag — works, but pushes the error to runtime; that's the flag-argument smell at interface scale.
  • ✘ Common mistake: "DI" implemented as a static ServiceLocator the player calls — dependencies are then hidden again, which is the exact disease DI cures.

Phase 4 — the recommendation engine

Decide Swappable algorithms the dashboard can trust

Admins switch between PopularityBased and HistoryBased recommendation algorithms at runtime; the dashboard calling GetSuggestions(user) must keep working identically whichever is active — no crashes, no surprise behaviour changes. Which principle is being invoked when the requirement says "swap without the UI behaving unexpectedly", and what does it demand of the two implementations beyond "they compile against the interface"?

Your answer is saved locally in your browser.
Hint 1 — nudge

Compiling against IRecommendationAlgorithm is the easy half. What if HistoryBased returned null for brand-new users while PopularityBased never does? Which SOLID letter forbids that?

Model answer & rubric

Structure: IRecommendationAlgorithm { IReadOnlyList<string> Generate(User u); }, two concrete algorithms, and a RecommendationService holding the current one (Strategy again — polymorphic Generate call, swap method for admins). The principle in "swap without unexpected behaviour" is Liskov Substitution (LSP): every implementation must honour the interface's behavioural contract, not just its signature — same preconditions (any valid user, including one with zero history), same guarantees (non-null list, possibly empty, no exceptions for valid input, no hidden side effects like mutating the user). If HistoryBased threw on a new user, it would compile fine and still violate LSP — the dashboard would "behave unexpectedly", which is precisely what the requirement forbids. LSP is what makes the Strategy swap safe rather than merely possible.

Marking rubric
  • ✔ Full credit: names LSP, states the contract in behavioural terms (preconditions/postconditions), gives a concrete violation example.
  • ◐ Partial: names LSP but explains it as "they implement the same interface".
  • ✘ Common mistake: answering "polymorphism" alone — polymorphism is the mechanism; LSP is the promise that makes the mechanism trustworthy.
Build Prove it in code — the four deliverables

No test harness for this one — the capstone is yours. Spin up a scratch console app and implement your Phase 1–4 designs in C#, then demonstrate the course's four proofs (this mirrors the original capstone's deliverables checklist):

mkdir StreamArchitect && cd StreamArchitect && dotnet new console && dotnet run
  1. The hybrid-movie test — create one Movie tagged Action + Sci-Fi + Thriller, print its genres: proves genres-by-composition.
  2. The plan swap — a Basic user tries 4K (fails), upgrades to Premium at runtime, retries (succeeds): proves Strategy.
  3. The mock region lock — construct VideoPlayer with an always-allow MockRegionValidator and play region-locked content: proves DI.
  4. The interface split — write the line downloadManager.Queue(liveSportEvent);, watch it fail to compile, then comment it out with a note: proves ISP (the compiler is your test runner here).
Reference checkpoints

Wire it in Program.cs top-level statements; each proof is 3–6 lines given the Phase-model classes. If a proof feels hard to write, the corresponding design decision is wrong — e.g. if proof 2 needs an if (user is PremiumUser) anywhere, Phase 2 regressed to type-checking. Want the same rhythm with a harness? Labs C03 (composition) and C02 (test-first) are the drilled versions of proofs 1 and 2's underlying skills.

Articulate The closing exam answer — why does StreamArchitect survive change?

Final articulation, exam voice: "Using two concrete examples from your design, explain how StreamArchitect allows new content types and new subscription models to be added without rewriting core logic." (This is the system's stated #1 non-functional requirement — argue it end to end.)

Your answer is saved locally in your browser.
Hint 1 — nudge

The strongest form of this answer is a negative space argument: list the classes that do NOT change. WatchList, BillingService, VideoPlayer, RecommendationService — why is each untouched?

Model answer & rubric

Example 1 — new content type (say, Documentary): one new class implementing IWatchable (and IStreamable/IDownloadable as appropriate). The WatchList, VideoPlayer and DownloadManager don't change — they depend only on the abstractions (program to an interface, DIP), and genres attach by composition so no tree is disturbed. Example 2 — new plan (Family Plan): one new ISubscriptionPlan implementation; User and BillingService are untouched because privileges and cost are answered by the plan object itself (Strategy + OCP), and LSP guarantees the newcomer behaves at every site the old plans did. The pattern behind both: every axis of change was identified up front and put behind an abstraction (encapsulate what varies) — so change arrives as addition, and the "core logic" the requirement protects is precisely the set of classes that only ever see interfaces.

Marking rubric
  • ✔ Full credit: two traced examples with the changed/unchanged file lists and principles named per example.
  • ◐ Partial: principles recited without tracing an actual change.
  • ✘ Common mistake: claiming nothing changes — the composition root (where concrete classes are constructed/registered) always changes by one line; saying so shows you actually understand DI.

Closing quiz

💡 Where to next

You've now critiqued, rebuilt, test-driven and articulated. Two closers: sweep the confusable-pattern pairs in Chapter 12, then sit the timed papers in Chapter 17 — Mock Exams, where the real past paper is waiting for you, unspoiled.