Design patterns
What design patterns are, where they are already built into .NET, how similar patterns differ from each other, and which anti-patterns show up most often in real code. Interviewers don’t check whether you know the catalog by heart — they check whether you can name the problem a pattern solves and what it costs.
Before this topic: SOLID and principles.
What are design patterns and how are they classified?
Section titled “What are design patterns and how are they classified?”What it is
A design pattern describes a solution to a typical problem: what the problem is, which classes take part, how they relate, and what follows from that. It is not a library and not code to copy: you implement a pattern for your own problem every time.
Why
- Shared vocabulary: “wrap it in a decorator” is shorter than explaining the class diagram.
- Proven solutions: you don’t have to reinvent how to add behavior without changing a class.
How they are classified
The book “Design Patterns” (1994) by Gamma, Helm, Johnson and Vlissides — the “Gang of Four”, GoF — describes 23 patterns in three groups:
| Group | What it solves | Examples |
|---|---|---|
| Creational (5) | how to create objects | Singleton, Factory Method, Builder |
| Structural (7) | how to compose objects into structures | Adapter, Decorator, Facade |
| Behavioral (11) | how objects interact | Strategy, Observer, Iterator |
Common mistake
Learning patterns as code templates and sticking them everywhere. A pattern belongs where the problem it solves actually exists.
Takeaway: a pattern is a named solution to a typical problem; what matters is the shared vocabulary, not the code.
How it works
Every GoF pattern is described using the same template: intent, motivation, applicability, participants, consequences, implementation. Intent and consequences matter most: what the pattern solves and what you pay for it.
Where you meet them in .NET
| Pattern | Where in .NET |
|---|---|
| Builder | StringBuilder, WebApplicationBuilder |
| Abstract Factory | DbProviderFactory: CreateConnection, CreateCommand |
| Prototype | the with expression on a record |
| Decorator | BufferedStream, GZipStream on top of Stream |
| Iterator | IEnumerable<T>, yield return |
| Observer | event, IObservable<T> |
| Strategy | IComparer<T> in List<T>.Sort |
Pitfalls
- Look-alike structures. Adapter, Decorator and Proxy look the same — a wrapper around an object. They differ in intent, not in code.
- A name is not a pattern. A static
Createmethod is just a static factory method, not the GoF Factory Method: that one implies a subclass chooses the class of the object being created.
Rule of thumb
Problem first, pattern second. If you can’t name the problem a pattern solves in this particular place, it doesn’t belong there.
Takeaway: GoF patterns are already all over the BCL; recognizing them matters more than writing them from scratch.
Under the hood
- GoF classifies patterns along two axes: purpose (creational, structural, behavioral) and scope (class or object).
- Class-scope patterns — Factory Method, Template Method, Interpreter, the class version of Adapter — rely on inheritance and are fixed at compile time. The rest rely on composition and can change at run time.
Patterns and the language
Back in 1996 Peter Norvig showed that in dynamic languages 16 of the 23 patterns become simpler or disappear. In C# language features did the same: Strategy and Command are Func<T> and Action, Iterator is yield return, Observer is event, Prototype is with, a lazy singleton is Lazy<T>.
Trade-offs
- Every pattern adds types and indirection: code is harder to navigate and stack traces get longer. In return you get an extension point.
- An extension point pays off only when it is used: a second implementation, a substitute in tests, frequent changes in the past.
- GoF is not the only catalog: there are Fowler’s enterprise application patterns (Repository, Unit of Work), enterprise integration patterns, and Microsoft’s cloud design patterns (Retry, Circuit Breaker).
How to discuss it with the team
In review, a pattern name is shorthand for talking about consequences: “this is a decorator, so registration order matters”. If you need to know the pattern to understand a class and the pattern gives nothing back, that is “pattern for the sake of pattern”.
Takeaway: a pattern is vocabulary for discussing design; in C# many GoF patterns became language features, and you pick them by their consequences, not from the catalog.
using System.IO.Compression;
// Iterator: yield return generates an IEnumerator<T>foreach (var n in Evens(6)) Console.Write($"{n} "); // 0 2 4Console.WriteLine();
// Strategy: the comparison algorithm is passed in from outsidevar words = new List<string> { "ccc", "a", "bb" };words.Sort(Comparer<string>.Create((x, y) => x.Length.CompareTo(y.Length)));Console.WriteLine(string.Join(",", words)); // a,bb,ccc
// Decorator: GZipStream adds compression to any Streamusing var buffer = new MemoryStream();using (var gzip = new GZipStream(buffer, CompressionLevel.Optimal, leaveOpen: true)) gzip.Write(new byte[1000]);Console.WriteLine(buffer.Length < 1000); // True
static IEnumerable<int> Evens(int max){ for (var i = 0; i < max; i += 2) yield return i;}- How many patterns are in GoF and how are they grouped? — 23: 5 creational, 7 structural, 11 behavioral.
- Name some patterns in the BCL. —
IEnumerable<T>is Iterator,StringBuilderis Builder,Streamwrappers are Decorator,eventis Observer. - How is a pattern different from an algorithm? — An algorithm is a sequence of steps; a pattern is a set of object roles and relationships.
- How is a pattern different from an architecture style? — Scale: a pattern covers a few classes, a style such as layered architecture covers the whole application.
- Which two principles does GoF put at the start of the book? — Program to an interface, and favor composition over inheritance.
Singleton, Factory and Builder: when to use which, and why is Singleton often called an anti-pattern?
Section titled “Singleton, Factory and Builder: when to use which, and why is Singleton often called an anti-pattern?”What it is
| Pattern | What it does | Example in .NET |
|---|---|---|
| Singleton | one instance per application | AddSingleton in DI |
| Factory | creates an object and picks the class | IHttpClientFactory.CreateClient() |
| Builder | assembles an object step by step | StringBuilder, WebApplicationBuilder |
Why
Code that needs an object shouldn’t have to know how to build it: which class to pick, which parameters to pass, in what order to configure it.
How to use
- Need one shared object — register it in DI as a singleton instead of writing a static
Instanceproperty. - The class is picked by a condition — use a factory.
- Many optional settings — use a builder with chained calls.
Common mistake
Writing a singleton with static Instance for a service that has dependencies. You can’t replace it in a test, and all tests share the same state.
Takeaway: Singleton is one instance, Factory is choosing the class, Builder is step-by-step assembly.
How it works
- Singleton. A private constructor and a static property. Thread-safe and lazy via
Lazy<T>: by default its factory runs exactly once, even when threads race. - Factory Method. The base class declares a virtual creation method, and a subclass decides which object to return.
- Abstract Factory. An interface with several creation methods for a family of matching objects:
DbProviderFactorycreates the connection, command and parameter of a single provider. - Builder. The object is assembled through method calls, and
Build()validates the data and returns the result.
When to choose what
| Task | Solution |
|---|---|
| One instance per application | AddSingleton in DI |
| Object with run-time parameters | factory delegate or factory interface |
| Family of matching objects | Abstract Factory |
| Many optional parts plus validation | Builder |
Pitfalls
- Why Singleton is an anti-pattern. A global access point hides the dependency, state survives between tests, and the class manages its own lifetime — an extra responsibility.
- A fluent API is not always a Builder. Method chaining is an interface style. It’s a Builder when there is a separate builder object and a final
Build().
Rule of thumb
Let the container decide lifetime, a factory handle creation with parameters, and a builder handle complex assembly.
Takeaway: creational patterns separate “what I need” from “how to create it”, and “one instance” in .NET is a registration, not a static.
Under the hood
- A
static readonlyfield is initialized by the type constructor, and the CLR guarantees it runs once. But without an explicit static constructor the runtime chooses when initialization happens, so for real laziness you useLazy<T>. - By default
Lazy<T>runs inExecutionAndPublicationmode: the factory runs under a lock on a single thread. A factory exception is cached — every laterValuethrows the same exception.PublicationOnlydoesn’t cache exceptions but may create extra instances during a race. IHttpClientFactoryis a pooling factory: anHttpClientis created every time, while theHttpMessageHandleris reused and recreated on a timer, every two minutes by default.
Trade-offs
- Static versus DI. A static singleton is reachable from anywhere; DI makes the dependency visible and replaceable. Static is fine for immutable values with no dependencies:
Comparer<T>.Default,Array.Empty<T>(). - Your own factory. The container already is a factory, and a factory interface per service is a useless layer. You need your own factory when the object depends on request data or has to live shorter than a long-lived consumer.
- Builder versus
record. A builder gives step-by-step validation and an immutable result, but doubles the number of types.
How to check in production
- A singleton with mutable state shows up as race conditions under load. In review, look for
staticfields holding mutable collections and forInstanceproperties. - A failed
Lazy<T>initialization shows up as the same exception on every request until the process restarts.
Takeaway: “one instance” is a composition root decision, a factory is for run-time creation, a builder is for complex assembly, and each comes at the price of indirection.
// Singleton via Lazy<T>: the factory runs once, even from several threadsParallel.For(0, 8, i => { var config = Config.Instance; });Console.WriteLine(Config.Created); // 1
// Builder: step-by-step assembly, validation in Build()var mail = new EmailBuilder().To("ann@example.com").Subject("Hi").Build();Console.WriteLine(mail); // Email { To = ann@example.com, Subject = Hi }
public sealed class Config{ private static readonly Lazy<Config> instance = new(() => new Config()); public static int Created; public static Config Instance => instance.Value; private Config() => Interlocked.Increment(ref Created);}
public record Email(string To, string Subject);
public class EmailBuilder{ private string? to, subject; public EmailBuilder To(string value) { to = value; return this; } public EmailBuilder Subject(string value) { subject = value; return this; } public Email Build() => new(to ?? throw new InvalidOperationException("To is required"), subject ?? "");}Exporter exporter = new CsvExporter();Console.WriteLine(exporter.Export([1, 2, 3])); // 1;2;3
// The base class fixes the workflow, the subclass picks which object to createpublic abstract class Exporter{ public string Export(int[] rows) => CreateFormatter().Format(rows); protected abstract IFormatter CreateFormatter(); // factory method}
public class CsvExporter : Exporter{ protected override IFormatter CreateFormatter() => new CsvFormatter();}
public interface IFormatter { string Format(int[] rows); }
public class CsvFormatter : IFormatter{ public string Format(int[] rows) => string.Join(";", rows);}- How do you write a thread-safe lazy singleton in C#? —
Lazy<T>in a static field, or simply astatic readonlyfield. - Why is
AddSingletonbetter than a staticInstance? — The dependency is visible in the constructor and replaceable in tests, and the composition root sets the lifetime. - How does Factory Method differ from Abstract Factory? — The first is one virtual method with the choice made in a subclass; the second is an object with methods for a whole family.
- Why
IHttpClientFactoryif you can justnew HttpClient()? — It reuses and refreshesHttpMessageHandler: no socket exhaustion and no stale DNS. - What happens if the
Lazy<T>factory throws? — In the default mode the exception is cached and rethrown on every access toValue.
Adapter, Decorator, Facade and Proxy: how do they differ?
Section titled “Adapter, Decorator, Facade and Proxy: how do they differ?”What it is
All four patterns wrap another object. The difference is why.
| Pattern | Why wrap | Example in .NET |
|---|---|---|
| Adapter | convert a foreign interface into yours | StreamReader over Stream |
| Decorator | add behavior, same interface | BufferedStream, GZipStream |
| Facade | simple entry into a complex subsystem | DbContext |
| Proxy | control access, same interface | lazy loading in EF Core |
How to use
- Plugging in a library with an awkward API — write an adapter to your own interface.
- Need logging or caching without changing the class — use a decorator.
Common mistake
Calling every wrapper a decorator. If the interface changes, it’s an adapter. If the wrapper adds no behavior but decides whether to call the object at all, it’s a proxy.
Takeaway: an adapter changes the interface, a decorator adds behavior, a facade simplifies, a proxy controls access.
How it works
- Adapter. Implements the client’s interface and translates calls into the API of the adapted object. In your own code it is usually a wrapper over an external service’s SDK.
- Decorator. Implements the same interface and receives it in the constructor, so decorators nest inside each other.
DelegatingHandleris a decorator forHttpMessageHandler: it can do something before and afterbase.SendAsync. - Facade. Its own interface in front of several objects.
DbContexthides the connection, change tracking and SQL generation, and itsDatabaseproperty returns aDatabaseFacadeobject for operations on the database itself. - Proxy. The same interface, but it decides whether and when to call the real object: lazy creation, remote call, permission check.
When to choose what
| Task | Pattern |
|---|---|
| A foreign API doesn’t match your interface | Adapter |
| Add logging, retries, caching | Decorator |
| Hide five calls behind one | Facade |
| Defer loading or call remotely | Proxy |
Pitfalls
- A proxy hides the cost. Accessing a lazily loaded navigation property is a database query, and in a loop you get N+1.
- Decorator order. Logging outside the retries writes one entry, inside them — one entry per attempt.
How to register a decorator in the built-in container is covered in Dependency injection.
Rule of thumb
State what the wrapper changes: the interface, the behavior, the complexity or the access — that gives you the name.
Takeaway: the four patterns are built alike and are told apart by intent.
Under the hood
- A dynamic proxy is a type generated at run time with
System.Reflection.Emit. Castle DynamicProxy creates a subclass that overridesvirtualmembers, or an interface implementation. This is how Moq mocks and the lazy-loading proxies fromMicrosoft.EntityFrameworkCore.Proxieswork. DispatchProxyin the BCL creates a type that implements an interface and routes every call toInvoke(MethodInfo, object[]).- Run-time code generation isn’t available under Native AOT, so these proxies don’t work there.
Cost
- Every layer is one more call through an interface or a virtual method. Next to I/O it’s invisible; in a hot loop it shows.
DispatchProxyallocates an argument array on every call and boxes value types.
Trade-offs
- A hand-written decorator is explicit and checked by the compiler, but you write one per interface. An intercepting proxy covers all interfaces, but its behavior isn’t visible in the code.
- Source generators are a third option: the wrapper is generated at build time, visible in the debugger and AOT-compatible.
- A facade at a module boundary reduces coupling, but a fat facade gradually absorbs all the logic.
How to check in production
- Lazy loading shows up as a stream of small identical queries in the EF Core log for a single operation.
- Types like
Castle.Proxies.OrderProxyin an exception stack trace or in a dump tell you the call went through an interceptor.
Takeaway: wrappers differ in intent and contract; a dynamic proxy is more powerful than a hand-written decorator, but hides behavior and doesn’t work with AOT.
INotifier notifier = new LoggingNotifier(new SmsAdapter(new LegacySmsClient()));notifier.Send("Order shipped");// log: Order shipped// SMS: ORDER SHIPPED
public interface INotifier { void Send(string message); }
// Someone else's class with an awkward API: we can't change itpublic class LegacySmsClient{ public void Transmit(string upperText) => Console.WriteLine($"SMS: {upperText}");}
// Adapter: the INotifier interface on top of the foreign APIpublic class SmsAdapter(LegacySmsClient client) : INotifier{ public void Send(string message) => client.Transmit(message.ToUpperInvariant());}
// Decorator: same interface plus loggingpublic class LoggingNotifier(INotifier inner) : INotifier{ public void Send(string message) { Console.WriteLine($"log: {message}"); inner.Send(message); }}using System.Reflection;
var greeter = DispatchProxy.Create<IGreeter, LoggingProxy>();((LoggingProxy)(object)greeter).Target = new Greeter();Console.WriteLine(greeter.Greet("Ann"));// call Greet// Hello, Ann
public interface IGreeter { string Greet(string name); }
public class Greeter : IGreeter{ public string Greet(string name) => $"Hello, {name}";}
// The proxy type is generated at run time; every call lands in Invokepublic class LoggingProxy : DispatchProxy{ public IGreeter? Target { get; set; }
protected override object? Invoke(MethodInfo? method, object?[]? args) { Console.WriteLine($"call {method!.Name}"); return method.Invoke(Target, args); }}- How does a decorator differ from a proxy if the interface is the same? — A decorator adds behavior; a proxy controls access: whether and when to call the object.
- Why must properties be
virtualfor EF Core lazy loading? — The proxy is a generated subclass that overrides the navigation properties. - Where are the decorators in
HttpClient? —DelegatingHandlerin the handler chain: each one calls the next throughbase.SendAsync. - How does a facade differ from an adapter? — An adapter fits one interface to the expected one; a facade introduces a new simplified interface to several objects.
- Why don’t dynamic proxies work under Native AOT? — They generate types at run time, while AOT compiles everything ahead of time.
Strategy, Observer, Command, Chain of Responsibility and Template Method: where are they in everyday code?
Section titled “Strategy, Observer, Command, Chain of Responsibility and Template Method: where are they in everyday code?”What it is
Behavioral patterns describe how objects share responsibilities and communicate.
| Pattern | Idea | Where in .NET |
|---|---|---|
| Strategy | the algorithm is passed in from outside | IComparer<T>, lambdas in LINQ |
| Observer | an object notifies subscribers | event, IObservable<T> |
| Command | a request is an object | ICommand in WPF, command handlers |
| Chain of Responsibility | a request passes through links in turn | ASP.NET Core middleware |
| Template Method | algorithm skeleton in a base class | BackgroundService.ExecuteAsync |
How to use
list.Sort(comparer)— you pass a comparison strategy.button.Click += OnClick— you subscribe as an observer.- You inherit
BackgroundServiceand writeExecuteAsync— you fill in a step of a template method.
Common mistake
Subscribing to an event of a long-lived object and never unsubscribing. The publisher holds a reference to the subscriber, and the garbage collector can’t free it.
Takeaway: Strategy is a swappable algorithm, Observer is notification, Command is a request as an object, Chain is a pipeline, Template Method is a skeleton with hooks.
How it works
- Strategy. An interface or delegate with one method; the context calls it without knowing the implementation.
- Observer. An
eventis a list of delegates.IObservable<T>adds completion and error signals, plus unsubscribing throughIDisposable. - Command. A request with its data is a separate object, executed by a handler. Queues and CQRS handlers are built on this; more in Architecture styles.
- Chain of Responsibility. A link receives the request and a reference to the next link and decides: handle it, pass it on, or short-circuit. Middleware works this way; more in Hosting and the pipeline.
- Template Method. A non-virtual method calls virtual steps.
BackgroundServiceimplements start and stop and calls yourExecuteAsync.
When to choose what
| Task | Pattern |
|---|---|
| The algorithm changes with configuration | Strategy |
| Several reactions to one event | Observer |
| A request must be deferred, retried, logged | Command |
| Sequential checks and handlers | Chain |
| Common order of steps, different details | Template Method |
Pitfalls
- An exception in one
eventhandler stops the remaining handlers from being called. - A template method with a dozen hooks makes the base class fragile: changing the order of steps breaks subclasses.
Rule of thumb
One operation — a delegate; several operations or state — an interface; a shared skeleton — a template method, when composition isn’t enough.
Takeaway: behavioral patterns decide who makes the decision, and in C# many of them are expressed with delegates.
Under the hood
- A field-like event compiles into a private delegate field and
addandremovemethods. They atomically swap the immutable multicast delegate usingInterlocked.CompareExchange, and invocation walks a snapshot of the list, so unsubscribing during notification is safe. - A strategy lambda that captures variables means a closure allocation. A lambda without captures is cached by the compiler.
- The middleware pipeline is built once at startup: each link wraps the next, and a request runs through a ready-made chain of delegates.
Cost
- A subscription holds the subscriber through a strong reference: an object subscribed to a singleton’s event lives as long as the singleton.
- A command object and a dispatcher add an allocation and an indirect call; finding handlers via reflection also adds startup time.
Trade-offs
- Template Method versus Strategy. Inheritance guarantees the order of steps; composition lets you change and test steps independently.
eventversusIObservable<T>and channels. An event is synchronous and has no backpressure; for asynchronous data streams there are Rx andSystem.Threading.Channels.- Your own dispatcher versus MediatR. The library gives you a shared handler pipeline but hides the command-to-handler link from code navigation. Since version 13 it has a commercial license.
How to check in production
- A leak through events is visible in
dotnet-gcdump: the subscriber’s retention path goes through a delegate in the publisher’s field. - Middleware order is pinned down with an integration test on
WebApplicationFactory.
Takeaway: behavioral patterns in .NET are mostly delegates and pipelines; their price is implicit control flow and strong references from subscriptions.
// Strategy: the discount rule is passed as a delegatevar checkout = new Checkout(price => price * 90 / 100);
// Observer: subscribers learn about the payment without knowing about each othercheckout.Paid += total => Console.WriteLine($"email: paid {total}");checkout.Paid += total => Console.WriteLine($"audit: {total}");
checkout.Pay(200);// email: paid 180// audit: 180
// Template Method: the order of steps is fixed in the base classnew CsvReport().Print(); // header | csv rows | footer
public class Checkout(Func<int, int> discount){ public event Action<int>? Paid; public void Pay(int price) => Paid?.Invoke(discount(price));}
public abstract class Report{ public void Print() => Console.WriteLine($"header | {Body()} | footer"); protected abstract string Body(); // the step a subclass provides}
public class CsvReport : Report{ protected override string Body() => "csv rows";}- How does Strategy differ from Template Method? — Strategy swaps the algorithm through composition at run time; Template Method does it through inheritance at compile time.
- Why can an event cause a memory leak? — The publisher holds the subscriber through a delegate; without unsubscribing it lives as long as the publisher.
- What does
IObservable<T>offer beyondevent? — Completion, errors, unsubscribing throughIDisposable, and Rx operators. - Is middleware Chain of Responsibility? — Yes, a variant: a link can handle the request, pass it on, or short-circuit the chain.
- Is MediatR an implementation of CQRS? — No, it’s the Mediator pattern for delivering messages to handlers; CQRS is about separate read and write models.
Which anti-patterns are the most common and how do you get rid of them?
Section titled “Which anti-patterns are the most common and how do you get rid of them?”What it is
An anti-pattern is a solution that is common and seems convenient, but over time makes code more expensive to change. It has a known replacement.
The most common ones
- God object. A single 3,000-line
OrderManagerdoes everything: validation, calculation, SQL, sending emails. - Magic numbers.
if (status == 3)— only the author knows what 3 means. - Copy-paste. The same logic in five places, fixed in four of them.
- Service Locator.
provider.GetService<T>()inside methods instead of constructor parameters.
How to get rid of them
- Cover the code with tests so refactoring doesn’t break anything.
- Change things in small steps: extract a constant, extract a method, extract a class.
Common mistake
Rewriting everything from scratch. The old code holds knowledge about edge cases that the new code will lose.
Takeaway: an anti-pattern is a familiar bad solution; you fix it in small steps protected by tests.
How it works
A code smell is a visible symptom: a long function, a large class, a long parameter list. An anti-pattern is the diagnosis behind it. Martin Fowler collected the catalog of smells and the refactorings against them in “Refactoring”; the second edition came out in 2018.
| Anti-pattern | Symptom | Refactoring |
|---|---|---|
| God object | Large Class, dozens of dependencies | Extract Class |
| Magic numbers | literals in conditions | Replace Magic Literal |
| Primitive obsession | string for email, decimal for money |
Replace Primitive with Object |
| Shotgun Surgery | one edit — many files | Move Function, Combine Functions into Class |
| Pattern for the sake of pattern | Speculative Generality | Inline Class, Collapse Hierarchy |
| Service Locator | GetService in business code |
constructor parameter |
Pitfalls
- The anemic model is a debatable label. For CRUD without rules it’s fine. It becomes a problem when the same checks are duplicated across services; more in Architecture styles.
- The cure causes a new disease. Split a God object into small meaningless classes and you get Shotgun Surgery.
Rule of thumb
Refactoring is a separate commit with no behavior change: small steps and green tests after each one.
Takeaway: a smell is the symptom, an anti-pattern is the diagnosis, a refactoring from Fowler’s catalog is the cure.
How they appear
- Anti-patterns are rarely written on purpose. A God object grows because adding a method to an existing class is cheaper than inventing a new one: each decision is locally reasonable, the sum is not.
- Big Ball of Mud (Foote and Yoder, 1997) is the result of deadline pressure, people turnover, and growth without refactoring.
Cost
- God objects and Shotgun Surgery make every change more expensive: more merge conflicts, more regressions, longer reviews.
- Primitive obsession pushes validation to run time: an email is checked in ten places or in none.
Trade-offs
- Not every label is a verdict. An anemic model is acceptable for CRUD, and
TimeSpan.FromSeconds(30)reads fine without a constant. - “Pattern for the sake of pattern” and YAGNI pull one way, DRY and extensibility the other. Introduce an abstraction on the second or third case, not the first.
- A large refactoring is a project: you cut it into steps that can each ship on their own, for example using a Strangler Fig.
How to check in production
- Roslyn analyzers and SonarQube catch some smells: method complexity, duplication, magic numbers.
- Change frequency per file in git history shows hotspots: a large file that changes in almost every commit is a God object candidate.
- Architecture tests (NetArchTest, ArchUnitNET) pin down layer boundaries so the Big Ball of Mud doesn’t come back.
Takeaway: anti-patterns grow out of locally reasonable decisions; you catch them with metrics and review and cure them with gradual refactoring, not a rewrite.
// Before: if (status == 3 && email.Contains('@')) — a magic number and a string instead of a type// After: a named state and a type validated on creationvar email = Email.Parse("ann@example.com");Console.WriteLine(CanShip(email, OrderStatus.Paid)); // True
try { Email.Parse("not-an-email"); }catch (ArgumentException e) { Console.WriteLine(e.Message); } // Invalid email
static bool CanShip(Email email, OrderStatus status) => status == OrderStatus.Paid;
public enum OrderStatus { New = 1, Reserved = 2, Paid = 3 }
// Replace Primitive with Object: an invalid email can't be createdpublic sealed record Email{ public string Value { get; } private Email(string value) => Value = value;
public static Email Parse(string value) => value.Contains('@') ? new Email(value) : throw new ArgumentException("Invalid email");}- How is an anti-pattern different from a code smell? — A smell is a local symptom; an anti-pattern is the recurring bad solution behind it.
- Why is Service Locator an anti-pattern? — It hides dependencies and moves errors to run time; details are in the “Dependency injection” topic.
- How do you break up a God object? — Find groups of fields and methods that change together and move them into separate classes (Extract Class) in small steps.
- Is an anemic model always bad? — No: for CRUD without invariants it’s fine; it’s bad when rules are duplicated across services.
- When is a rewrite better than refactoring? — Rarely; more often the old system is replaced piece by piece with a Strangler Fig.