Design patterns are a vocabulary. Saying "this is a strategy" or "that is an adapter" lets a team discuss structure quickly. The trouble starts when the vocabulary becomes a checklist and patterns get applied because they exist, not because the code needs them.

This post covers four patterns I have removed from production code in the last few years. For each one: what it looked like, what it cost, and the signals that would have made it worth keeping.

1. Singleton holding mutable state

What we had

A Python service had a Config singleton that also cached feature flags fetched from a remote service:

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.flags = {}
            cls._instance.loaded_at = None
        return cls._instance

    def flag(self, name):
        if self.loaded_at is None or time.time() - self.loaded_at > 60:
            self.flags = fetch_flags()
            self.loaded_at = time.time()
        return self.flags.get(name, False)

What it cost

  • Tests interfered with each other. One test set a flag; a later test in the same process saw it. We had a fixture that reached into Config._instance to reset it, which every new test author had to know about.
  • Hidden network calls. Any function anywhere could call Config().flag(...), which might trigger an HTTP request. Latency spikes in unrelated endpoints traced back to flag refreshes.
  • No way to run two configurations side by side, which we needed for a multi-tenant migration.

What we did instead

A plain FlagClient object created once at startup and passed to the code that needs it. The module-level instance still exists for convenience in scripts, but nothing reaches for it implicitly.

When a singleton pays off

When the object is genuinely one-per-process, immutable or thread-safe, and expensive to create: a connection pool, a compiled regex table, a logger. Even then, a module-level variable created at startup is usually simpler than the __new__ dance. The problem is rarely "only one instance"; it is global, mutable, implicitly accessed state.

2. Abstract factory with one product

What we had

Code that generated PDF invoices:

public interface DocumentFactory {
    Renderer createRenderer();
    Layout createLayout();
    FontProvider createFontProvider();
}

public class PdfDocumentFactory implements DocumentFactory { ... }

Plus a DocumentFactoryProvider, and a config key to choose the factory. There was one factory. There had only ever been one factory. The original author expected HTML and Word output "soon"; four years later neither had been asked for.

What it cost

Every change to invoice rendering touched four or five files. New engineers spent their first day on the billing team working out which interface to open. When we finally did add HTML output, the abstraction did not fit: HTML had no use for FontProvider and needed a template engine the interface did not model. We changed the interface anyway, so the up-front abstraction saved nothing.

What we did instead

Deleted the factory, provider and interfaces. PdfInvoiceRenderer became one class. When HTML arrived we wrote HtmlInvoiceRenderer and introduced a small InvoiceRenderer interface with one method, render(Invoice): byte[], shaped by two real implementations instead of one guess.

When an abstract factory pays off

When you have families of related objects that must match, and more than one family exists today. UI toolkits with per-platform widgets are the classic case: a Windows button must go with a Windows scrollbar. If you have one family, you have a constructor.

3. Observer that leaked memory

What we had

A Node.js server used an EventEmitter so modules could react to order updates. A websocket handler subscribed per connection:

wss.on("connection", (socket, req) => {
  const onUpdate = (order) => {
    if (order.userId === req.userId) socket.send(JSON.stringify(order));
  };
  orderEvents.on("updated", onUpdate);
});

What it cost

Nobody removed the listener when the socket closed. Every closed connection left a closure holding the socket in memory. After a day the process used several gigabytes, and Node had been printing a warning we ignored:

MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 updated listeners added to [EventEmitter]. MaxListeners is 10.
Use emitter.setMaxListeners() to increase limit

Someone had "fixed" that warning with setMaxListeners(0).

What we did instead

Tied every subscription to the lifetime of its owner:

wss.on("connection", (socket, req) => {
  const onUpdate = (order) => { /* ... */ };
  orderEvents.on("updated", onUpdate);
  socket.once("close", () => orderEvents.off("updated", onUpdate));
});

In newer code we use an AbortSignal: addEventListener on an EventTarget, and Node's events.on() and events.once() helpers, accept a signal, and aborting it removes the listener. One controller.abort() on close cleans up everything the connection registered.

When an observer pays off

When publishers genuinely should not know their subscribers, and subscribers have a clear lifetime you can tie the unsubscribe to. If the subscriber lives as long as the process, it is fine. If it lives as long as a request, connection or component, unsubscribe must be part of the same code that subscribes, not a separate cleanup someone has to remember.

4. Strategy hierarchy for two if statements

What we had

class ShippingStrategy(ABC):
    @abstractmethod
    def cost(self, order) -> int: ...

class StandardShipping(ShippingStrategy):
    def cost(self, order): return 0 if order.total >= 5000 else 499

class ExpressShipping(ShippingStrategy):
    def cost(self, order): return 1299

class ShippingStrategyFactory:
    _map = {"standard": StandardShipping, "express": ExpressShipping}
    @classmethod
    def for_method(cls, method): return cls._map[method]()

Three classes and a factory for what is a two-line lookup.

What we did instead

def shipping_cost(order) -> int:
    if order.shipping == Shipping.EXPRESS:
        return 1299
    return 0 if order.total_cents >= 5000 else 499

When a strategy pays off

When strategies have real behaviour of their own (several methods, state, configuration), when they are chosen at runtime from a list that grows, or when callers outside your code need to supply their own. In languages with first-class functions, a dictionary of functions often gives you the strategy pattern without the class hierarchy.

Signals that a pattern is worth it

Across all four cases, the patterns were added in anticipation. They were removed because the anticipated need never came, or came in a different shape. The signals I look for now before adding one:

Signal Example
A second real implementation exists or is scheduled HTML renderer is in this sprint, not "someday"
Tests need a substitute A fake payment gateway for unit tests
Callers outside your module need to plug in A library letting users supply serialisers
The variation is runtime, not compile time Strategy chosen per tenant from config
Lifetime and ownership are clear Observer unsubscribes when its owner closes

If none of these apply, write the direct version. Refactoring toward a pattern when the second case arrives is usually a small change. Living with the wrong abstraction for years is not.

Patterns that rarely cost anything

To be fair to the catalogue, some patterns almost always earn their place:

  • Adapter around third-party clients, so the rest of your code depends on your interface and you can swap or fake the vendor.
  • Builder for objects with many optional parameters, in languages without named arguments.
  • Repository at the boundary between domain logic and the database, as long as it stays thin.

What they have in common: they sit at a boundary you already have, rather than inventing a new one.

Summary

  • Patterns are vocabulary first, structure second. Use them when the code already has the shape.
  • Singletons hurt when they hold mutable state accessed implicitly. Pass the object instead.
  • An abstract factory with one family is a constructor with extra files.
  • Every observer subscription needs an unsubscribe tied to the subscriber's lifetime.
  • Prefer a function or a dictionary of functions to a strategy hierarchy until behaviour grows.

References: Node.js EventEmitter, AbortSignal on MDN, Python abc module.