Observer & When Patterns Hurt
Observer decouples an audit/event publisher from its subscribers (a preview of domain events) - and an honest lesson on over-engineering: when a pattern adds cost without benefit.
On this page
One last behavioral pattern, then the module's most important lesson. Observer decouples a source of events from the things that react to them - the foundation of event-driven design and a preview of domain events. Then we close with the counterweight to a whole module about patterns: when patterns hurt. Because the mark of seniority isn't knowing twenty-three patterns - it's the restraint to use three.
Observer: publish, don't call
ledger-legacy posts a transaction and then, inline, must update the audit log, refresh a cache, and email a
receipt. That hard-wires the transaction code to three unrelated concerns - and every new reaction means editing
it (an Open/Closed violation). Observer inverts this: the source publishes an event; interested parties
subscribe; the source never knows who's listening.
interface TransactionListener { void onPosted(Transaction t); } // observer interface
class TransactionService { // the SUBJECT
private final List<TransactionListener> listeners; // subscribers
void post(Transaction t) {
ledger.record(t);
listeners.forEach(l -> l.onPosted(t)); // notify all - who they are is irrelevant here
}
}
class AuditListener implements TransactionListener { public void onPosted(Transaction t){ audit.log(t); } }
class ReceiptListener implements TransactionListener { public void onPosted(Transaction t){ email.receipt(t); } }Now adding a reaction is registering a new listener - TransactionService never changes. The publisher and
subscribers are decoupled: each can change independently. This is the seed of domain events (next module) and
of Spring's ApplicationEventPublisher, which is Observer built into the framework. It underlies message queues,
UI event handlers, and reactive streams.
Observer's hidden costs
Decoupling has a price you should name. Event flows are harder to trace - 'who reacts to this?' has no compile-time answer, so debugging means finding every listener. Ordering and error handling get subtle (if the audit listener throws, does the receipt still send?). And synchronous listeners can silently slow the publisher. Observer is powerful, but it trades an obvious call stack for a decoupled-but-invisible one. Use it when reactions genuinely vary and multiply, not to decouple two things that only ever talk to each other.
When patterns hurt
Here's the counterweight to this whole module. Every pattern buys flexibility with complexity - more classes, more indirection, more names to learn. That trade is worth it only against real, present need. Applied reflexively, patterns produce code that's harder to change than the mess they replaced. Watch for these smells:
- A pattern with one implementation, forever. A Strategy interface with a single strategy, an Abstract Factory making one family - the flexibility is never used, so it's pure overhead.
- Indirection you can't follow. If tracing one request means hopping through five patterns, you've optimized for imagined change at the cost of actual readability.
- Patterns naming trivia. A
NullObjectSingletonProxyFactoryfor a config flag. The name-to-value ratio is the tell. - Speculative generality. "We might need to swap databases / support plugins / go multi-tenant" - built now, needed never. This is YAGNI's exact target.
The senior heuristic: write the simplest thing that works, and let patterns emerge when simplicity actually
strains. A duplicated if isn't yet a Strategy; a second use might make it one. Refactor toward a pattern
when a real second case arrives - not in anticipation of one. Removing a needless pattern is as valuable a
refactor as adding a needed one.
A novice carpenter, thrilled by every new tool, mounts all forty on the bench and reaches for the biscuit joiner to hang a picture - the tools now crowd the work. A master keeps a curated few within reach and fetches the specialist jig only when a job truly needs it; their bench looks almost bare. Patterns are those tools. Knowing all twenty-three is good; covering every surface of your code with them is the novice's cluttered bench. The craft is choosing the fewest tools that do the job cleanly - and knowing that most days, a chisel and a plane are all you need.
A service currently has one payment provider, integrated with a direct call. A teammate wants to introduce a
PaymentStrategy interface, an AbstractPaymentProviderFactory, and an Observer-based event system 'so we're
ready for more providers and easy to extend.' There are no concrete plans for a second provider. Make the case
for keeping it simple now, and state the specific condition under which you'd add each pattern later.
What is the senior heuristic for applying design patterns?
Key takeaways
- Observer decouples an event source (subject) from reactors (observers): the subject publishes, subscribers register, and the subject never knows who listens.
- It's the foundation of domain events, Spring's ApplicationEventPublisher, message queues, and reactive streams - adding a reaction needs no change to the publisher.
- Observer's costs: harder-to-trace flows, subtle ordering/error handling, and synchronous listeners that can slow the publisher.
- Every pattern trades complexity for flexibility; applied without real need, patterns make code harder to change than the mess they replace.
- The senior heuristic: write the simplest thing that works and refactor toward a pattern when simplicity strains against real, present change - removing a needless pattern is a valid refactor too.