Concepts
What you will learn: When adding an export format or shipping rule makes a switch grow, decide whether a pattern can separate the choice from the work—or would add needless complexity. Each pattern starts with a practical problem, shows the shape of a solution, then explains its cost or limits.
Before you start: Examples use C#: an object is an instance of a class, a method is an operation, and an interface is a contract between application parts. You should be able to read a small class and method. You do not need to memorize names or use a pattern just because it has a name.
Select a term with a dotted underline for its meaning, then continue reading from the same place.
#Patterns and Singleton: start with the problem
A pattern names a recurring design solution with context and costs; it is neither a requirement nor a library. The 23 patterns cover creation, structure and behavior. Examples here are independent, assume conventional usings, and should not be combined where type names overlap.
Singleton addresses a shared instance with one access point. A traditional static instance hides the dependency and complicates substitution. Thread-safe lazy initialization does not make the object's methods thread-safe.
public sealed class OrderNumberFormatter
{
private static readonly Lazy<OrderNumberFormatter> _instance =
new(() => new OrderNumberFormatter()); // (1)
public static OrderNumberFormatter Instance => _instance.Value;
private OrderNumberFormatter() { }
public string Format(int number) => $"ORD-{number:000000}";
}
// Alternative in a .NET application's composition root:
builder.Services.AddSingleton(TimeProvider.System); // (2)
- (1) Lazy creates the stateless formatter on first access; it formats numbers and does not generate unique IDs.
- (2) owns and explicitly supplies a shared service. Its lifetime is per service provider, not per cluster, and does not globally prevent construction.
| Traditional approach | Often preferable in |
|---|---|
| Static singleton access | Replaceable DI singleton with explicit lifetime |
| Static clock in business rules | Injected TimeProvider for testing |
Avoid it for request/user state or a service needing a scoped . Do not capture scoped services inside singletons or make everything singleton merely to reduce .
Creational: constructing objects
#Factory Method and Abstract Factory
Factory Method: vary a product within a fixed workflow
An invoice-export workflow needs a different writer for each export type. A subclass inherits shared behavior from a base class; override means replacing a method’s implementation when the base permits it. Factory Method lets a specialized type choose the product without repeating the workflow. A switch returning an implementation is a useful simple factory, but not the classic structure.
public interface IInvoiceWriter { string Write(decimal total); }
public sealed class TextInvoiceWriter : IInvoiceWriter
{
public string Write(decimal total) => $"Total: {total}";
}
public abstract class InvoiceExport
{
protected abstract IInvoiceWriter CreateWriter(); // (1)
public string Export(decimal total) => CreateWriter().Write(total);
}
public sealed class TextInvoiceExport : InvoiceExport
{
protected override IInvoiceWriter CreateWriter() =>
new TextInvoiceWriter(); // (2)
}
- (1) The base owns the export workflow and product selection.
- (2) A subclass chooses the product without changing the workflow.
: factory such as IHttpClientFactory are common, but the name alone does not imply GoF Factory Method. Avoid the inheritance hierarchy when injecting a writer is sufficient.
Abstract Factory: a compatible product family
If a payment client must match its provider's verifier, a family factory centralizes compatible choices. This example models configuration only, not payment execution or signature verification.
public sealed record ChargeClient(string Provider);
public sealed record WebhookVerifier(string Provider);
public interface IPaymentKit
{
ChargeClient CreateClient();
WebhookVerifier CreateVerifier(); // (1)
}
public sealed class SandboxKit : IPaymentKit
{
public ChargeClient CreateClient() => new("Sandbox");
public WebhookVerifier CreateVerifier() => new("Sandbox"); // (2)
}
- (1) The contract creates a family rather than one product.
- (2) One implementation chooses both members; real contracts and behavior must preserve compatibility.
.NET: DbProviderFactory creates families such as connections and commands. Avoid it for a single product or unrelated products. Adding a new product kind changes the factory interface and its implementations.
| Pattern | Main variation |
|---|---|
| Factory Method | A subclass selects a product through an override |
| Abstract Factory | A factory object creates a compatible family; it may use factory methods internally |
| Simple factory | A central method selects a type, such as a switch or keyed |
#Builder and Prototype
Builder: steps instead of an unwieldy constructor
When report construction involves related options, a builder collects and validates them at Build. Fluent syntax alone is not the whole pattern; the point is managing construction separately from the product.
public sealed record ReportDefinition(DateOnly From, DateOnly To);
public sealed class ReportBuilder
{
private DateOnly? _from, _to;
public ReportBuilder Between(DateOnly from, DateOnly to)
{
_from = from; _to = to; return this; // (1)
}
public ReportDefinition Build()
{
if (_from is null || _to is null || _from > _to)
throw new InvalidOperationException("Invalid date range");
return new(_from.Value, _to.Value); // (2)
}
}
- (1) Collect settings and return this for chaining.
- (2) Return a new product rather than leaking mutable builder state.
: WebApplicationBuilder and connection-string builders offer staged construction . Avoid it for a simple object with two parameters; a constructor or initializer is clearer.
Prototype: start from an existing instance
Instead of rebuilding a similar order configuration, copy a template and adjust the differences. Define whether copying is shallow or deep: copying a reference does not clone its target.
public sealed record OrderTemplate(string Name, List<string> Tags);
// Usage fragment:
var original = new OrderTemplate("Standard", ["Gift"]);
var shallow = original with { Name = "Express" }; // (1)
var independent = original with { Tags = [.. original.Tags] }; // (2)
- (1) Record with is shallow; Tags remains shared.
- (2) A new list is enough because its strings are immutable; mutable child objects would need a deeper copying policy.
.NET: records and copy constructors implement copying; ICloneable does not specify shallow versus deep. Avoid cloning resource owners such as connections or blindly duplicating entity identity.
Structural: composing objects
#Adapter and Facade
Adapter: translate an incompatible interface
A legacy expects cents while the application uses decimal amounts. An adapter is like a plug converter: it exposes the expected contract and translates data. This example assumes a currency with one hundred minor units and previously validated input.
public sealed class LegacyPayment
{
public string ChargeCents(long cents) => $"receipt-{cents}";
}
public interface IPayment { string Charge(decimal amount); }
public sealed class PaymentAdapter(LegacyPayment legacy) : IPayment
{
public string Charge(decimal amount)
{
long cents = checked((long)Math.Round(amount * 100m)); // (1)
return legacy.ChargeCents(cents); // (2)
}
}
- (1) Translate units; production code must define its rounding policy.
- (2) The legacy stays outside callers.
: adapters around generated or clients can also form an . Avoid an empty wrapper when interfaces already fit.
Facade: simplify a subsystem
An invoice-viewing client needs a final document, not parsing and rendering details. A facade offers one clear operation over the subsystem without pretending to change each component's contract.
public sealed class InvoiceData
{
public decimal Parse(string input) =>
decimal.Parse(input, System.Globalization.CultureInfo.InvariantCulture);
}
public sealed class InvoiceRenderer
{
public string Render(decimal total) => $"Invoice total: {total}";
}
public sealed class InvoiceFacade(InvoiceData data, InvoiceRenderer renderer)
{
public string Create(string input) =>
renderer.Render(data.Parse(input)); // (1)
}
- (1) Clients call Create; parsing can fail, so the input boundary must handle invalid data.
.NET: an application service over parsing and rendering libraries is a natural example. Avoid a God facade knowing every subsystem and rule; split by use case. A facade does not imply transactional rollback.
#Decorator, Proxy and their difference from Adapter
A Decorator adds responsibility around an object using the same contract, avoiding a subclass for every combination. A Proxy controls access through lazy creation, remote access or protection. Their structures may match; intent distinguishes them, and a wrapper can serve multiple intentions.
public interface IPriceReader { decimal Read(int productId); }
public sealed class FixedPrices : IPriceReader
{
public decimal Read(int productId) => 100m;
}
public sealed class LoggedPrices(IPriceReader inner, Action<int> log)
: IPriceReader
{
public decimal Read(int id)
{
log(id); return inner.Read(id); // (1) Decorator
}
}
public sealed class LazyPrices(Func<IPriceReader> factory) : IPriceReader
{
private readonly Lazy<IPriceReader> _inner = new(factory); // (2) Proxy
public decimal Read(int id) => _inner.Value.Read(id);
}
- (1) Logging adds responsibility around reading; decorator ordering can change behavior.
- (2) The proxy postpones reader creation, without making its implementation thread-safe.
: GZipStream decorates Stream with compression. lazy-loading proxies illustrate Proxy with an risk; generated remote clients can disguise network costs behind local-looking calls.
Avoid decorators when simple centralized tracing suffices and composition adds no value. Avoid proxies that hide unexpected or unclear lifetimes. Define inner-object ownership and disposal, especially for streams and scopes.
| Pattern | Interface | Intent |
|---|---|---|
| Adapter | Changes | Translate to an expected interface |
| Decorator | Preserves | Add composable behavior |
| Proxy | Usually preserves | Control access |
#Composite, Bridge and Flyweight
Composite: leaves and groups share a contract
A bundle total must handle individual items and nested bundles. Composite lets the client use a tree without branching on each node type.
public interface ICartNode { decimal Total { get; } }
public sealed record CartItem(decimal Price) : ICartNode
{
public decimal Total => Price; // (1) leaf
}
public sealed class Bundle(IReadOnlyList<ICartNode> children) : ICartNode
{
public decimal Total => children.Sum(c => c.Total); // (2) composite
}
- (1) A leaf calculates itself.
- (2) A composite to children, including other composites.
: expression trees contain leaf and compound nodes, though not every operation fits every node. Avoid it for a flat list or an interface forcing fake AddChild operations on leaves. Prevent cycles in mutable input structures.
Bridge: two independent variation axes
Report types and output formats can create a subclass explosion. The abstraction is the report type; the implementation is how it is rendered. Bridge separates these two variation axes so each can evolve independently.
public interface IReportOutput { string Write(string content); }
public sealed class TextOutput : IReportOutput
{
public string Write(string content) => content;
}
public abstract class Report(IReportOutput output)
{
protected string Emit(string text) => output.Write(text); // (1)
public abstract string Render();
}
public sealed class OrderSummary(IReportOutput output) : Report(output)
{
public override string Render() => Emit("Order summary"); // (2)
}
- (1) The report abstraction delegates output mechanics.
- (2) A new report needs no subclass per output; a new output implements the contract.
.NET: report services with format adapters are a natural application; Stream implementations alone do not establish Bridge without two axes. Avoid it when only one variation axis needs a simple strategy.
Flyweight: share repeated intrinsic state
A million product tiles repeat the same large style. Share style as intrinsic state and keep product ID and price outside it as extrinsic state. Measure memory first.
public sealed record TileStyle(string Font, string Color);
public sealed class StylePool
{
private readonly System.Collections.Concurrent.ConcurrentDictionary
<string, TileStyle> _styles = new();
public TileStyle Get(string color) =>
_styles.GetOrAdd(color, c => new("Inter", c)); // (1)
}
public sealed record ProductTile(int ProductId, decimal Price, TileStyle Style); // (2)
- (1) Shared style is immutable; GetOrAdd can execute the factory more than once under contention, so it has no side effects.
- (2) Product-specific state stays outside the flyweight.
.NET: string interning shares equal values; it is not a recommendation to intern arbitrary input. Avoid mostly unique data or an unbounded user-driven pool where lookup and retention cost more than they save.
Behavioral: behavior and collaboration
#Strategy and State
- A store supports pickup, standard shipping and express shipping. The same pricing question has a different algorithm for each customer choice.
- Strategy puts each calculation behind a shared contract and selects the appropriate one. Adding a method need not duplicate order creation.
- State addresses behavior changing with an object's state, such as forbidding edits after dispatch. Distinguish a selectable algorithm from lifecycle-dependent behavior.
Strategy: select an algorithm
Shipping methods calculate the same kind of result differently. Strategy calculation to an interchangeable algorithm selected by the client or configuration.
public interface IShippingFee { decimal Calculate(decimal weight); }
public sealed class FlatFee(decimal fee) : IShippingFee
{
public decimal Calculate(decimal weight) => fee;
}
public sealed class WeightFee(decimal perUnit) : IShippingFee
{
public decimal Calculate(decimal weight) => weight * perUnit;
}
public sealed class ShippingQuote(IShippingFee strategy)
{
public decimal Get(decimal weight) => strategy.Calculate(weight); // (1)
}
- (1) The quote knows only the contract; validate weights and rates at the boundary.
: IComparer<T> supplies comparison strategies. Avoid classes for one simple algorithm or when a small delegate suffices.
State: behavior changes with state
An order's Pay and Cancel operations behave differently by state; repeated status checks scatter transition rules. State groups each state's behavior and makes transitions explicit.
public interface IOrderState { void Pay(Order order); }
public sealed class Pending : IOrderState
{
public void Pay(Order order) => order.ChangeState(new Paid()); // (1)
}
public sealed class Paid : IOrderState
{
public void Pay(Order order) { } // (2)
}
public sealed class Order
{
private IOrderState _state = new Pending();
internal void ChangeState(IOrderState state) => _state = state;
public void Pay() => _state.Pay(this); // (3)
}
- (1) Pending chooses the transition.
- (2) Repeated Pay is an state transition only; no external charge occurs here.
- (3) The context delegates to its state; persistence and concurrency are separate concerns.
.NET: complex order lifecycles can use State objects or a state-machine library. Avoid the hierarchy for a small stable set where a validated enum/switch is clearer.
| Strategy | State |
|---|---|
| Algorithm usually selected externally | Transitions follow the context's state |
| Strategies usually independent | States may know transition targets |
#Observer and Mediator
Observer: a publisher and subscribers
Adding a reaction to order confirmation should not require changing the publisher. Observer creates a one-to-many notification relationship. An ordinary C# event is synchronous and in-process, not a or durable delivery mechanism.
public sealed class OrderEvents
{
public event Action<int>? Confirmed;
public void RaiseConfirmed(int id) => Confirmed?.Invoke(id); // (1)
}
// Subscription fragment:
Action<int> record = id => Console.WriteLine($"Confirmed {id}");
publisher.Confirmed += record;
// When the subscriber's lifetime ends:
publisher.Confirmed -= record; // (2)
- (1) Current subscribers are invoked; a throwing handler can prevent later handlers from running.
- (2) Unsubscribe from longer-lived publishers to avoid retaining subscribers.
: events and IObservable<T>. Avoid it when one explicit result or transactional ordering is needed; an event is not an .
Mediator: centralize collaboration
If cart, stock and notification components all know each other, a Mediator is an object that receives requests and coordinates their collaboration, so each component need not hold references to the others. This does not mean every method call needs a dispatcher.
public interface IStock { bool Reserve(int productId); }
public interface IOrderNotice { void Reserved(int productId); }
public sealed class CheckoutMediator(IStock stock, IOrderNotice notices)
{
public bool RequestReservation(int id)
{
if (!stock.Reserve(id)) return false; // (1)
notices.Reserved(id); return true; // (2)
}
}
- (1) The caller talks to the mediator; collaborators need not reference each other.
- (2) Coordination is centralized, without automatic rollback if notification fails.
.NET: MediatR request dispatch and pipeline behaviors are a common mediation approach, not a requirement. Review applicable commercial licensing. Avoid indirection without benefit or a God mediator; direct handler injection is a simple alternative.
| Observer | Mediator |
|---|---|
| Publishes a fact to multiple subscribers | Coordinates collaborators or routes a request |
| Publisher need not own the entire workflow | Mediator knows coordination rules |
#Command and Chain of Responsibility
Command: make a request an object
To decouple submission from execution or support queuing, encapsulate request intent and data in a command. Retry and undo require additional design; a record does not provide them automatically.
public sealed record ReserveProduct(Guid RequestId, int ProductId, int Quantity); // (1)
public interface IReservationStore
{
Task ReserveOnceAsync(ReserveProduct command, CancellationToken ct);
}
public sealed class ReserveProductHandler(IReservationStore store)
{
public Task Handle(ReserveProduct command, CancellationToken ct) =>
store.ReserveOnceAsync(command, ct); // (2)
}
- (1) A data-only command can be recorded or transmitted; and DbContexts do not belong in the payload.
- (2) The handler executes it; the store contract requires atomic RequestId-based , not an implementation supplied here.
: handlers separate commands from executors; ICommand is another form. Avoid ceremony around a simple call with no queuing, history or timing requirement. Undo needs an inverse or compensation and may be impossible.
Chain of Responsibility: handle or forward
A discount approval escalates by limit. Each handler knows its own capability and successor; the caller need not know the whole chain.
public sealed class Approver(string name, decimal limit, Approver? next = null)
{
public string Approve(decimal discount)
{
if (discount <= limit) return name; // (1)
return next?.Approve(discount)
?? throw new InvalidOperationException("No approver"); // (2)
}
}
// Usage fragment; discount has already been validated as nonnegative.
var chain = new Approver("Cashier", 5, new Approver("Manager", 15));
- (1) The first capable handler accepts the request.
- (2) An unhandled request fails explicitly; prevent chain cycles.
.NET: and DelegatingHandler are related chain structures with before/after work and short-circuiting; pipelines need not stop after one handler. Avoid hidden ordering or accidental early termination when all validation errors must be collected.
#Template Method, Iterator and Visitor
Template Method: a fixed algorithm skeleton
Invoice imports share a sequence: parse, validate, save. Keep the skeleton in a base class and expose hooks for variable steps.
public abstract class InvoiceImport
{
public void Run(string input)
{
decimal total = Parse(input); // (1)
ArgumentOutOfRangeException.ThrowIfNegative(total);
Save(total);
}
protected abstract decimal Parse(string input);
protected abstract void Save(decimal total); // (2)
}
- (1) Run is nonvirtual to preserve sequencing.
- (2) Subclasses supply steps while the base enforces validation.
: BackgroundService provides lifecycle behavior around the variable ExecuteAsync step. Avoid deep inheritance when independently replaceable steps fit Strategy composition better.
Iterator: traverse without exposing storage
A consumer needs invoice items without knowing whether storage is an array or tree. Iterator separates traversal from representation; C# usually supplies the machinery.
static IEnumerable<int> ProductIds()
{
yield return 10; // (1)
yield return 20;
}
// Usage: foreach (int id in ProductIds()) Console.WriteLine(id); // (2)
- (1) The compiler creates a state machine; values are produced during MoveNext, not merely when the method is called.
- (2) foreach uses and IEnumerator with appropriate disposal.
.NET: IEnumerable<T> and IAsyncEnumerable<T>. Avoid relying on lazy iteration when repeated random access or a fixed snapshot is required. Re-enumeration can repeat computation or .
Visitor: new operations over stable node types
A stable needs printing and validation without every operation in every node. Visitor moves operations into separate objects; Accept enables double dispatch.
public interface IRuleVisitor
{
string Visit(MinimumTotal rule);
string Visit(AllRules rule);
}
public interface IRule { string Accept(IRuleVisitor visitor); }
public sealed record MinimumTotal(decimal Amount) : IRule
{
public string Accept(IRuleVisitor visitor) => visitor.Visit(this); // (1)
}
public sealed record AllRules(IRule Left, IRule Right) : IRule
{
public string Accept(IRuleVisitor visitor) => visitor.Visit(this);
}
public sealed class RulePrinter : IRuleVisitor
{
public string Visit(MinimumTotal rule) => $"Total >= {rule.Amount}";
public string Visit(AllRules rule) =>
$"({rule.Left.Accept(this)} AND {rule.Right.Accept(this)})"; // (2)
}
- (1) The concrete node selects the appropriate overload inside Accept.
- (2) Printing visits children; a new visitor adds another operation.
.NET: ExpressionVisitor visits or rewrites expression trees. Avoid it when node types change frequently, since every visitor then changes. C# type-pattern switches are smaller for simple cases, without guaranteeing exhaustiveness for arbitrary class hierarchies.
#Memento and InterpreterBonus
Optional but valuable: not asked in every interview, but knowing it deepens your understanding and sets you apart.
Memento: restore state without exposing its representation
An order-draft editor needs undo before saving. Memento lets the object capture and restore its own state while a caretaker keeps the snapshot without modifying its contents.
public sealed class OrderDraft
{
private sealed record Snapshot(Guid Owner, string Note);
private readonly Guid _owner = Guid.NewGuid();
public string Note { get; set; } = "";
public object Capture() => new Snapshot(_owner, Note); // (1)
public void Restore(object token)
{
if (token is not Snapshot s || s.Owner != _owner)
throw new ArgumentException("Wrong snapshot", nameof(token));
Note = s.Note; // (2)
}
}
- (1) The private snapshot is exposed only as an opaque .
- (2) Restore checks that the snapshot belongs to this draft.
: a C# editor's undo stack is a natural application. Event-sourcing snapshots primarily accelerate replay and are not automatically Mementos. Avoid it for undoing external payments or huge states; consider compensation or diffs and bound retention.
Interpreter: a small grammar with evaluation rules
A discount language with TotalAtLeast and AND can represent sentences as a tree whose nodes evaluate themselves. This is evaluation after parsing; the parser is not shown.
public interface IDiscountExpression { bool Evaluate(decimal total); }
public sealed record TotalAtLeast(decimal Minimum) : IDiscountExpression
{
public bool Evaluate(decimal total) => total >= Minimum; // (1)
}
public sealed record Both(IDiscountExpression Left, IDiscountExpression Right)
: IDiscountExpression
{
public bool Evaluate(decimal total) =>
Left.Evaluate(total) && Right.Evaluate(total); // (2)
}
- (1) A terminal expression has a defined meaning.
- (2) A nonterminal combines smaller expressions.
.NET: rule evaluators and expression engines are related applications; Regex evaluates a language without necessarily using this class-tree implementation. Avoid large grammars or unbounded untrusted input; choose a parser and enforce depth/size limits rather than executing arbitrary user code.
Additional patterns and anti-patterns
#Repository, Unit of Work, Specification, Result, Options, Null Object and Pipeline
Repository and Unit of Work
Repository provides collection-like access; Unit of Work coordinates changes in one commit. supplies tracking and a save boundary, while DbSet offers repository-like access. A generic wrapper may add little; aggregate-specific contracts can protect domain boundaries.
public interface IOrderRepository
{
Task<Order?> GetAsync(Guid id, CancellationToken ct);
void Add(Order order);
}
// Handler fragment: repository and db share the same scoped DbContext.
orders.Add(order); // (1)
await db.SaveChangesAsync(ct); // (2)
- (1) Add records a change without an independent internal commit.
- (2) One save commits tracked changes; multiple saves need an explicit if they must be atomic together.
Avoid repositories that merely duplicate EF or expose while promising full provider independence. A UoW is not a distributed transaction; outbound is outside SaveChanges.
Specification and Result
Specification names a reusable business predicate; use an expression when a provider must translate it. Result represents success or an expected failure as a value instead of throwing for every rejected coupon.
public sealed record OrderRow(DateTime DueAt, bool IsPaid);
public sealed class OverdueOrders(DateTime now)
{
public System.Linq.Expressions.Expression<Func<OrderRow, bool>> Criteria =>
o => !o.IsPaid && o.DueAt < now; // (1)
}
public sealed class CouponResult
{
public bool IsSuccess { get; }
public string? Error { get; }
private CouponResult(bool success, string? error)
=> (IsSuccess, Error) = (success, error);
public static CouponResult Applied() => new(true, null);
public static CouponResult Expired() => new(false, "coupon.expired"); // (2)
}
- (1) Evaluation time is an explicit input; EF can translate the predicate when mappings support it. Do not Compile before Where on IQueryable.
- (2) A private constructor prevents inconsistent success/error combinations. The boundary maps failures to suitable HTTP responses, not always 500.
Avoid a specification for a one-off trivial filter or a Result convention whose failures callers ignore. Do not turn every unexpected infrastructure exception into coupon.expired.
Options, Null Object and Pipeline
Options groups related configuration in a validated type. Null Object supplies intentional no-op behavior instead of null checks. A Pipeline composes stages with explicit contracts and ordering, often overlapping Chain and Decorator.
public sealed class InvoiceOptions
{
public int MaxLines { get; set; } = 100;
}
// ASP.NET Core composition-root fragment:
builder.Services.AddOptions<InvoiceOptions>()
.BindConfiguration("Invoices")
.Validate(o => o.MaxLines > 0, "MaxLines must be positive")
.ValidateOnStart(); // (1)
// Null Object from Microsoft.Extensions.Logging.Abstractions:
var log = Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; // (2)
// Small pipeline: validation before rendering.
Func<decimal, string> render = total => $"Total: {total}";
Func<decimal, string> pipeline = total =>
{
ArgumentOutOfRangeException.ThrowIfNegative(total); // (3)
return render(total);
};
- (1) Invalid configuration fails startup. Choose IOptions, IOptionsSnapshot or IOptionsMonitor according to lifetime and reload requirements.
- (2) Logging deliberately does nothing; do not use this to hide a missing essential payment service.
- (3) Ordering is contractual; this stage stops processing invalid input.
Avoid Options for an internal constant, Null Objects hiding missing dependencies, or dynamic pipelines for two fixed steps. Options documentation.
| Anti-pattern | Symptom and response |
|---|---|
| Pattern obsession | More classes without a change problem; return to a simple explicit flow |
| Service locator | IServiceProvider inside business logic hides dependencies; use constructor injection |
| God object | A manager owns calculation, storage and every workflow; separate reasons to change |
| Leaky abstraction | A repository promises EF but accidentally exposes its details; acknowledge or narrow the contract |
| Mutable singleton state | User data crosses requests; select an appropriate lifetime |
| Shotgun surgery | One new variant changes many switches; identify the shared variation point |
Interview questions
No results
Check your reasoning
Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?
15.1What is a design pattern, and how do you decide whether to use one?Junior
I treat a pattern as a named design trade-off, not a requirement to add classes. The groups are creational, structural and behavioral: construction, composition and collaboration. I first describe the repeated change or coupling problem, then choose the smallest design that addresses it. A pattern's name helps the team communicate, but I still explain ownership, failure behavior and when the design should be simplified. More patterns do not automatically make a system more maintainable.
15.2How does a DI singleton differ from a classic Singleton?Junior
A classic Singleton controls construction and exposes a global access point, commonly through a private constructor and Lazy instance. A singleton is a container lifetime: consumers explicitly request the service, and one provider supplies a shared instance. It is easier to substitute in tests but does not prohibit other construction or create one instance across servers. I still require thread-safe behavior and avoid capturing scoped dependencies. Thread-safe initialization alone does not protect mutable service state.
15.3When is Builder preferable to a constructor?Junior
I use a builder when construction has several optional or dependent steps and the result benefits from validation at completion. It can make report definitions or host configuration readable without an unwieldy constructor. I make Build return a valid independent product so later builder changes cannot mutate earlier results. For a simple object with a few required values, a constructor or initializer is usually clearer. Fluent chaining is helpful syntax, but not a reason by itself to introduce a builder.
15.4What problem does Adapter solve?Junior
Adapter lets an incompatible interface satisfy the contract a client expects. For example, an adapter can translate a decimal amount into a legacy payment 's minor units and map its response into the application's result type. I keep external types and semantics at that boundary. It can also support an anti-corruption layer when concepts differ. I avoid empty adapters that add no translation or , and I do not confuse adaptation with a facade merely simplifying a subsystem.
15.5How does Strategy reduce repeated branching?Junior
Strategy separates interchangeable algorithms behind one contract, so a shipping quote can use a flat fee or weight-based fee without every rule in a growing switch. The client or configuration chooses the strategy. I use it when variants change independently or recur across workflows. I keep the contract's inputs and guarantees consistent across implementations. If the variation is a tiny expression or the set is small and stable, a or straightforward switch can be easier to maintain.
15.6How do Factory Method, Abstract Factory and a simple factory differ?Mid
In the classic Factory Method, a base workflow calls an overridable creation method and a subclass chooses the concrete product. Abstract Factory is an object that creates a compatible family, such as database connections and commands. A simple factory centralizes selection, often with a switch, without requiring that inheritance structure. I distinguish them because many called factories are not GoF Factory Method. I prefer direct injection when product creation does not need its own variation mechanism.
15.7What is the main danger of Prototype copying in C#?Mid
I check copy semantics first. A record with expression creates a shallow copy, so a referenced mutable list is still shared unless I copy it separately. Copying that list is not automatically a deep copy of mutable elements either. I define which parts should be shared, which should be independent, and whether identity must be regenerated. values make this easier. I avoid cloning resource owners such as database connections, and I do not assume ICloneable specifies deep-copy behavior.
15.8How do Decorator, Proxy and Adapter differ?Mid
Adapter changes the exposed interface to match a client's expectation. Decorator preserves the contract and adds composable responsibilities, such as logging or compression. Proxy generally preserves the contract while controlling access, for example by delaying creation or representing a remote object. Their code can look similar, so I explain intent rather than identifying them from a wrapper alone. I also define lifetime, disposal and failure behavior, because stacking wrappers can change ordering and hide network or storage costs.
15.9How does Facade differ from a God object?Mid
A facade simplifies a cohesive subsystem for a particular client need, while a God object accumulates unrelated policies and workflows. I keep the facade focused on orchestration and leave domain with their proper owners. If every feature depends on it and every change edits it, that is a warning sign. I also make its failure contract explicit: wrapping several calls in one method does not make them atomic or provide rollback across external systems.
15.10How do Strategy and State differ?Mid
Both behavior, but they model different reasons for change. Strategy selects an interchangeable algorithm, usually chosen by a client or configuration, such as a shipping-fee calculation. State changes a context's behavior as its lifecycle changes, and states may choose transitions to other states. I use State when transition rules are otherwise repeated across operations. For a small stable lifecycle, an enum with explicit transition validation can be simpler. Neither pattern automatically handles persistence or concurrent transitions.
15.11How do Observer and Mediator differ?Mid
Observer publishes notifications to interested subscribers without the publisher coordinating their complete workflow. Mediator centralizes collaboration or request routing so components do not directly depend on each other. A C# event is a familiar Observer mechanism, but it is normally synchronous and not durable. MediatR is one mediation implementation, not a requirement. I choose explicit calls when there is one clear collaborator, and use notifications carefully when ordering, failures and subscriber lifetimes need well-defined behavior.
15.12What do Command and Chain of Responsibility each contribute?Mid
Command packages an operation's intent and inputs separately from the sender, making queuing or recording possible. Chain of Responsibility routes a request through handlers that can handle it, forward it or stop processing. They can work together, but neither guarantees safe retries, undo or atomicity. I define ordering and unhandled-request behavior for the chain, and an policy for replayable commands. For trivial direct calls, those abstractions may add more than value.
15.13When would you use Template Method rather than Strategy?Mid
I use Template Method when several implementations share an algorithm's fixed sequence and only specific steps should vary. The base class can enforce validation order while subclasses implement parsing or persistence hooks. Strategy uses composition and is easier when whole algorithms or independent steps must be selected dynamically. I avoid deep template hierarchies where hooks interact unpredictably. A simple function or an explicit pipeline can be clearer if there is little stable behavior to inherit.
15.14When are Composite, Bridge and Flyweight worth their complexity?Senior
I use Composite for a genuine tree where leaves and groups share meaningful operations. Bridge pays off when two independent axes, such as report type and output format, would otherwise create a subclass for every combination. Flyweight helps when many objects repeat substantial state and measurements show a memory benefit. I avoid fake leaf operations, speculative variation axes and unbounded pools. Each pattern solves a specific structural problem; none is a general way to make ordinary object models better.
15.15Why does Visitor favor stable node types but changing operations?Senior
Visitor moves operations into separate visitor objects, with Accept selecting the correct node-specific operation through double dispatch. Adding printing or validation then means another visitor, while the node model stays stable. Adding a node type is expensive because the visitor contract and existing visitors must change. That trade-off suits expression trees better than a rapidly evolving domain hierarchy. I consider C# pattern matching for smaller cases, while remembering that arbitrary class hierarchies are not automatically checked exhaustively.
15.16What are the limits of Iterator, Memento and Interpreter?Senior
I check the hidden cost each introduces. Iterator hides representation, but lazy traversal can repeat on re-enumeration and may hold resources open. Memento supports local state restoration, not reversal of an external payment, and retained snapshots need memory limits. Interpreter makes a small language explicit, but a large grammar or unbounded input needs stronger parsing and resource controls. I do not treat their names as guarantees of streaming efficiency, transactional undo or safe execution of arbitrary user rules.
15.17Would you add Repository and Unit of Work over EF Core?Senior
Not automatically. already coordinates tracked changes and saving, and DbSet provides repository-like access. A generic wrapper can hide projections and useful behavior without creating a meaningful boundary. I add an -specific repository when the domain needs a focused persistence contract or loading policy. I keep its methods aligned with the aggregate and share one unit of work. I also avoid claiming provider independence while exposing or provider-specific assumptions through the abstraction.
15.18How do you keep enterprise patterns from becoming ceremony?Senior
I ask what concrete contract each pattern improves. A specification should name a reused predicate, a Result should make expected failure handling explicit, Options should group validated configuration, and a Null Object should represent an intentional safe no-op. A pipeline needs meaningful ordering and composition. I remove abstractions that only forward calls, hide missing dependencies or let callers ignore errors. I also keep infrastructure failures observable instead of converting every exception into an ordinary business rejection.
Test yourself
Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.
Which statement best describes classic GoF Factory Method?
Which pattern creates a compatible family such as connections and commands?
A record with expression copies an object containing a mutable List. What happens by default?
Which pattern adds compression around an existing Stream while preserving the Stream contract?
A report type hierarchy delegates output format to an independent implementation hierarchy. Which pattern?
Which state belongs outside a shared Flyweight style?
The context's lifecycle chooses transitions among behavior objects. Which pattern is most direct?
Which limitation applies to an ordinary C# event?
Visitor is usually easiest when which part is stable?
What does an in-memory Memento normally provide?
Why use a Result for an expired coupon?
What should happen if pattern usage increases files without solving a change or coupling problem?