Design Patterns
Creational, structural, and behavioral patterns expressed in C# idioms — delegates over Strategy, events over Observer, a DI container over a hand-rolled Singleton.
13 questions
JuniorTheoryVery commonWhat is a design pattern, and what are the three GoF categories?
What is a design pattern, and what are the three GoF categories?
A design pattern is a named, reusable template for a recurring problem — you adapt it, not import it. The Gang of Four sorts them into creational (object creation, Factory), structural (composition), and behavioral (interaction, Strategy).
Common mistakes
- ✗Treating a pattern as ready-made code to import rather than a template you adapt to context
- ✗Misfiling patterns across the categories — putting
Factoryunder structural instead of creational - ✗Reaching for a pattern where plain code is simpler, adding indirection with no payoff
Follow-up questions
- →Name one behavioral pattern and say what responsibility it moves out of the caller.
- →Why is a pattern a template rather than reusable library code?
MiddleTheoryVery commonHow do the creational patterns Factory Method and Abstract Factory differ?
How do the creational patterns Factory Method and Abstract Factory differ?
Factory Method is a single overridable method that creates one product — the subclass picks the concrete type. Abstract Factory is an interface with several creation methods producing a family of related products, keeping that family consistent.
Common mistakes
- ✗Calling any
staticCreatehelper anAbstract Factoryregardless of what it returns - ✗Missing that
Abstract Factoryexists to keep a product family consistent, not just to hidenew - ✗Believing the choice between them depends on abstract class versus interface
Follow-up questions
- →What does the creational pattern
Abstract Factorycost when a new product joins the family? - →How does a dependency-injection container replace most hand-written factories?
JuniorTheoryCommonWhat does the Decorator pattern do, and how does it differ from inheritance?
What does the Decorator pattern do, and how does it differ from inheritance?
A decorator wraps an object that implements the same interface, forwards the calls to it, and adds behavior around them. Unlike inheritance it composes at run time — decorators stack in any order, and the wrapped object never knows it was wrapped.
Common mistakes
- ✗Confusing a decorator with an adapter — a decorator keeps the interface, an adapter changes it
- ✗Implementing the extra behavior by subclassing, which fixes it at compile time
- ✗Forgetting to delegate to the wrapped instance, so the original behavior is silently lost
Follow-up questions
- →Why must a decorator implement the same interface as the object it wraps?
- →What changes when you swap the order of two stacked decorators?
MiddleTheoryCommonHow do the structural patterns Adapter and Facade differ in intent?
How do the structural patterns Adapter and Facade differ in intent?
Adapter converts an existing interface into the one a client already expects, usually wrapping one incompatible type. Facade invents a new, simpler interface over a whole subsystem. Adapter is about compatibility; facade is about simplification.
Common mistakes
- ✗Calling any wrapper class an adapter regardless of whether the interface actually changes
- ✗Building a facade that merely forwards to one type — that is an adapter
- ✗Confusing both with a decorator, which keeps the interface and adds behavior
Follow-up questions
- →Where does the structural pattern
Adaptertypically live in a layered solution? - →When does a facade turn into a god object, and how do you split it?
MiddleTheoryCommonHow does a C# event natively implement the behavioral pattern Observer?
How does a C# event natively implement the behavioral pattern Observer?
An event is a multicast delegate whose += and -= are subscribe and unsubscribe: the publisher owns the invocation list and raises it, and subscribers are plain handlers. The language supplies both roles — no IObserver and no subscriber list.
Common mistakes
- ✗Forgetting
-=, so the publisher keeps its subscribers alive and leaks them - ✗Believing an
eventsupports only a single handler at a time - ✗Assuming handlers run asynchronously rather than inline on the raising thread
Follow-up questions
- →Why does a handler that was never removed keep the subscriber object alive?
- →When would you reach for
IObservable<T>instead of a plainevent?
MiddleTheoryCommonHow does the ASP.NET Core middleware pipeline resemble the structural pattern Decorator?
How does the ASP.NET Core middleware pipeline resemble the structural pattern Decorator?
Each middleware wraps the rest of the pipeline: it receives next, may run code before calling it, may short-circuit, and may run code after. That is a decorator chain over one RequestDelegate, so registration order is the wrapping order.
Common mistakes
- ✗Registering middleware in the wrong order and expecting the framework to sort it out
- ✗Forgetting to
await next(context), silently short-circuiting the rest of the pipeline - ✗Confusing the wrapping with an adapter — middleware keeps the
RequestDelegatesignature
Follow-up questions
- →What happens to the response when a middleware writes to it after awaiting
next? - →Why does the terminal middleware in the pipeline never call
next?
MiddleTheoryCommonWhat are the risks of a hand-rolled creational Singleton versus a container-registered singleton?
What are the risks of a hand-rolled creational Singleton versus a container-registered singleton?
A hand-rolled Singleton is global mutable state: hard to substitute in tests, tied to a static field, and its lazy initialisation needs Lazy<T> or a static constructor to be race-free. A container singleton you can swap, mock, and dispose.
Common mistakes
- ✗Believing a
staticInstanceis easy to substitute or reset between tests - ✗Hand-rolling double-checked locking and omitting
volatileorLazy<T> - ✗Hiding the dependency in a
staticcall instead of declaring it in the constructor
Follow-up questions
- →Why is a captive dependency a risk when a singleton takes a scoped service?
- →When is a
staticLazy<T>still the right answer over a container registration?
JuniorTheoryOccasionalHow does C#'s object-initializer syntax relate to the creational pattern Builder?
How does C#'s object-initializer syntax relate to the creational pattern Builder?
Both set state after construction, but an object initializer is only sugar over a parameterless new plus property assignments — it enforces nothing. Builder is a separate object that validates and returns a finished instance from Build().
Common mistakes
- ✗Believing an object initializer validates required properties or enforces invariants
- ✗Thinking initializer assignments run before the constructor rather than after it
- ✗Assuming
init-only setters remove the need for a builder that validates combinations
Follow-up questions
- →How do
requiredmembers change what an object initializer can guarantee? - →When does the creational pattern
Builderbeat a constructor with many optional parameters?
MiddleTheoryOccasionalHow does the in-process messaging library MediatR implement the behavioral pattern Mediator?
How does the in-process messaging library MediatR implement the behavioral pattern Mediator?
The sender sends a request object and never names a handler: IMediator resolves the IRequestHandler<TRequest, TResponse> from the container and invokes it. Sender and handler are coupled only by the request type, and IPipelineBehavior wraps it.
Common mistakes
- ✗Assuming
MediatRis a queue or message bus rather than an in-process dispatcher - ✗Confusing a request, which has exactly one handler, with a notification, which has many
- ✗Adding
MediatRfor indirection alone, hiding straightforward service calls behind it
Follow-up questions
- →How does an
IPipelineBehaviordiffer from an ASP.NET Core middleware component? - →When does routing everything through a mediator make the call graph harder to follow?
MiddleTheoryOccasionalHow do you implement the behavioral pattern Strategy with Func<T> instead of a class hierarchy?
How do you implement the behavioral pattern Strategy with Func<T> instead of a class hierarchy?
Replace the strategy interface with a delegate: the class takes a Func<Order, decimal> and invokes it, so each strategy is a lambda or method group. You lose a named type and per-strategy state, but drop an interface plus a class per algorithm.
Common mistakes
- ✗Thinking a delegate cannot replace a strategy interface because it has no named type
- ✗Keeping a one-method interface plus a class per algorithm where a
Func<T>would do - ✗Missing that the delegate form gives up per-strategy state and a discoverable named type
Follow-up questions
- →When does a strategy that needs its own state force you back to an interface?
- →How would you select the delegate at startup from configuration?
SeniorTheoryOccasionalWhat problem does Command Query Responsibility Segregation (CQRS) solve, and what does it cost?
What problem does Command Query Responsibility Segregation (CQRS) solve, and what does it cost?
CQRS splits the write model from the read model: entities with invariants for commands, denormalised projections for queries. The cost is two models to maintain and, with a separate read store, eventual consistency — a client may not see its write.
Common mistakes
- ✗Confusing
CQRSwith method-level command-query separation inside one service class - ✗Assuming
CQRSnecessarily requires event sourcing and cannot be applied without it - ✗Ignoring eventual consistency, then being surprised a client cannot read its own write
Follow-up questions
- →How would you hide read-your-own-write staleness from the user interface?
- →When is
CQRSoverkill for a straightforward create-read-update-delete service?
SeniorDesignOccasionalA payments service exposes POST /payments over an at-least-once delivery channel: the caller retries on timeouts and network failures, so the same logical request can arrive two or three times, sometimes concurrently on different instances behind the load balancer. Charging a customer twice is unacceptable. Design the endpoint so that repeating a request has the same effect as sending it once. Cover how the caller marks a retry as the same request; what you store to recognise a duplicate, and where; how you stop the duplicate check and the charge from racing across instances; what you return for a duplicate that is still in flight versus one that already completed; and how long a deduplication record must live. Name the failure modes your design still has.
A payments service exposes POST /payments over an at-least-once delivery channel: the caller retries on timeouts and network failures, so the same logical request can arrive two or three times, sometimes concurrently on different instances behind the load balancer. Charging a customer twice is unacceptable. Design the endpoint so that repeating a request has the same effect as sending it once. Cover how the caller marks a retry as the same request; what you store to recognise a duplicate, and where; how you stop the duplicate check and the charge from racing across instances; what you return for a duplicate that is still in flight versus one that already completed; and how long a deduplication record must live. Name the failure modes your design still has.
The caller sends an idempotency key. The server inserts it into a dedupe store under a unique constraint before charging, so a concurrent duplicate loses the insert. The completed response is replayed on retry, an in-flight one gets a conflict, and records outlive retries.
Common mistakes
- ✗Deduplicating on a request-body hash instead of a caller-supplied idempotency key
- ✗Checking then writing the key as two separate steps, leaving a race window between instances
- ✗Deleting the dedupe record as soon as the response is sent, so a late retry charges the customer again
Follow-up questions
- →What should the endpoint return when the same key arrives with a different request body?
- →How do you keep the dedupe write and the charge atomic when they live in two different stores?
SeniorTheoryRareWhen does the query-criteria pattern Specification earn its keep with Entity Framework Core?
When does the query-criteria pattern Specification earn its keep with Entity Framework Core?
When the same business filter is needed in several places. A specification is a named Expression<Func<T, bool>> you can unit-test and combine with And/Or, and Entity Framework Core translates it to SQL. It stops paying off once it hides SQL.
Common mistakes
- ✗Typing the specification as
Func<T, bool>, which forces client-side evaluation of the whole table - ✗Building a specification layer so deep that the generated SQL becomes invisible
- ✗Assuming expression trees cannot be composed, and duplicating the filter instead
Follow-up questions
- →Why does an
Expression<Func<T, bool>>translate to SQL while aFunc<T, bool>does not? - →How would you combine two specifications without losing the expression tree?