Lesson 1 of 12 | 1. Foundations and Pattern Selection
Pattern Selection Under Interview Pressure
Learning outcome
You will be able to: (1) translate short interview requirements into the core forces that matter, (2) pick a small set of candidate patterns and articulate explicit trade-offs, and (3) communicate a concise, production-aware design rationale that an interviewer (or teammate) can evaluate quickly.
Intuition
In interviews you rarely need to name the "perfect" pattern. Instead, you should: (a) identify the dominant forces (change frequency, runtime performance, testability, sequencing/ordering needs, error handling), (b) choose the simplest pattern that addresses those forces, and (c) explain what you would change as requirements evolve. Practically this is: list 3 forces, pick 1–2 patterns, explain trade-offs, and show a tiny example or API sketch.
A mental checklist that speeds decision-making:
- Is the variation about interchangeable algorithms? (favor Strategy)
- Is it about a dynamic sequence of independent handlers/filters? (favor Chain of Responsibility)
- Do you need to combine a pipeline with a pluggable executor? (compose CoR + Strategy)
- Is global shared state involved? (be wary of Singleton; prefer DI)
Example one-liner to use in an interview: "The core force is runtime algorithm swap versus pipeline validation: Strategy handles swap, Chain handles pipeline; I'd compose them so validation is orthogonal to execution. That keeps testing and observability straightforward."
Deep dive
When you state a choice, structure the explanation as: Force → Pattern → Trade-offs → How you'd evolve it in production. Use this template aloud:
- Forces: what changes most often? who owns data? is ordering important? are handlers independent? must be horizontally scalable?
- Pattern: name it and give 1-sentence intent.
- Trade-offs: list 2 positives and 2 negatives relevant to the forces.
- Production notes: telemetry, latency, error handling, DI, testing.
Failure modes
Know and call out common misuse signals in interviews — this shows production sense:
- Premature use of Singleton for convenience rather than DI: leads to hard-to-test global state.
- Applying Strategy when what you need is a pipeline of independent filters (you'll end up reimplementing sequencing logic outside the pattern).
- Excessive composition: composing many tiny patterns without reason increases cognitive load and deployment risk.
- Hidden side effects in handlers: chaining mutable state across handlers will make failures hard to debug.
When you detect these in an interview prompt, say them: e.g., "If the system needs ordering-dependent side effects, I'd avoid a blind CoR and prefer an explicit pipeline API with documented handler contracts."
Interview drill
Practice drills (timed): 5–8 minutes per prompt.
- Read a short requirement (30s): highlight 3 forces. Speak them out loud.
- Choose 1–2 patterns (60s): name the pattern(s) and one-sentence intent.
- Trade-offs (60s): give two pros and two cons tied to the forces.
- Sketch a call-flow (60–90s): show how components interact, who owns data, where to put metrics.
- Answer a follow-up (30s): How to evolve for multi-region or high throughput?
Micro-tests to practice aloud:
- Replace Strategy with a simple if/else switch — when is that acceptable? (Answer: when the number of variants is very small and changes infrequently.)
- If validators must run concurrently, would you still use CoR? (Answer: probably not; use independent parallel validators with an aggregator.)
Revision checklist
Before an interview, practice these quick checks:
- Can I name the dominant force in one sentence?
- Can I map that force to 1–2 patterns and explain why other patterns are worse?
- Can I list 2 production concerns (observability, error handling, testability) and how I'd address them?
- Can I sketch a small API or sequence diagram in 90 seconds?
Production code
Keep examples minimal but production-aware. The included runnable example demonstrates selecting Strategy vs Chain of Responsibility, shows why each pattern is preferable to a simpler alternative, warns against misuse, and shows a composed pipeline (validate then execute). The code prints deterministic output so you can reason about trade-offs and test expectations.
Important production notes (discuss in interviews):
- Prefer composition over inheritance to keep behaviors orthogonal.
- Inject strategies and handlers via DI so tests can replace them with fakes.
- Add metrics per handler and per strategy to monitor latency and failure rates.
- Document handler semantics (idempotency, side effects) to avoid surprises in chains.
Code walkthrough
The runnable example (below) contains three demonstrations:
- Strategy alone: show a pluggable payment gateway implementation.
- Chain of Responsibility alone: show validation pipeline and short-circuiting.
- Combined pipeline: validate via CoR then execute via Strategy.
The code prints why you would pick the pattern (preferable to a simple if/else or ad-hoc checks) and a misuse warning for each pattern. When you present this in an interview, walk through the forces you observed and then step through the small trace logs produced by the example to justify your choice.
Exercises
- Given a logging system that must be extensible to send logs to multiple backends and optionally filter sensitive fields, which patterns would you choose and why? (Expected: Adapter/Decorator for backends, Chain/Filter for transform pipeline.)
- Convert the included demo so validators run in parallel and report aggregated results. Explain trade-offs (reduced latency vs. complexity and harder per-validator tracing).
<!-- tin:verified-code:start -->
Executable code examples
Strategy vs Chain of Responsibility demo
Program.cs
using System;
using System.Collections.Generic;
// Minimal, deterministic demo to show pattern selection and trade-offs.
// Target: net10.0 / modern C# features (records, pattern matching allowed).
record PaymentRequest(decimal Amount, string AccountId, string Method);
// -------- Strategy: interchangeable executors --------
interface IPaymentStrategy
{
string Execute(PaymentRequest req);
}
class StripeStrategy : IPaymentStrategy
{
public string Execute(PaymentRequest req) => $"StripeStrategy: charged {req.Amount} to {req.AccountId}";
}
class PayPalStrategy : IPaymentStrategy
{
public string Execute(PaymentRequest req) => $"PayPalStrategy: charged {req.Amount} to {req.AccountId}";
}
class BankTransferStrategy : IPaymentStrategy
{
public string Execute(PaymentRequest req) => $"BankTransferStrategy: charged {req.Amount} to {req.AccountId}";
}
// -------- Chain of Responsibility: validators/handlers --------
interface IValidator
{
// Return (passed, message)
(bool Passed, string Message) Handle(PaymentRequest req);
}
class FraudHandler : IValidator
{
public (bool, string) Handle(PaymentRequest req)
{
if (req.Amount > 5000m)
return (false, $"FraudHandler: failed for {req.AccountId} amount {req.Amount}");
return (true, $"FraudHandler: passed for {req.AccountId}");
}
}
class BalanceHandler : IValidator
{
public (bool, string) Handle(PaymentRequest req)
{
// Deterministic rule for demo: any amount > 5000 fails balance check
if (req.Amount > 5000m)
return (false, $"BalanceHandler: insufficient funds for {req.AccountId} amount {req.Amount}");
return (true, $"BalanceHandler: passed for {req.AccountId}");
}
}
class ComplianceHandler : IValidator
{
public (bool, string) Handle(PaymentRequest req)
{
// For demo, compliance always passes
return (true, $"ComplianceHandler: passed for {req.AccountId}");
}
}
class ValidationPipeline
{
private readonly IEnumerable<IValidator> _validators;
public ValidationPipeline(IEnumerable<IValidator> validators) => _validators = validators;
public (bool Passed, string StoppedBy) Execute(PaymentRequest req)
{
foreach (var v in _validators)
{
var (passed, message) = v.Handle(req);
Console.WriteLine(message);
if (!passed)
return (false, message);
}
return (true, "All validators passed");
}
}
// -------- Demo runner --------
class Demo
{
public static void Run()
{
// 1) Strategy-only scenario
Console.WriteLine("=== Scenario: Strategy chosen for payment method pluggability ===");
var reqA = new PaymentRequest(100m, "acct-001", "Stripe");
IPaymentStrategy strategyA = new StripeStrategy(); // chosen based on Method
Console.WriteLine(strategyA.Execute(reqA));
Console.WriteLine("Why: Strategy is preferable to a long if/else switch when gateways change or grow.");
Console.WriteLine("Misuse: Strategy alone doesn't handle ordered validations or short-circuiting checks.\n");
// 2) Chain of Responsibility-only scenario
Console.WriteLine("=== Scenario: Chain of Responsibility for validations ===");
var validators = new List<IValidator>
{
new FraudHandler(),
new BalanceHandler(),
new ComplianceHandler()
};
var pipeline = new ValidationPipeline(validators);
var reqB = new PaymentRequest(10000m, "acct-002", "BankTransfer");
Console.WriteLine("Starting validation pipeline (deterministic rules):");
var (passedB, stoppedBy) = pipeline.Execute(reqB);
Console.WriteLine(passedB ? "Pipeline result: passed" : $"Pipeline result: stopped - {stoppedBy}");
Console.WriteLine("Why: CoR fits when you need a dynamic sequence of independent checks.");
Console.WriteLine("Misuse: order-sensitive side effects and hidden state between handlers make debugging hard.\n");
// 3) Combined: validate then execute (common production choice)
Console.WriteLine("=== Scenario: Combined (validate then execute) ===");
var reqC = new PaymentRequest(50m, "acct-003", "PayPal");
Console.WriteLine("Validation trace:");
var (passedC, _) = pipeline.Execute(reqC);
if (passedC)
{
IPaymentStrategy strategyC = new PayPalStrategy();
Console.WriteLine(strategyC.Execute(reqC));
Console.WriteLine("Combined rationale: validators are orthogonal to executors; composition improves testability and observability.");
}
else
{
Console.WriteLine("Validation failed; execution skipped.");
}
Console.WriteLine("\nProduction notes: inject validators and strategies via DI, emit metrics per step, and document handler contracts for idempotency.");
}
}
class Program
{
static void Main()
{
Demo.Run();
}
}
<!-- tin:verified-code:end -->