Free Interactive Course · Design Patterns

Observer Design Pattern

Announce that something happened, and let anyone who cares react — without ever learning who they are.

Behavioural Patternseasya.k.a. Publish–Subscribea.k.a. Listener
ShareXLinkedIn
In one sentence

Define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically.

01

The problem Observer solves

The problem

OrderService.place() saves an order. Then marketing asks for a confirmation email, so it calls the mailer. Then ops wants an SMS. Then analytics wants an event. Then the warehouse wants stock decremented, and finance wants an invoice row.

Now a method whose job is save an order imports six unrelated subsystems, and a flaky SMS provider can fail an order that was already written to the database. Every new reaction means editing, re-reviewing and re-deploying the single most critical method in your system.

And the coupling points the wrong way: ordering is core, notifications are peripheral, yet the core is the thing that has to know about all of them.

02

How the Observer pattern works

Invert who knows whom. Instead of the order service calling six subsystems, the six subsystems register their interest and the order service announces one fact:

  1. The subject keeps a list of observers and offers subscribe() / unsubscribe().
  2. When something noteworthy happens, the subject walks the list and calls one agreed method on each — onOrderPlaced(order).
  3. The subject knows only the interface. It never learns that EmailListener exists, and adding a seventh listener changes nothing inside it.
Observer vs. Pub/Sub — the distinction people trip on. Classic Observer is in-process and direct: the subject holds references to its observers and calls them, usually synchronously, on the caller's thread. Publish–subscribe with a message broker (Kafka, SNS, RabbitMQ) adds a middleman so publisher and subscriber don't even share a process. Same intent, wildly different failure modes — the broker version survives a subscriber being down, and the in-process version does not.
OrderService- listeners: List<OrderListener>+ subscribe() / place()«interface» OrderListener+ onOrderPlaced(order)0..*EmailListenerSmsListenerAnalyticsListenerOrderService points only at the interface — never at a concrete listener
Participants. The Subject owns the subscriber list and does the notifying. The Observer interface is the only type the subject knows. Concrete Observers implement it. Every arrow from the subject stops at the dashed box — that boundary is the entire benefit.
03

See it: one event, many reactions

Publish the event and watch it fan out. Then detach a listener and publish again, or attach a new one — and note the counter tracking edits to OrderService.

▶ Try it — publish once, fan out

An interactive event bus: publishing order.placed notifies every subscribed listener in turn, and listeners can be detached or attached at runtime without any change to OrderService.

Now look at the failure mode you just created. The listeners run in order, on the caller's thread. If SmsListener throws, does AnalyticsListener ever run? Does the order still get placed? Answering that is the real work of using this pattern — see the pitfalls below.
04

Observer pattern code examples

An order service that announces, and listeners that react.

public interface OrderListener {
    void onOrderPlaced(Order order);
}

public class OrderService {
    // CopyOnWriteArrayList: iteration is safe even if a listener unsubscribes
    // itself during notification, which is a classic source of
    // ConcurrentModificationException with a plain ArrayList.
    private final List<OrderListener> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(OrderListener l)   { listeners.add(l); }
    public void unsubscribe(OrderListener l) { listeners.remove(l); }

    public Order place(Cart cart) {
        Order order = repository.save(Order.from(cart));
        notifyPlaced(order);
        return order;
    }

    private void notifyPlaced(Order order) {
        for (OrderListener l : listeners) {
            try {
                l.onOrderPlaced(order);
            } catch (RuntimeException e) {
                // One broken listener must not lose the order or stop the others.
                log.error("listener {} failed", l.getClass().getSimpleName(), e);
            }
        }
    }
}

class EmailListener implements OrderListener {
    public void onOrderPlaced(Order order) { mailer.sendConfirmation(order); }
}

// In Spring this is usually ApplicationEventPublisher + @EventListener, which
// is the same pattern with the subscriber list managed by the container.
Read across the tabs and notice what every version spends its comments on: not subscribing — that's three lines everywhere — but unsubscribing safely, iterating a list that a callback may mutate, and one listener's exception not eating the others. C# has the pattern in the language as event, C++ makes unsubscription automatic with an RAII token, Go must snapshot outside the lock to avoid deadlock. Those three problems are Observer in production.
05

How to implement Observer

  1. Define the event, not the reaction. Name it after what happened in the domain — OrderPlaced, not SendEmail. If the name contains a verb aimed at one subscriber, you've built a disguised method call.
  2. Give the subject a listener collection plus subscribe/unsubscribe, and have subscribe return an unsubscribe handle rather than asking callers to keep the original reference.
  3. Pass the data the listener needs in the event. Making observers call back into the subject to ask what changed re-couples them.
  4. Iterate over a copy of the list, or use a copy-on-write collection — a listener unsubscribing itself mid-notification is normal, not exotic.
  5. Wrap each callback in its own try/catch, so one failing subscriber can't lose the others or fail the original operation.
  6. Decide, explicitly, whether notification is synchronous or queued — and write it down. This is the decision that determines whether a flaky SMS provider can fail a checkout.
06

When to use Observer — and when not to

Use it when one event genuinely has several independent consequences, when the set of consequences changes over time or by deployment, or when a low-level component must inform a high-level one without depending on it. UI frameworks, domain events and hot-reloading config are all this pattern.

Where it goes wrong

The lapsed-listener leak. A long-lived subject holding a reference to a short-lived subscriber keeps it alive forever. This is the single most common Observer bug in Java, C# and JavaScript alike. Always return and use an unsubscribe handle; consider weak references when the subject outlives subscribers.

Control flow you can't read. With ten listeners on one event, no stack trace and no "find usages" tells you what actually happens when an order is placed. Debugging becomes archaeology. Keep the listener set small and discoverable, and log the fan-out.

Hidden synchronous coupling. In-process observers run on the caller's thread, inside the caller's transaction. A listener that makes a slow HTTP call has just made checkout slow; one that throws may roll the order back. If listeners are allowed to fail independently, they need a queue, not a method call.

Notification storms. An observer that updates state which triggers another notification can loop, or turn one change into thousands. Batch or debounce at the subject.

You need…UseBecause
Several independent reactions, same process, must all succeed togetherObserver, synchronous, inside the transactionSimple, ordered, and failure is visible immediately.
Reactions that may fail or be slow without affecting the callerAn outbox row plus a queueA synchronous observer makes their failure your failure.
Subscribers in other services or processesA broker — Kafka, SNS, RabbitMQDirect references can't cross a process boundary, and you want durability.
Exactly one reaction that will never changeJust call itA subscriber list of one is indirection with no benefit.
Many objects talking to many objectsMediatorObserver is one-to-many. Mediator is the pattern for the many-to-many mesh.
07

Quick check

🧠 Quick check
A long-lived OrderService holds listeners. A short-lived CheckoutPage subscribes and is then discarded without unsubscribing. What happens?

In the wild

JavaScriptaddEventListener in the DOM and Node's EventEmitter — the pattern is the platform.
C#The event keyword and IObservable<T>/IObserver<T> — Observer is built into the language and the BCL.
JavaSpring's ApplicationEventPublisher + @EventListener, and Swing's ActionListener. Note java.util.Observable was deprecated in Java 9 — don't use it.
PythonDjango's post_save signals and Blinker — signal.connect(receiver) is subscribe, verbatim.
Gocontext.Context cancellation: ctx.Done() is a broadcast every goroutine can observe without the canceller knowing who's listening.
TypeScriptRxJS Subject and Angular's @Output() EventEmitter — Observer with operators layered on top.

Frequently asked questions

What is the difference between Observer and Pub/Sub?
Classic Observer is direct and in-process: the subject holds references to its observers and calls them itself, usually synchronously. Publish–subscribe puts a broker in between, so publishers and subscribers never reference each other and can live in different processes. The intent is the same; the failure modes are not. In-process Observer fails when a subscriber throws; broker pub/sub survives a subscriber being entirely offline, at the cost of infrastructure and eventual consistency.
Should observers be notified synchronously or asynchronously?
Synchronously when all the reactions are part of the same unit of work and must succeed or fail together — for example, decrementing stock as part of placing an order. Asynchronously (via a queue or outbox) when the reactions are peripheral and can be retried, like sending an email. The mistake is not choosing: a synchronous listener doing a network call quietly makes a third party's uptime part of your checkout's uptime.
How do I avoid memory leaks with the Observer pattern?
Return an unsubscribe handle from subscribe() and actually call it — in a UI teardown, a Dispose, a finally, or an RAII destructor as the C++ tab shows. When subscribers routinely outlive their usefulness and you can't guarantee cleanup, hold them weakly (WeakReference, weakref.WeakMethod) so the subject's list can't keep them alive.
What happens if one observer throws an exception?
Whatever you decide — which is why you must decide. By default in most languages the exception propagates and the remaining observers never run, and it may fail the original operation too. Wrapping each callback in its own try/catch keeps the fan-out going and logs the failure. Note that a C# multicast delegate stops at the first throw, so per-handler invocation is the only way around it there.
Is Observer still relevant with modern frameworks?
Very — you're mostly consuming it rather than writing it. Every UI event system, every reactive stream library, every framework's domain-event mechanism and every message broker client is this pattern. Writing your own subject is worth it for in-process domain events where you want the vocabulary and the decoupling without adding infrastructure.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.