A curated set is defined as much by what it excludes as what it keeps. Every pattern below is real, useful, and earns its place in some system somewhere. The question is never whether a pattern is good. It's whether the production-readiness it buys you is worth the capacity it costs a team that has to maintain what it ships. For each one here, the answer is usually no until something specific forces your hand. That something is the skip-if, read in reverse.
A pattern you carry without a reason isn't architecture. It's debt with a fancier name.
The Gang of Four gave us Singleton (GoF, 1994) as a way to guarantee one instance. In practice most teams use it as a respectable-looking global variable, and a global variable is a global variable however you dress it. It hides dependencies and defeats your tests. Worse, it turns object lifetime into something nobody on the team can reason about.
You already have a better tool. A DI container manages a single instance for you when you register it, and does it without the static coupling. This is the Dependency Inversion Principle doing its job (Robert C. Martin, the "D" in SOLID, Agile Software Development: Principles, Patterns, and Practices): depend on an abstraction the container supplies, not on a static you reach for by name. Register the thing as a singleton lifetime and inject it.
// Not this: a static no test can replace.
public static class Clock { public static DateTime Now => DateTime.UtcNow; }
// This: one instance, injected, swappable in a test.
services.AddSingleton<IClock, SystemClock>();The skip-if reads in reverse: if you find yourself writing Whatever.Instance, you wanted a DI lifetime.
These three won so completely that C# absorbed them into the language. Observer is what event and IObservable<T> already do. Iterator is IEnumerable<T> and yield return. Builder is the object initialiser and, increasingly, the with expression on a record. The concepts are still correct, all three of them GoF. As hand-rolled patterns they're now redundant.
Writing them out by hand in 2026 reinvents a compiler feature you already paid for, and there's nothing disciplined about that. We teach what the language doesn't give you for free and skip what it does.
Don't hand-roll a pattern your compiler already ships.
This is the worked over-engineering example in the component chapter, so the verdict is short here. An ORM trades hand-written SQL for change-tracking magic, LINQ that compiles to queries you didn't write, and a migration history a junior can't read. The book's stance is SQL-first data access: a thin Data Gateway over stored procedures (Fowler, PoEAA), where the query you ship is the query you wrote.
Skip the ORM until you genuinely have a domain so large that mapping by hand is the bottleneck, and a team big enough to own the framework's behaviour when it surprises you. Most small teams never reach either.
A Saga (Garcia-Molina & Salem, 1987; Richardson) coordinates a multi-step transaction across services using compensating actions, because you can't hold a single database lock across a network. It is the right answer to a real and painful problem. The trouble is that most systems billed as needing one have a transaction that still fits inside a single database.
If your work commits in one transaction against one store, you don't have a distributed transaction, and a Saga is pure overhead: orchestration code, compensation logic, and a state machine to debug at 3am. Reach for it the day a business operation truly spans two systems that can each fail on their own. Not before.
Download the full PDF for free?
Free download — no account required