Chain of Responsibility — how it works, Java implementation, real uses (filters, approvals, validation), loose coupling and drawbacks — and Observer — purpose, why java.util.Observer/Observable are deprecated and what to use instead, Observer vs Pub/Sub, staggered notifications, and thread-safety challenges.
Published September 25, 2026
Both patterns are everywhere in Java frameworks: servlet filters and the Spring Security filter chain are Chains of Responsibility, and Spring application events and UI listeners are Observers. Use those as your examples, and be ready for the concurrency follow-ups on Observer.
Short answer: It passes a request along a chain of handlers. Each handler either handles it, passes it on, or does some work and passes it on. The sender doesn't know which handler (or how many) will process it, which decouples the sender from the receivers.
Learn it in depth → Chain of Responsibility
Short answer: Observer defines a one-to-many dependency. When a subject changes state, all registered observers are notified automatically, and the subject doesn't know what they do. It's used for event listeners, UI updates, domain events and cache invalidation.
Learn it in depth → Observer Pattern
Short answer: Handlers share an interface (handle(request)), and each knows its successor, or the chain is iterated by a runner. Two common variants:
Short answer: In modern Java, a list of handler beans, iterated in order, is usually cleaner than linked next references:
public interface RefundRule { // the handler
Optional<Decision> evaluate(RefundRequest r); // empty = "not my concern, continue"
}
@Component @Order(1) class FraudHoldRule implements RefundRule {
public Optional<Decision> evaluate(RefundRequest r) { return r.flaggedForFraud() ? Optional.of(Decision.manualReview("fraud")) : Optional.empty(); }
}
@Component @Order(2) class AutoApproveSmallRule implements RefundRule {
public Optional<Decision> evaluate(RefundRequest r) { return r.amount().compareTo(new BigDecimal("1000")) <= 0 ? Optional.of(Decision.approve()) : Optional.empty(); }
}
@Service
class RefundDecider {
private final List<RefundRule> rules; // Spring injects them in @Order order
RefundDecider(List<RefundRule> rules) { this.rules = rules; }
Decision decide(RefundRequest r) {
return rules.stream().map(rule -> rule.evaluate(r)).flatMap(Optional::stream)
.findFirst().orElse(Decision.manualReview("no rule matched")); // explicit default: never unhandled
}
}
Short answer:
SecurityFilterChain, Netty handlers, OkHttp interceptors).Short answer: The sender only knows the chain's entry point, not the concrete handlers. Handlers don't know about each other either, beyond "next". You can add, remove or reorder handlers (often through configuration or @Order) without changing the sender or the other handlers. Each handler has one responsibility.
Short answer:
Short answer: Use it when several independent parts must react to a change, and the source shouldn't depend on them. For example, when an order is placed: send an email, award loyalty points, update analytics. In Spring:
@Service
class OrderService {
private final ApplicationEventPublisher events;
@Transactional public Order place(Cart c) {
Order o = repo.save(Order.from(c));
events.publishEvent(new OrderPlaced(o.getId())); // the subject doesn't know its observers
return o;
}
}
@Component class LoyaltyObserver {
@TransactionalEventListener void on(OrderPlaced e) { loyalty.award(e.orderId()); }
}
Observer and Observable?Short answer: java.util.Observable was a class that kept a list of Observers. The subject called setChanged() then notifyObservers(arg), and each observer's update(Observable, Object) ran. These types have been deprecated since Java 9:
Observable is a class, so subjects can't extend anything else.Object arg).Use instead:
OrderListener), with a CopyOnWriteArrayList of listeners.java.beans.PropertyChangeSupport, for property changes.java.util.concurrent.Flow (reactive streams), or Reactor, for asynchronous streams with back-pressure.Common trap: presenting Observer/Observable as the recommended Java mechanism today.
Short answer:
Pub/Sub scales across services and survives consumer downtime. Observer is simpler, immediate and local.
Short answer:
@Order on listeners).@EventListener(condition = "#e.amount > 1000")).@TransactionalEventListener(phase = AFTER_COMMIT)).For cross-service or durable timing, publish to a broker, and let each consumer process at its own pace.
Short answer:
ConcurrentModificationException. Use CopyOnWriteArrayList.private final List<PriceListener> listeners = new CopyOnWriteArrayList<>();
void publish(PriceChanged event) { // immutable event, no lock held while notifying
for (PriceListener l : listeners) {
try { l.onPriceChanged(event); } catch (RuntimeException ex) { log.warn("listener failed", ex); }
}
}
Q: How do Spring's synchronous event listeners behave with exceptions?
A: They run in the publisher's thread. An exception propagates to the publisher, and can roll back its transaction. Use @Async and/or @TransactionalEventListener(AFTER_COMMIT) for side effects that must not affect the main flow.
Q: Chain of Responsibility vs Decorator? A: Both wrap or forward calls. Decorators always delegate, and add behaviour, while a chain handler may stop the request (handle it or reject it). Chains are also usually assembled as lists, and decorators as nested wrappers.
Q: How do you make an event-driven flow observable in production? A: Give every event an ID and a correlation (trace) ID, log each listener's handling, and use tracing across asynchronous boundaries. Monitor queue lag and dead-letter counts for broker-based pub/sub.
Q: What is the "lapsed listener" problem? A: A listener that's registered but never removed keeps its owner reachable, so it's never garbage-collected, and it keeps receiving events. Tie listener registration to lifecycle callbacks, or use weak references.