When a pattern is the wrong answer

An abstraction costs a file, a name and a place for a bug to hide. The test is whether there is a second implementation today.

6 min read📐 Low-Level Design

Every pattern in this course solves a real problem. Applied where that problem does not exist, each one is pure cost — more files, more indirection, and a reader who has to hold more in their head to understand less.

This lesson is the other half, and it is the more useful half, because the failure mode of learning patterns is applying them.

The cost, stated plainly

An abstraction is not free. It costs:

  • A file to open. A strategy interface with one implementation means finding the answer takes two jumps instead of none.
  • A name to learn. OrderProcessingStrategyFactory is four concepts before any behaviour.
  • A place for a bug to hide. Indirection is where "which implementation is actually running" becomes a question.
  • Something to explain, in review, in onboarding, and to yourself in six months.

Those are worth paying when the abstraction earns them. They are pure loss when it does not, and the loss is invisible in the diff that introduces it — a factory looks like good engineering at the moment it is written and like noise a year later.

The test: is there a second one?

The single most useful question, and it is nearly always decisive:

Is there a second implementation, today, that this abstraction serves?

Not "might there be". Not "this makes it easy to add one later". Today.

A strategy interface with one implementation is an interface with one implementation. It has not made anything flexible; it has made one class into two files and moved the decision nowhere. When the second implementation arrives you will discover that the interface you guessed at is the wrong shape for it anyway — which is the practical answer to "but it will be easy to add later": it will not, because you abstracted before you knew what varied.

The corollary is the rule worth keeping: wait for the second case. Two implementations tell you what actually differs. One tells you nothing, and the refactor from a concrete class to an interface is fifteen minutes with an IDE.

The specific overuses

A factory for one type. UserFactory.create() calling new User(). It has added a file and a name and removed nothing.

A singleton for something that is not global. The moment it has mutable state it is shared state; the moment it is injected it did not need to be a singleton pattern, only a singleton scope.

Interfaces named for their implementation. UserService and UserServiceImpl, with nothing else implementing it and nothing planning to. The interface exists because a habit said so. If there is one implementation and no test needs a double — and modern mocking does not — the interface is a second file that must be kept in step with the first.

An event for a direct call. Publishing OrderPlaced so that one listener in the same module can react, instead of calling the method. You have swapped a stack trace you can read for control flow you cannot.

A layer that only forwards. A service whose every method calls one repository method with the same arguments. It is a file of return repository.x(y), and it is where "we should have a service layer" was applied without asking what the service layer is for.

Generic everything. A configurable, pluggable, strategy-driven system for a problem with one case, built because the second case is imagined. This is the most expensive version, because it is expensive to remove as well as to write.

Where the patterns genuinely earn it

The same list, inverted — and this is what the earlier lessons were showing:

  • Decorator, when behaviours genuinely combine. Buffered, gzipped and counted is four classes instead of eight.
  • Chain of responsibility, when the set of handlers changes and the handlers must not know each other. Spring Security's sixteen filters.
  • Strategy, when there are several implementations and the choice is made at run time. A payment processor per method is the real case.
  • Builder, when there are many parameters and some are optional.
  • Observer, when the publisher genuinely must not know its subscribers — usually because they are in another module or another service.

Every one of those has "several" or "changes" or "another module" in it. That is the difference.

A question that resolves most arguments

When somebody proposes a pattern, ask: what will be easier to change, and what will be harder?

A good answer names both. "Adding a payment method becomes one class instead of editing a switch; the cost is that finding which processor ran now needs the registry." That is the trade-off shape from the system design course, applied at the level of one class, and it is the same discipline — a decision with a price is a decision, and a decision with only benefits has not been examined.

If the answer is only "it is more flexible", nothing has been said.

Progress is saved on this device and to your account when signed in.